Created
July 18, 2020 21:16
-
-
Save CodeSlicing/defa66329759444713568375a7730786 to your computer and use it in GitHub Desktop.
Simplified version of the SwiftUI icon gist where the emphasis is on matchedGeometryEffect rather than recreating the SwiftUI icon to demonstrate the use of this modifier with multiple views and delays on the animations (Xcode 12 required)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SwiftUIIconMatchedGeometryEffectSimplifiedBetterDelay.swift | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
// of the Software, and to permit persons to whom the Software is furnished to do so, | |
// subject to the following conditions: | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A | |
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | |
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN | |
// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
// | |
// Created by Adam Fordyce on 18/07/2020. | |
// Copyright © 2020 Adam Fordyce. All rights reserved. | |
// | |
import SwiftUI | |
import PureSwiftUI | |
private func generateColor(_ red: Double, _ green: Double, _ blue: Double) -> Color { | |
Color(red: red / 255, green: green / 255, blue: blue / 255) | |
} | |
private let swiftLayoutConfig = LayoutGuideConfig.grid(columns: 1000, rows: 1000) | |
private let numIcons = 3 | |
private let numSwifts = 4 | |
private let baseSize: CGFloat = 300 | |
private let vStackSpacing: CGFloat = 20 | |
private func sizeForIconIndex(_ index: Int) -> CGFloat { | |
baseSize * CGFloat(1 - index * 0.4) | |
} | |
private class SwiftUIIconState: ObservableObject { | |
@Published var positions = [CGPoint](repeating: .zero, count: numIcons) | |
@Published var rectangles = [CGRect](repeating: .zero, count: numIcons) | |
@Published var currentIconIndexes = [Int](repeating: 0, count: numSwifts) | |
@Published var currentOffsets = [CGSize](repeating: .zero, count: numSwifts) | |
@Published var dragging = false | |
var primaryIconIndex: Int { | |
currentIconIndexes[0] | |
} | |
var primaryPosition: CGPoint { | |
positions[primaryIconIndex] | |
} | |
func findContainingRect(for position: CGPoint) -> Int? { | |
for (index, rect) in rectangles.enumerated() { | |
if rect.contains(position) { | |
return index | |
} | |
} | |
return nil | |
} | |
} | |
struct SwiftUIIconMatchedGeometryEffectSimplifiedBetterDelay: View { | |
@StateObject fileprivate var swiftUIIconState = SwiftUIIconState() | |
var body: some View { | |
ZStack { | |
IconBackgrounds() | |
Swifts() | |
} | |
.environmentObject(swiftUIIconState) | |
} | |
} | |
private struct IconBackgrounds: View { | |
var body: some View { | |
VStack(spacing: vStackSpacing) { | |
ForEach(0..<numIcons) { index in | |
let size = sizeForIconIndex(index) | |
Color.white | |
.frame(size) | |
.cornerRadius(0.2 * size) | |
.shadow(5) | |
} | |
} | |
} | |
} | |
private struct Swifts: View { | |
@EnvironmentObject private var swiftUIIconState: SwiftUIIconState | |
@Namespace private var namespace | |
var body: some View { | |
VStack(spacing: vStackSpacing) { | |
ForEach(0..<numIcons) { iconIndex in | |
let size = sizeForIconIndex(iconIndex) | |
ZStack { | |
ForEach(0..<numSwifts) { (swiftIndex: Int) in | |
if swiftUIIconState.currentIconIndexes[swiftIndex] == iconIndex { | |
RenderIf(swiftIndex == 0) { | |
Swift() | |
.gesture(DragGesture(coordinateSpace: .global).onChanged { value in | |
swiftUIIconState.dragging = true | |
for swiftIndex in 0..<numSwifts { | |
withAnimation(Animation.default.delay(delayForIndex(swiftIndex))) { | |
swiftUIIconState.currentOffsets[swiftIndex] = value.translation | |
} | |
} | |
}.onEnded { value in | |
swiftUIIconState.dragging = false | |
let iconIndex = swiftUIIconState.findContainingRect(for: value.location) ?? swiftUIIconState.primaryIconIndex | |
for swiftIndex in 0..<numSwifts { | |
withAnimation(Animation.default.delay(delayForIndex(swiftIndex))) { | |
swiftUIIconState.currentOffsets[swiftIndex] = .zero | |
swiftUIIconState.currentIconIndexes[swiftIndex] = iconIndex | |
} | |
} | |
}) | |
.zIndex(0) | |
}.elseRender { | |
let zIndexValue = Double(-swiftIndex) | |
Swift() | |
.fill(colorForIndex(swiftIndex)) | |
.zIndex(zIndexValue) | |
} | |
.matchedGeometryEffect(id: "swift-\(swiftIndex)", in: namespace) | |
.offset(offsetForIndex(swiftIndex, size: size)) | |
.offset(swiftUIIconState.currentOffsets[swiftIndex]) | |
.shadow(5) | |
} else { | |
Color.clear | |
} | |
} | |
.frame(sizeForIconIndex(iconIndex)) | |
} | |
.frame(size) | |
.zIndex(iconIndex == swiftUIIconState.primaryIconIndex ? 1 : -1) | |
.geometryReader { (geo: GeometryProxy) in | |
swiftUIIconState.positions[iconIndex] = geo.globalCenter | |
swiftUIIconState.rectangles[iconIndex] = geo.globalFrame | |
} | |
} | |
} | |
} | |
func colorForIndex(_ index: Int) -> Color { | |
Color(white: 0.15 + index * 0.2) | |
} | |
func delayForIndex(_ index: Int) -> Double { | |
Double(index * 0.15) | |
} | |
func offsetForIndex(_ index: Int, size: CGFloat) -> CGPoint { | |
let offsetBase = 0.1 * size | |
return CGPoint(-offsetBase * CGFloat(index)) | |
} | |
} | |
private typealias Curve = (p: CGPoint, cp1: CGPoint, cp2: CGPoint) | |
private struct Swift: Shape { | |
func path(in rect: CGRect) -> Path { | |
var path = Path() | |
var g = swiftLayoutConfig.layout(in: rect) | |
.scaled(0.8) | |
.offset(.point(rect.widthScaled(0.15))) | |
let p1 = g[845,817] | |
let p2 = g[750,750] | |
let p3 = g[510,814] | |
let p4 = g[102,575] | |
let p5 = g[527,625] | |
let p6 = g[175,244] | |
let p7 = g[475,470] | |
let p8 = g[264,204] | |
let p9 = g[630,500] | |
let p10 = g[564,140] | |
let p11 = g[790,617] | |
var curves: [Curve] = [] | |
curves.append(Curve(p2, g[815,747], p2)) | |
curves.append(Curve(p3, g[715,747], g[615,814])) | |
curves.append(Curve(p4, g[235,814], p4)) | |
curves.append(Curve(p5, g[335,754], p5)) | |
curves.append(Curve(p6, g[305,454], p6)) | |
curves.append(Curve(p7, g[425,454], p7)) | |
curves.append(Curve(p8, p7, g[405,404])) | |
curves.append(Curve(p9, g[465,404], p9)) | |
curves.append(Curve(p10, g[700,304], p10)) | |
curves.append(Curve(p11, g[750,254], g[850,464])) | |
curves.append(Curve(p1, g[891,750], p1)) | |
path.move(p1) | |
for curve in curves { | |
path.curve(curve.p, cp1: curve.cp1, cp2: curve.cp2, showControlPoints: false) | |
} | |
return path | |
} | |
} | |
struct SwiftUIIconMatchedGeometryEffectSimplifiedBetterDelay_Previews: PreviewProvider { | |
struct SwiftUIIconMatchedGeometryEffectSimplifiedBetterDelay_Harness: View { | |
var body: some View { | |
SwiftUIIconMatchedGeometryEffectSimplifiedBetterDelay() | |
} | |
} | |
static var previews: some View { | |
SwiftUIIconMatchedGeometryEffectSimplifiedBetterDelay_Harness() | |
.padding(50) | |
.previewSizeThatFits() | |
.showLayoutGuides(true) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment