Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save CodeSlicing/feb79d1e2023eda4c460536f3e42ed71 to your computer and use it in GitHub Desktop.
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)
//
// 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