Last active
July 18, 2020 23:15
-
-
Save CodeSlicing/feb79d1e2023eda4c460536f3e42ed71 to your computer and use it in GitHub Desktop.
Uses matchedGeometryEffect to drag swifts between icons - emphasis is trying to recreate as accurately as possible the SwiftUI icon as a way of demonstrating using this modifier with multiple views with delays on 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 bgDarkBlue = generateColor(0, 4, 127) | |
private let bgLightBlue = generateColor(1, 239, 245) | |
private let swift1Color1 = generateColor(9, 144, 250) | |
private let swift1Color2 = generateColor(19, 63, 199) | |
private let swift2Color1 = generateColor(1, 224, 248) | |
private let swift2Color2 = generateColor(7, 127, 254) | |
private let swift3Color1 = bgLightBlue | |
private let swift3Color2 = generateColor(1, 203, 249) | |
private let swiftGradients: [LinearGradient] = [ | |
LinearGradient([swift1Color1, swift1Color2], to: .init(0.75, 1)), | |
LinearGradient([swift2Color1, swift2Color2], to: .init(0.75, 1)), | |
LinearGradient([swift3Color1, swift3Color2], to: .init(0.75, 1)), | |
] | |
private let swiftLayoutConfig = LayoutGuideConfig.grid(columns: 1000, rows: 1000) | |
private let numSwifts = 4 | |
private let numIcons = 3 | |
private let baseSize: CGFloat = 300 | |
private let vStackSpacing: CGFloat = 20 | |
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 | |
} | |
} | |
private func sizeForIconIndex(_ index: Int) -> CGFloat { | |
baseSize * CGFloat(1 - index * 0.4) | |
} | |
struct SwiftUIIconMatchedGeometryEffectBetterDelay: View { | |
@StateObject fileprivate var swiftUIIconState = SwiftUIIconState() | |
@Namespace private var namespace | |
var body: some View { | |
let size = sizeForIconIndex(swiftUIIconState.primaryIconIndex) | |
return ZStack { | |
IconBackgrounds() | |
IconContents() | |
.mask(IconMask(size: size)) | |
} | |
.environmentObject(swiftUIIconState) | |
} | |
} | |
private struct IconMask: View { | |
@EnvironmentObject private var swiftUIIconState: SwiftUIIconState | |
let size: CGFloat | |
var body: some View { | |
let dragging = swiftUIIconState.dragging | |
return ZStack { | |
RoundedRectangle(size * 0.2) | |
.frame(size) | |
.scaleIf(dragging, maskScaleFactorForSize(size)) | |
.blurIf(dragging, 50) | |
.offsetToPositionIfNot(dragging, swiftUIIconState.primaryPosition, in: .global) | |
.animation(Animation.easeInOut(duration: dragging ? 0.3 : 0.6).delay(dragging ? 0 : 0.1)) | |
}.greedyFrame() | |
} | |
func maskScaleFactorForSize(_ size: CGFloat) -> CGSize { | |
let widthScale = UIScreen.mainWidth / size | |
let heightScale = UIScreen.mainHeight / size | |
return .size(widthScale * 1.5, heightScale * 1.5) | |
} | |
} | |
private struct IconBackgrounds: View { | |
@EnvironmentObject private var swiftUIIconState: SwiftUIIconState | |
private static let gradient = LinearGradient([bgDarkBlue, bgLightBlue], to: .top) | |
var body: some View { | |
VStack(spacing: vStackSpacing) { | |
ForEach(0..<numIcons) { index in | |
let size = sizeForIconIndex(index) | |
let indexIsCurrentIndex = index == swiftUIIconState.primaryIconIndex | |
let dragging = swiftUIIconState.dragging | |
let draggingOrNotCurrentIndex = dragging || !indexIsCurrentIndex | |
ZStack { | |
IconBackgrounds.gradient | |
.opacityIf(draggingOrNotCurrentIndex, 0) | |
.animation(Animation.easeInOut(duration: 0.5)) | |
Circle().fill(bgLightBlue).frame(0.57 * size).blur(size / 15.0).blendMode(.hardLight) | |
.opacityIf(draggingOrNotCurrentIndex, 0) | |
.animation(Animation.easeInOut(duration: dragging ? 0.1 : 0.3).delay(dragging ? 0.1 : 0.4)) | |
.offset(.square(-size / 3)) | |
} | |
.background(Color.white) | |
.cornerRadius(size * 0.2) | |
.geometryReader { (geo: GeometryProxy) in | |
swiftUIIconState.positions[index] = geo.globalCenter | |
swiftUIIconState.rectangles[index] = geo.globalFrame | |
} | |
.frame(size) | |
.shadow(5) | |
} | |
} | |
} | |
} | |
private extension AnyTransition { | |
static var tailMask: AnyTransition { | |
AnyTransition.asymmetric(insertion: AnyTransition.opacity.animation(Animation.default.delay(0.4)), removal: AnyTransition.opacity.animation(.default)) | |
} | |
} | |
private struct IconContents: View { | |
@EnvironmentObject private var swiftUIIconState: SwiftUIIconState | |
@Namespace private var namespace | |
@State private var showingMask = false | |
var body: some View { | |
let dragging = swiftUIIconState.dragging | |
return VStack(spacing: vStackSpacing) { | |
ForEach(0..<numIcons) { iconIndex in | |
let indexIsCurrentIndex = iconIndex == swiftUIIconState.primaryIconIndex | |
let size = sizeForIconIndex(iconIndex) | |
ZStack { | |
if indexIsCurrentIndex { | |
Color.clear | |
.overlay( | |
Capsule() | |
.fill(bgLightBlue) | |
.frame(size * 0.5, size * 0.3) | |
.rotate(45.degrees) | |
.offset(.point(size * -0.4)).blur(size * 0.05) | |
.opacity(0.9) | |
).cornerRadius(size * 0.2) | |
.opacityIf(dragging, 0) | |
.frame(size) | |
.zIndex(0) | |
.transition(.tailMask) | |
} | |
StackedSwifts(iconIndex: iconIndex, size: size, namespace: namespace) | |
} | |
.frame(size) | |
.zIndex(indexIsCurrentIndex ? 1 : -1) | |
} | |
} | |
.greedyFrame() | |
} | |
} | |
private struct StackedSwifts: View { | |
@EnvironmentObject private var swiftUIIconState: SwiftUIIconState | |
let iconIndex: Int | |
let size: CGFloat | |
let namespace: Namespace.ID | |
var body: some View { | |
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 | |
let iconIndex = swiftUIIconState.findContainingRect(for: value.location) ?? swiftUIIconState.primaryIconIndex | |
for swiftIndex in 0..<numSwifts { | |
withAnimation(Animation.default.delay(delayForIndex(swiftIndex))) { | |
swiftUIIconState.currentIconIndexes[swiftIndex] = iconIndex | |
swiftUIIconState.currentOffsets[swiftIndex] = .zero | |
} | |
} | |
swiftUIIconState.dragging = false | |
}) | |
.zIndex(1) | |
}.elseRender { | |
let zIndexValue = Double(-swiftIndex) | |
Swift() | |
.fill(swiftGradients[swiftIndex - 1]) | |
.zIndex(zIndexValue) | |
} | |
.matchedGeometryEffect(id: "swift-\(swiftIndex)", in: namespace) | |
.offset(offsetForIndex(swiftIndex, size: size)) | |
.offset(swiftUIIconState.currentOffsets[swiftIndex]) | |
} else { | |
Color.clear | |
} | |
} | |
} | |
func delayForIndex(_ index: Int) -> Double { | |
index * 0.1 | |
} | |
func offsetForIndex(_ index: Int, size: CGFloat) -> CGPoint { | |
let extra = 0.04 * size | |
let offsetBase = 0.083 * size | |
return CGPoint(-offsetBase * CGFloat(index) - (extra * index)) | |
} | |
func scaleForIndex(_ index: Int) -> CGSize { | |
CGSize(1 - CGFloat(index) * 0.03) | |
} | |
} | |
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) | |
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 SwiftUIIconMatchedGeometryEffectBetterDelay_Previews: PreviewProvider { | |
struct SwiftUIIconMatchedGeometryEffectBetterDelay_Harness: View { | |
var body: some View { | |
SwiftUIIconMatchedGeometryEffectBetterDelay() | |
} | |
} | |
static var previews: some View { | |
SwiftUIIconMatchedGeometryEffectBetterDelay_Harness() | |
.padding(50) | |
.previewSizeThatFits() | |
.showLayoutGuides(true) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment