├── Examples ├── MatchedAnimationDemo │ ├── Shared │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── MatchedAnimationDemoApp.swift │ │ └── ContentView.swift │ ├── MatchedAnimationDemo.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ ├── MatchedAnimationDemo (iOS).xcscheme │ │ │ │ └── MatchedAnimationDemo (macOS).xcscheme │ │ └── project.pbxproj │ └── macOS │ │ └── macOS.entitlements └── Package.swift ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── MatchedAnimation.xcscheme ├── Package.swift ├── LICENSE ├── README.md ├── Sources └── MatchedAnimation │ ├── MatchedAnimation.swift │ ├── Animation.swift │ └── ParsedAnimation.swift └── Tests └── MatchedAnimationTests └── AnimationTests.swift /Examples/MatchedAnimationDemo/Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Examples/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Examples", 7 | products: [], 8 | targets: [] 9 | ) 10 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/MatchedAnimationDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/MatchedAnimationDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/Shared/MatchedAnimationDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MatchedAnimationDemoApp.swift 3 | // Shared 4 | // 5 | // Created by Ryan Carver on 12/17/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct MatchedAnimationDemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/macOS/macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-matched-animation", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v11) 11 | ], 12 | products: [ 13 | .library( 14 | name: "MatchedAnimation", 15 | targets: ["MatchedAnimation"]), 16 | ], 17 | targets: [ 18 | .target( 19 | name: "MatchedAnimation", 20 | dependencies: []), 21 | .testTarget( 22 | name: "MatchedAnimationTests", 23 | dependencies: ["MatchedAnimation"]), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ryan Carver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MatchedAnimation 2 | 3 | Match animations in SwiftUI and UIKit/AppKit. 4 | 5 | ```swift 6 | /// Draw a box in UIKit 7 | struct BoxView: UIViewRepresentable { 8 | let size: CGSize 9 | let position: CGPoint 10 | func makeUIView(context: Context) -> UIView { 11 | let view = UIView(frame: .zero) 12 | let box = UIView() 13 | box.backgroundColor = .red 14 | view.addSubview(box) 15 | return view 16 | } 17 | func updateUIView(_ view: UIView, context: Context) { 18 | // 🌟 Wrap changes to UIKit with UIPropertyAnimator that matches the SwiftUI Animation 19 | withMatchedAnimation(context.transaction.animation) { 20 | view.subviews.first?.frame = CGRect(origin: position, size: size) 21 | } 22 | } 23 | } 24 | ``` 25 | 26 | ![RocketSim_Recording_iPhone_13_2021-12-18_15 22 41](https://user-images.githubusercontent.com/2343/146658069-dba5fd25-020d-4a2d-aa00-d8e87d3b98ff.gif) 27 | 28 | ## Limitations 29 | 30 | * FIXME: bezier conversions are not quite right 31 | * Currently only supports easing animations, not spring animations 32 | 33 | ## License 34 | 35 | This library is released under the MIT license. See LICENSE for details. 36 | -------------------------------------------------------------------------------- /Sources/MatchedAnimation/MatchedAnimation.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if canImport(UIKit) 4 | /// Perform the body with animation, matching animations in UIKit and SwiftUI. 5 | /// 6 | /// If the animation cannot be matched, no animation is performed. 7 | public func withMatchedAnimation(_ animation: Animation? = .default, _ body: @escaping () -> Void) { 8 | guard let animation = animation, 9 | let animator = animation.uiPropertyAnimator 10 | else { 11 | return body() 12 | } 13 | animator.addAnimations(body) 14 | withAnimation(animation) { 15 | animator.startAnimation() 16 | } 17 | } 18 | #endif 19 | 20 | #if canImport(AppKit) 21 | /// Perform the body with animation, matching animations in AppKit and SwiftUI. 22 | /// 23 | /// Defaults to implicit animations enabled. 24 | /// If the animation cannot be matched, no animation is performed. 25 | public func withMatchedAnimation(_ animation: Animation?, allowsImplicitAnimation: Bool = true, _ body: @escaping () -> Void) { 26 | guard let animation = animation else { 27 | withoutNSAnimation(body) 28 | return 29 | } 30 | NSAnimationContext.runAnimationGroup { context in 31 | do { 32 | try animation.applyToContext( 33 | context, 34 | allowsImplicitAnimation: allowsImplicitAnimation 35 | ) 36 | withAnimation(animation, body) 37 | } catch { 38 | withoutNSAnimation(body) 39 | } 40 | } 41 | } 42 | // Remove default animations that would be used by animator() proxy. 43 | private func withoutNSAnimation(_ body: @escaping () -> Void) { 44 | NSAnimationContext.runAnimationGroup { context in 45 | context.duration = 0 46 | body() 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /Tests/MatchedAnimationTests/AnimationTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import XCTest 3 | @testable import MatchedAnimation 4 | 5 | final class AnimationDurationTests: XCTestCase { 6 | func test_duration() { 7 | XCTAssertEqual(Animation.default.duration, 0.35) 8 | XCTAssertEqual(Animation.linear(duration: 0.5).duration, 0.5) 9 | XCTAssertEqual(Animation.linear(duration: 1.0).duration, 1.0) 10 | XCTAssertEqual(Animation.easeInOut(duration: 1.0).duration, 1.0) 11 | } 12 | func test_duration_speed() { 13 | XCTAssertEqual(Animation.easeInOut(duration: 1.0).speed(2).duration, 2.0) 14 | } 15 | } 16 | 17 | final class AnimationControlPointTests: XCTestCase { 18 | func test_linear() throws { 19 | let p = try XCTUnwrap(Animation.linear.controlPoints) 20 | print(Animation.linear.parse()) 21 | XCTAssertEqual(p.cp1, CGPoint(x: 2.0/3.0, y: 2.0/3.0)) 22 | XCTAssertEqual(p.cp2, CGPoint(x: 1, y: 1)) 23 | } 24 | func test_timingCurve_linear() throws { 25 | let p = try XCTUnwrap(Animation.timingCurve(0, 0, 1, 1).controlPoints) 26 | XCTAssertEqual(p.cp1, CGPoint(x: 2.0/3.0, y: 2.0/3.0)) 27 | XCTAssertEqual(p.cp2, CGPoint(x: 1, y: 1)) 28 | } 29 | func test_timingCurve_other() throws { 30 | let p = try XCTUnwrap(Animation.timingCurve(0.17,0.67,0.85,0.65).controlPoints) 31 | print(Animation.timingCurve(0.17,0.67,0.85,0.65) 32 | .parse()) 33 | XCTAssertEqual(p.cp1, CGPoint(x: 0.17, y: 0.67)) 34 | XCTAssertEqual(p.cp2, CGPoint(x: 0.85, y: 0.65)) 35 | } 36 | func test_default() throws { 37 | // let p = try XCTUnwrap(Animation.linear.controlPoints) 38 | try XCTSkipIf(true, "unknown values") 39 | } 40 | func test_easeIn() throws { 41 | // let p = try XCTUnwrap(Animation.easeIn.controlPoints) 42 | try XCTSkipIf(true, "unknown values") 43 | } 44 | func test_easeOut() throws { 45 | // let p = try XCTUnwrap(Animation.easeOut.controlPoints) 46 | try XCTSkipIf(true, "unknown values") 47 | } 48 | func test_easeInOut() throws { 49 | // let p = try XCTUnwrap(Animation.easeInOut.controlPoints) 50 | try XCTSkipIf(true, "unknown values") 51 | } 52 | func test_spring() { 53 | XCTAssertNil(Animation.spring().controlPoints) 54 | } 55 | func test_interactiveSpring() { 56 | XCTAssertNil(Animation.interactiveSpring().controlPoints) 57 | } 58 | func test_interpolatingSpring() { 59 | XCTAssertNil(Animation.interpolatingSpring(stiffness: 0.5, damping: 0.5).controlPoints) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/MatchedAnimation/Animation.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if canImport(UIKit) 4 | extension Animation { 5 | /// Create a UIPropertyAnimator that matches this Animation. 6 | /// 7 | /// Returns nil if anmy required values of the animation couldn't be found. 8 | public var uiPropertyAnimator: UIViewPropertyAnimator? { 9 | let p = parse() 10 | guard let duration = p.duration, 11 | let controlPoints = p.controlPoints 12 | else { return nil } 13 | return UIViewPropertyAnimator( 14 | duration: duration, 15 | controlPoint1: controlPoints.cp1, 16 | controlPoint2: controlPoints.cp2 17 | ) 18 | } 19 | } 20 | #endif 21 | 22 | #if canImport(AppKit) 23 | extension Animation { 24 | /// Set the timing function and duration on the animation context. 25 | /// - Parameters: 26 | /// - context: The context to modify 27 | /// - allowsImplicitAnimation: Whether to enable implicit animations. 28 | /// - Throws: 29 | /// - Error if the animation can't be applied. 30 | public func applyToContext(_ context: NSAnimationContext, allowsImplicitAnimation: Bool) throws { 31 | let p = parse() 32 | guard let duration = p.duration else { 33 | throw MatchedAnimationError.missingDuration 34 | } 35 | guard let controlPoints = p.controlPoints else { 36 | throw MatchedAnimationError.missingContolPoints 37 | } 38 | context.duration = duration 39 | context.timingFunction = CAMediaTimingFunction(controlPoints: controlPoints) 40 | context.allowsImplicitAnimation = allowsImplicitAnimation 41 | } 42 | } 43 | extension CAMediaTimingFunction { 44 | convenience init(controlPoints p: ControlPoints) { 45 | self.init(controlPoints: Float(p.cp1.x), Float(p.cp1.y), Float(p.cp2.x), Float(p.cp2.y)) 46 | } 47 | } 48 | #endif 49 | 50 | enum MatchedAnimationError: Error { 51 | case missingDuration 52 | case missingContolPoints 53 | } 54 | 55 | extension Animation { 56 | 57 | func parse() -> ParsedAnimation { 58 | var output = ParsedAnimation() 59 | guard let base = Mirror(reflecting: self).children.first else { return output } 60 | parseAnimation(&output, mirror: Mirror(reflecting: base.value)) 61 | return output 62 | } 63 | 64 | /// The duration of the animation, if available. 65 | var duration: Double? { 66 | parse().duration 67 | } 68 | 69 | /// The cubic control points for the animation, if available. 70 | var controlPoints: ControlPoints? { 71 | parse().controlPoints 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/MatchedAnimation.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | }, 93 | { 94 | "idiom" : "mac", 95 | "scale" : "1x", 96 | "size" : "16x16" 97 | }, 98 | { 99 | "idiom" : "mac", 100 | "scale" : "2x", 101 | "size" : "16x16" 102 | }, 103 | { 104 | "idiom" : "mac", 105 | "scale" : "1x", 106 | "size" : "32x32" 107 | }, 108 | { 109 | "idiom" : "mac", 110 | "scale" : "2x", 111 | "size" : "32x32" 112 | }, 113 | { 114 | "idiom" : "mac", 115 | "scale" : "1x", 116 | "size" : "128x128" 117 | }, 118 | { 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "128x128" 122 | }, 123 | { 124 | "idiom" : "mac", 125 | "scale" : "1x", 126 | "size" : "256x256" 127 | }, 128 | { 129 | "idiom" : "mac", 130 | "scale" : "2x", 131 | "size" : "256x256" 132 | }, 133 | { 134 | "idiom" : "mac", 135 | "scale" : "1x", 136 | "size" : "512x512" 137 | }, 138 | { 139 | "idiom" : "mac", 140 | "scale" : "2x", 141 | "size" : "512x512" 142 | } 143 | ], 144 | "info" : { 145 | "author" : "xcode", 146 | "version" : 1 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/MatchedAnimationDemo.xcodeproj/xcshareddata/xcschemes/MatchedAnimationDemo (iOS).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/MatchedAnimationDemo.xcodeproj/xcshareddata/xcschemes/MatchedAnimationDemo (macOS).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Sources/MatchedAnimation/ParsedAnimation.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// The result of parsing an Animation. 4 | struct ParsedAnimation { 5 | var type: AnimationType? 6 | var baseDuration: Double? 7 | var speedMultipler: Double? 8 | } 9 | 10 | extension ParsedAnimation { 11 | var duration: Double? { 12 | guard let d = baseDuration else { return nil } 13 | guard let s = speedMultipler else { return d } 14 | return d * s 15 | } 16 | var controlPoints: ControlPoints? { 17 | switch type { 18 | case .bezier(let points): 19 | return points.controlPoints 20 | case .none: 21 | return nil 22 | } 23 | } 24 | } 25 | 26 | enum AnimationType { 27 | case bezier(BezierPoints) 28 | } 29 | 30 | /// Quadradic bezier; how Animation represents itself internally. 31 | struct BezierPoints { 32 | let ax: CGFloat 33 | let ay: CGFloat 34 | let bx: CGFloat 35 | let by: CGFloat 36 | let cx: CGFloat 37 | let cy: CGFloat 38 | } 39 | 40 | /// Cubic bezier: how we convert to CoreGraphics, UIKit, and AppKit animations. 41 | struct ControlPoints: Equatable { 42 | var cp1: CGPoint 43 | var cp2: CGPoint 44 | } 45 | 46 | extension BezierPoints { 47 | var controlPoints: ControlPoints { 48 | let cp1 = CGPoint( 49 | x: ((2 * cx) - ax) / 3, 50 | y: ((2 * cy) - ay) / 3 51 | ) 52 | let cp2 = CGPoint( 53 | x: ((2 * cx) + bx) / 3, 54 | y: ((2 * cy) + by) / 3 55 | ) 56 | return ControlPoints(cp1: cp1, cp2: cp2) 57 | } 58 | } 59 | 60 | func parseAnimation(_ output: inout ParsedAnimation, mirror: Mirror) { 61 | let labels = mirror.children.map(\.label) 62 | switch labels { 63 | case ["duration", "curve"]: 64 | parseBezierAnimation(&output, mirror: mirror) 65 | case ["animation", "speed"]: 66 | parseSpeedAnimation(&output, mirror: mirror) 67 | default: 68 | break 69 | } 70 | } 71 | 72 | func parseBezierAnimation(_ output: inout ParsedAnimation, mirror: Mirror) { 73 | for c in mirror.children { 74 | switch c.label { 75 | case "duration": 76 | output.baseDuration = c.value as? Double 77 | case "curve": 78 | parseBezierTimingCurve(&output, mirror: Mirror(reflecting: c.value)) 79 | default: 80 | break 81 | } 82 | } 83 | } 84 | 85 | func parseSpeedAnimation(_ output: inout ParsedAnimation, mirror: Mirror) { 86 | for c in mirror.children { 87 | switch c.label { 88 | case "speed": 89 | output.speedMultipler = c.value as? Double 90 | case "animation": 91 | parseAnimation(&output, mirror: Mirror(reflecting: c.value)) 92 | default: 93 | break 94 | } 95 | } 96 | } 97 | 98 | func parseBezierTimingCurve(_ output: inout ParsedAnimation, mirror: Mirror) { 99 | var ax, ay, bx, by, cx, cy: Double? 100 | for c in mirror.children { 101 | switch c.label { 102 | case "ax": ax = c.value as? Double 103 | case "ay": ay = c.value as? Double 104 | case "bx": bx = c.value as? Double 105 | case "by": by = c.value as? Double 106 | case "cx": cx = c.value as? Double 107 | case "cy": cy = c.value as? Double 108 | default: 109 | break 110 | } 111 | } 112 | if let ax = ax, let ay = ay, let bx = bx, let by = by, let cx = cx, let cy = cy { 113 | output.type = .bezier(BezierPoints(ax: ax, ay: ay, bx: bx, by: by, cx: cx, cy: cy)) 114 | } 115 | } 116 | 117 | func xcontrolPoints() -> ControlPoints? { 118 | 119 | /// The SwiftUI Animation to match 120 | let animation = Animation.easeInOut(duration: 1) 121 | print("input", animation) 122 | //BezierAnimation(duration: 1.0, curve: SwiftUI.(unknown context at $7fff5d38c400).BezierTimingCurve(ax: -2.0, bx: 3.0, cx: 0.0, ay: -2.0, by: 3.0, cy: 0.0)) 123 | //BezierAnimation(duration: 1.0, curve: SwiftUI.(unknown context at $7fff5d38c400).BezierTimingCurve(ax: 0.52, bx: -0.78, cx: 1.26, ay: -2.0, by: 3.0, cy: 0.0)) 124 | 125 | /// Copy the quadratic points 126 | let startA = CGPoint(x: -0.52, y: -3.0) 127 | let endA = CGPoint(x: -0.78, y: 3.0) 128 | let controlA = CGPoint(x: 1.26, y: 0.0) 129 | 130 | /// Copy the quadratic points 131 | let n: CGFloat = 1 132 | let start = CGPoint(x: startA.x/n, y: startA.y/n) 133 | let end = CGPoint(x: endA.x/n, y: endA.y/n) 134 | let control = CGPoint(x: controlA.x/n, y: controlA.y/n) 135 | 136 | let cp1 = CGPoint( 137 | x: ((2 * control.x) + start.x) / 3, 138 | y: ((2 * control.y) + start.y) / 3 139 | ) 140 | let cp2 = CGPoint( 141 | x: ((2 * control.x) + end.x) / 3, 142 | y: ((2 * control.y) + end.y) / 3 143 | ) 144 | 145 | // Constructing an animation from control points matches SwiftUI and UIKit 146 | let curve = Animation.timingCurve(cp1.x, cp1.y, cp2.x, cp2.y, duration: 1) 147 | print("curve", curve) 148 | 149 | print("cp1", cp1) 150 | print("cp2", cp2) 151 | 152 | return ControlPoints(cp1: cp1, cp2: cp1) 153 | } 154 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/Shared/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MatchedAnimation 3 | 4 | struct ContentView: View { 5 | 6 | static let animation: Animation? = .easeInOut(duration: 1) 7 | 8 | @State var flag: Bool = false 9 | @State var isAnimated: Bool = true 10 | 11 | var size: CGSize { 12 | CGSize(width: 40, height: 50) 13 | } 14 | 15 | func position(in container: CGSize) -> CGPoint { 16 | CGPoint( 17 | x: flag ? container.width - self.size.width : 0, 18 | y: 0 19 | ) 20 | } 21 | 22 | var offset: CGPoint { 23 | flag ? CGPoint(x: 20, y: 0) : .zero 24 | } 25 | 26 | var body: some View { 27 | ZStack { 28 | VStack { 29 | VStack(alignment: .leading) { 30 | Text("1. SwiftUI position") 31 | Text("2. *Kit position") 32 | Text("3. SwiftUI position and color") 33 | Text("4. *Kit position, SwiftUI color") 34 | } 35 | .font(.caption) 36 | .frame(maxWidth: .infinity, alignment: .leading) 37 | 38 | GeometryReader { g in 39 | VStack { 40 | Group { 41 | SwiftUIBox( 42 | size: self.size, 43 | position: self.position(in: g.size) 44 | ) 45 | RepresentedBox( 46 | size: self.size, 47 | position: self.position(in: g.size) 48 | ) 49 | Rectangle() 50 | .foregroundColor(flag ? .red : .orange) 51 | .offset(x: self.offset.x, y: self.offset.y) 52 | RepresentedContentBox( 53 | offset: self.offset 54 | ) { 55 | Rectangle() 56 | .foregroundColor(flag ? .red : .orange) 57 | } 58 | } 59 | .frame(height: self.size.height) 60 | .background(Color.primary.opacity(0.1)) 61 | } 62 | } 63 | .padding(.vertical, 100) 64 | // Implicit animations work. 65 | //.animation(isAnimated ? Self.animation : nil, value: flag) 66 | } 67 | 68 | VStack { 69 | Button { 70 | // Explicit animations work 71 | withAnimation(isAnimated ? Self.animation : nil) { 72 | flag.toggle() 73 | } 74 | } label: { Text("Flip") } 75 | Toggle("Animated", isOn: $isAnimated) 76 | } 77 | .frame(maxHeight: .infinity, alignment: .bottom) 78 | } 79 | .padding() 80 | } 81 | } 82 | 83 | struct ContentView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | ContentView() 86 | } 87 | } 88 | 89 | struct SwiftUIBox: View { 90 | let size: CGSize 91 | let position: CGPoint 92 | var body: some View { 93 | Rectangle() 94 | .foregroundColor(.blue) 95 | .frame(width: size.width, height: size.height) 96 | .position(x: position.x + (size.width/2), y: position.y + (size.height/2)) 97 | } 98 | } 99 | 100 | struct RepresentedBox { 101 | let size: CGSize 102 | let position: CGPoint 103 | } 104 | 105 | struct RepresentedContentBox { 106 | let offset: CGPoint 107 | let content: () -> Content 108 | func makeCoordinator() -> Coordinator { 109 | Coordinator(model: ViewModel(content: content)) 110 | } 111 | class Coordinator { 112 | init(model: ViewModel) { 113 | self.model = model 114 | } 115 | var model: ViewModel 116 | } 117 | class ViewModel: ObservableObject { 118 | init(content: @escaping () -> Content) { 119 | self.content = content 120 | } 121 | @Published var content: () -> Content 122 | } 123 | struct ViewWrapper: View { 124 | @ObservedObject var model: ViewModel 125 | var body: some View { 126 | model.content() 127 | } 128 | } 129 | } 130 | 131 | #if canImport(UIKit) 132 | extension RepresentedBox: UIViewRepresentable { 133 | func makeUIView(context: Context) -> UIView { 134 | let view = UIView(frame: .zero) 135 | let box = UIView() 136 | box.backgroundColor = .red 137 | view.addSubview(box) 138 | return view 139 | } 140 | func updateUIView(_ view: UIView, context: Context) { 141 | withMatchedAnimation(context.transaction.animation) { 142 | view.subviews.first?.frame = CGRect(origin: position, size: size) 143 | } 144 | } 145 | } 146 | extension RepresentedContentBox: UIViewControllerRepresentable { 147 | func makeUIViewController(context: Context) -> UIHostingController { 148 | UIHostingController(rootView: ViewWrapper(model: context.coordinator.model)) 149 | } 150 | func updateUIViewController(_ controller: UIHostingController, context: Context) { 151 | withMatchedAnimation(context.transaction.animation) { 152 | controller.view.frame.origin = offset 153 | context.coordinator.model.content = content 154 | } 155 | } 156 | } 157 | #endif 158 | 159 | #if canImport(AppKit) 160 | extension RepresentedBox: NSViewRepresentable { 161 | func makeNSView(context: Context) -> NSView { 162 | let view = NSView(frame: .zero) 163 | let box = NSView() 164 | box.wantsLayer = true 165 | box.layer?.backgroundColor = NSColor.red.cgColor 166 | view.addSubview(box) 167 | return view 168 | } 169 | func updateNSView(_ view: NSView, context: Context) { 170 | withMatchedAnimation(context.transaction.animation) { 171 | // Using animator proxy shows that animations are removed from implicit and explicit animations 172 | view.subviews.first?.animator().frame = CGRect(origin: position, size: size) 173 | } 174 | } 175 | } 176 | extension RepresentedContentBox: NSViewControllerRepresentable { 177 | func makeNSViewController(context: Context) -> NSHostingController { 178 | NSHostingController(rootView: ViewWrapper(model: context.coordinator.model)) 179 | } 180 | func updateNSViewController(_ controller: NSHostingController, context: Context) { 181 | withMatchedAnimation(context.transaction.animation) { 182 | // Not using animator proxy shows that animations are applied to implicit animations. 183 | controller.view.frame.origin = offset 184 | context.coordinator.model.content = content 185 | } 186 | } 187 | } 188 | #endif 189 | -------------------------------------------------------------------------------- /Examples/MatchedAnimationDemo/MatchedAnimationDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C402FF1F276D9CC80049E48B /* MatchedAnimationDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C402FF0F276D9CC60049E48B /* MatchedAnimationDemoApp.swift */; }; 11 | C402FF20276D9CC80049E48B /* MatchedAnimationDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C402FF0F276D9CC60049E48B /* MatchedAnimationDemoApp.swift */; }; 12 | C402FF21276D9CC80049E48B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C402FF10276D9CC60049E48B /* ContentView.swift */; }; 13 | C402FF22276D9CC80049E48B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C402FF10276D9CC60049E48B /* ContentView.swift */; }; 14 | C402FF23276D9CC80049E48B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C402FF11276D9CC70049E48B /* Assets.xcassets */; }; 15 | C402FF24276D9CC80049E48B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C402FF11276D9CC70049E48B /* Assets.xcassets */; }; 16 | C402FF34276DA4300049E48B /* MatchedAnimation in Frameworks */ = {isa = PBXBuildFile; productRef = C402FF33276DA4300049E48B /* MatchedAnimation */; }; 17 | C402FF36276DA4390049E48B /* MatchedAnimation in Frameworks */ = {isa = PBXBuildFile; productRef = C402FF35276DA4390049E48B /* MatchedAnimation */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | C402FF0F276D9CC60049E48B /* MatchedAnimationDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchedAnimationDemoApp.swift; sourceTree = ""; }; 22 | C402FF10276D9CC60049E48B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 23 | C402FF11276D9CC70049E48B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | C402FF16276D9CC80049E48B /* MatchedAnimationDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatchedAnimationDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | C402FF1C276D9CC80049E48B /* MatchedAnimationDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatchedAnimationDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | C402FF1E276D9CC80049E48B /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; 27 | C402FF37276DAA940049E48B /* swift-matched-animation */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "swift-matched-animation"; path = ../..; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | C402FF13276D9CC80049E48B /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | C402FF34276DA4300049E48B /* MatchedAnimation in Frameworks */, 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | C402FF19276D9CC80049E48B /* Frameworks */ = { 40 | isa = PBXFrameworksBuildPhase; 41 | buildActionMask = 2147483647; 42 | files = ( 43 | C402FF36276DA4390049E48B /* MatchedAnimation in Frameworks */, 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | C402FF09276D9CC50049E48B = { 51 | isa = PBXGroup; 52 | children = ( 53 | C402FF37276DAA940049E48B /* swift-matched-animation */, 54 | C402FF0E276D9CC60049E48B /* Shared */, 55 | C402FF1D276D9CC80049E48B /* macOS */, 56 | C402FF17276D9CC80049E48B /* Products */, 57 | C402FF2E276D9CEF0049E48B /* Frameworks */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | C402FF0E276D9CC60049E48B /* Shared */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | C402FF0F276D9CC60049E48B /* MatchedAnimationDemoApp.swift */, 65 | C402FF10276D9CC60049E48B /* ContentView.swift */, 66 | C402FF11276D9CC70049E48B /* Assets.xcassets */, 67 | ); 68 | path = Shared; 69 | sourceTree = ""; 70 | }; 71 | C402FF17276D9CC80049E48B /* Products */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | C402FF16276D9CC80049E48B /* MatchedAnimationDemo.app */, 75 | C402FF1C276D9CC80049E48B /* MatchedAnimationDemo.app */, 76 | ); 77 | name = Products; 78 | sourceTree = ""; 79 | }; 80 | C402FF1D276D9CC80049E48B /* macOS */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | C402FF1E276D9CC80049E48B /* macOS.entitlements */, 84 | ); 85 | path = macOS; 86 | sourceTree = ""; 87 | }; 88 | C402FF2E276D9CEF0049E48B /* Frameworks */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | ); 92 | name = Frameworks; 93 | sourceTree = ""; 94 | }; 95 | /* End PBXGroup section */ 96 | 97 | /* Begin PBXNativeTarget section */ 98 | C402FF15276D9CC80049E48B /* MatchedAnimationDemo (iOS) */ = { 99 | isa = PBXNativeTarget; 100 | buildConfigurationList = C402FF27276D9CC80049E48B /* Build configuration list for PBXNativeTarget "MatchedAnimationDemo (iOS)" */; 101 | buildPhases = ( 102 | C402FF12276D9CC80049E48B /* Sources */, 103 | C402FF13276D9CC80049E48B /* Frameworks */, 104 | C402FF14276D9CC80049E48B /* Resources */, 105 | ); 106 | buildRules = ( 107 | ); 108 | dependencies = ( 109 | ); 110 | name = "MatchedAnimationDemo (iOS)"; 111 | packageProductDependencies = ( 112 | C402FF33276DA4300049E48B /* MatchedAnimation */, 113 | ); 114 | productName = "MatchedAnimationDemo (iOS)"; 115 | productReference = C402FF16276D9CC80049E48B /* MatchedAnimationDemo.app */; 116 | productType = "com.apple.product-type.application"; 117 | }; 118 | C402FF1B276D9CC80049E48B /* MatchedAnimationDemo (macOS) */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = C402FF2A276D9CC80049E48B /* Build configuration list for PBXNativeTarget "MatchedAnimationDemo (macOS)" */; 121 | buildPhases = ( 122 | C402FF18276D9CC80049E48B /* Sources */, 123 | C402FF19276D9CC80049E48B /* Frameworks */, 124 | C402FF1A276D9CC80049E48B /* Resources */, 125 | ); 126 | buildRules = ( 127 | ); 128 | dependencies = ( 129 | ); 130 | name = "MatchedAnimationDemo (macOS)"; 131 | packageProductDependencies = ( 132 | C402FF35276DA4390049E48B /* MatchedAnimation */, 133 | ); 134 | productName = "MatchedAnimationDemo (macOS)"; 135 | productReference = C402FF1C276D9CC80049E48B /* MatchedAnimationDemo.app */; 136 | productType = "com.apple.product-type.application"; 137 | }; 138 | /* End PBXNativeTarget section */ 139 | 140 | /* Begin PBXProject section */ 141 | C402FF0A276D9CC50049E48B /* Project object */ = { 142 | isa = PBXProject; 143 | attributes = { 144 | BuildIndependentTargetsInParallel = 1; 145 | LastSwiftUpdateCheck = 1320; 146 | LastUpgradeCheck = 1320; 147 | TargetAttributes = { 148 | C402FF15276D9CC80049E48B = { 149 | CreatedOnToolsVersion = 13.2; 150 | }; 151 | C402FF1B276D9CC80049E48B = { 152 | CreatedOnToolsVersion = 13.2; 153 | }; 154 | }; 155 | }; 156 | buildConfigurationList = C402FF0D276D9CC50049E48B /* Build configuration list for PBXProject "MatchedAnimationDemo" */; 157 | compatibilityVersion = "Xcode 13.0"; 158 | developmentRegion = en; 159 | hasScannedForEncodings = 0; 160 | knownRegions = ( 161 | en, 162 | Base, 163 | ); 164 | mainGroup = C402FF09276D9CC50049E48B; 165 | productRefGroup = C402FF17276D9CC80049E48B /* Products */; 166 | projectDirPath = ""; 167 | projectRoot = ""; 168 | targets = ( 169 | C402FF15276D9CC80049E48B /* MatchedAnimationDemo (iOS) */, 170 | C402FF1B276D9CC80049E48B /* MatchedAnimationDemo (macOS) */, 171 | ); 172 | }; 173 | /* End PBXProject section */ 174 | 175 | /* Begin PBXResourcesBuildPhase section */ 176 | C402FF14276D9CC80049E48B /* Resources */ = { 177 | isa = PBXResourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | C402FF23276D9CC80049E48B /* Assets.xcassets in Resources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | C402FF1A276D9CC80049E48B /* Resources */ = { 185 | isa = PBXResourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | C402FF24276D9CC80049E48B /* Assets.xcassets in Resources */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXResourcesBuildPhase section */ 193 | 194 | /* Begin PBXSourcesBuildPhase section */ 195 | C402FF12276D9CC80049E48B /* Sources */ = { 196 | isa = PBXSourcesBuildPhase; 197 | buildActionMask = 2147483647; 198 | files = ( 199 | C402FF21276D9CC80049E48B /* ContentView.swift in Sources */, 200 | C402FF1F276D9CC80049E48B /* MatchedAnimationDemoApp.swift in Sources */, 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | }; 204 | C402FF18276D9CC80049E48B /* Sources */ = { 205 | isa = PBXSourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | C402FF22276D9CC80049E48B /* ContentView.swift in Sources */, 209 | C402FF20276D9CC80049E48B /* MatchedAnimationDemoApp.swift in Sources */, 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | /* End PBXSourcesBuildPhase section */ 214 | 215 | /* Begin XCBuildConfiguration section */ 216 | C402FF25276D9CC80049E48B /* Debug */ = { 217 | isa = XCBuildConfiguration; 218 | buildSettings = { 219 | ALWAYS_SEARCH_USER_PATHS = NO; 220 | CLANG_ANALYZER_NONNULL = YES; 221 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 222 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 223 | CLANG_CXX_LIBRARY = "libc++"; 224 | CLANG_ENABLE_MODULES = YES; 225 | CLANG_ENABLE_OBJC_ARC = YES; 226 | CLANG_ENABLE_OBJC_WEAK = YES; 227 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 228 | CLANG_WARN_BOOL_CONVERSION = YES; 229 | CLANG_WARN_COMMA = YES; 230 | CLANG_WARN_CONSTANT_CONVERSION = YES; 231 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 232 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 233 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 234 | CLANG_WARN_EMPTY_BODY = YES; 235 | CLANG_WARN_ENUM_CONVERSION = YES; 236 | CLANG_WARN_INFINITE_RECURSION = YES; 237 | CLANG_WARN_INT_CONVERSION = YES; 238 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 239 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 240 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 241 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 242 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 243 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 244 | CLANG_WARN_STRICT_PROTOTYPES = YES; 245 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 246 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 247 | CLANG_WARN_UNREACHABLE_CODE = YES; 248 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 249 | COPY_PHASE_STRIP = NO; 250 | DEBUG_INFORMATION_FORMAT = dwarf; 251 | ENABLE_STRICT_OBJC_MSGSEND = YES; 252 | ENABLE_TESTABILITY = YES; 253 | GCC_C_LANGUAGE_STANDARD = gnu11; 254 | GCC_DYNAMIC_NO_PIC = NO; 255 | GCC_NO_COMMON_BLOCKS = YES; 256 | GCC_OPTIMIZATION_LEVEL = 0; 257 | GCC_PREPROCESSOR_DEFINITIONS = ( 258 | "DEBUG=1", 259 | "$(inherited)", 260 | ); 261 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 262 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 263 | GCC_WARN_UNDECLARED_SELECTOR = YES; 264 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 265 | GCC_WARN_UNUSED_FUNCTION = YES; 266 | GCC_WARN_UNUSED_VARIABLE = YES; 267 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 268 | MTL_FAST_MATH = YES; 269 | ONLY_ACTIVE_ARCH = YES; 270 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 271 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 272 | }; 273 | name = Debug; 274 | }; 275 | C402FF26276D9CC80049E48B /* Release */ = { 276 | isa = XCBuildConfiguration; 277 | buildSettings = { 278 | ALWAYS_SEARCH_USER_PATHS = NO; 279 | CLANG_ANALYZER_NONNULL = YES; 280 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 281 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 282 | CLANG_CXX_LIBRARY = "libc++"; 283 | CLANG_ENABLE_MODULES = YES; 284 | CLANG_ENABLE_OBJC_ARC = YES; 285 | CLANG_ENABLE_OBJC_WEAK = YES; 286 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 287 | CLANG_WARN_BOOL_CONVERSION = YES; 288 | CLANG_WARN_COMMA = YES; 289 | CLANG_WARN_CONSTANT_CONVERSION = YES; 290 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 291 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 292 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 293 | CLANG_WARN_EMPTY_BODY = YES; 294 | CLANG_WARN_ENUM_CONVERSION = YES; 295 | CLANG_WARN_INFINITE_RECURSION = YES; 296 | CLANG_WARN_INT_CONVERSION = YES; 297 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 298 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 299 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 300 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 301 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 302 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 303 | CLANG_WARN_STRICT_PROTOTYPES = YES; 304 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 305 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 306 | CLANG_WARN_UNREACHABLE_CODE = YES; 307 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 308 | COPY_PHASE_STRIP = NO; 309 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 310 | ENABLE_NS_ASSERTIONS = NO; 311 | ENABLE_STRICT_OBJC_MSGSEND = YES; 312 | GCC_C_LANGUAGE_STANDARD = gnu11; 313 | GCC_NO_COMMON_BLOCKS = YES; 314 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 315 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 316 | GCC_WARN_UNDECLARED_SELECTOR = YES; 317 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 318 | GCC_WARN_UNUSED_FUNCTION = YES; 319 | GCC_WARN_UNUSED_VARIABLE = YES; 320 | MTL_ENABLE_DEBUG_INFO = NO; 321 | MTL_FAST_MATH = YES; 322 | SWIFT_COMPILATION_MODE = wholemodule; 323 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 324 | }; 325 | name = Release; 326 | }; 327 | C402FF28276D9CC80049E48B /* Debug */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 331 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 332 | CODE_SIGN_STYLE = Automatic; 333 | CURRENT_PROJECT_VERSION = 1; 334 | DEVELOPMENT_TEAM = 6P93C5F6VS; 335 | ENABLE_PREVIEWS = YES; 336 | GENERATE_INFOPLIST_FILE = YES; 337 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 338 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 339 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 340 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 341 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 342 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 343 | LD_RUNPATH_SEARCH_PATHS = ( 344 | "$(inherited)", 345 | "@executable_path/Frameworks", 346 | ); 347 | MARKETING_VERSION = 1.0; 348 | PRODUCT_BUNDLE_IDENTIFIER = Recentralized.MatchedAnimationDemo; 349 | PRODUCT_NAME = MatchedAnimationDemo; 350 | SDKROOT = iphoneos; 351 | SWIFT_EMIT_LOC_STRINGS = YES; 352 | SWIFT_VERSION = 5.0; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Debug; 356 | }; 357 | C402FF29276D9CC80049E48B /* Release */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 361 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 362 | CODE_SIGN_STYLE = Automatic; 363 | CURRENT_PROJECT_VERSION = 1; 364 | DEVELOPMENT_TEAM = 6P93C5F6VS; 365 | ENABLE_PREVIEWS = YES; 366 | GENERATE_INFOPLIST_FILE = YES; 367 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 368 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 369 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 370 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 371 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 372 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 373 | LD_RUNPATH_SEARCH_PATHS = ( 374 | "$(inherited)", 375 | "@executable_path/Frameworks", 376 | ); 377 | MARKETING_VERSION = 1.0; 378 | PRODUCT_BUNDLE_IDENTIFIER = Recentralized.MatchedAnimationDemo; 379 | PRODUCT_NAME = MatchedAnimationDemo; 380 | SDKROOT = iphoneos; 381 | SWIFT_EMIT_LOC_STRINGS = YES; 382 | SWIFT_VERSION = 5.0; 383 | TARGETED_DEVICE_FAMILY = "1,2"; 384 | VALIDATE_PRODUCT = YES; 385 | }; 386 | name = Release; 387 | }; 388 | C402FF2B276D9CC80049E48B /* Debug */ = { 389 | isa = XCBuildConfiguration; 390 | buildSettings = { 391 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 392 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 393 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; 394 | CODE_SIGN_STYLE = Automatic; 395 | COMBINE_HIDPI_IMAGES = YES; 396 | CURRENT_PROJECT_VERSION = 1; 397 | DEVELOPMENT_TEAM = 6P93C5F6VS; 398 | ENABLE_HARDENED_RUNTIME = YES; 399 | ENABLE_PREVIEWS = YES; 400 | GENERATE_INFOPLIST_FILE = YES; 401 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 402 | LD_RUNPATH_SEARCH_PATHS = ( 403 | "$(inherited)", 404 | "@executable_path/../Frameworks", 405 | ); 406 | MACOSX_DEPLOYMENT_TARGET = 12.0; 407 | MARKETING_VERSION = 1.0; 408 | PRODUCT_BUNDLE_IDENTIFIER = Recentralized.MatchedAnimationDemo; 409 | PRODUCT_NAME = MatchedAnimationDemo; 410 | SDKROOT = macosx; 411 | SWIFT_EMIT_LOC_STRINGS = YES; 412 | SWIFT_VERSION = 5.0; 413 | }; 414 | name = Debug; 415 | }; 416 | C402FF2C276D9CC80049E48B /* Release */ = { 417 | isa = XCBuildConfiguration; 418 | buildSettings = { 419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 420 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 421 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; 422 | CODE_SIGN_STYLE = Automatic; 423 | COMBINE_HIDPI_IMAGES = YES; 424 | CURRENT_PROJECT_VERSION = 1; 425 | DEVELOPMENT_TEAM = 6P93C5F6VS; 426 | ENABLE_HARDENED_RUNTIME = YES; 427 | ENABLE_PREVIEWS = YES; 428 | GENERATE_INFOPLIST_FILE = YES; 429 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 430 | LD_RUNPATH_SEARCH_PATHS = ( 431 | "$(inherited)", 432 | "@executable_path/../Frameworks", 433 | ); 434 | MACOSX_DEPLOYMENT_TARGET = 12.0; 435 | MARKETING_VERSION = 1.0; 436 | PRODUCT_BUNDLE_IDENTIFIER = Recentralized.MatchedAnimationDemo; 437 | PRODUCT_NAME = MatchedAnimationDemo; 438 | SDKROOT = macosx; 439 | SWIFT_EMIT_LOC_STRINGS = YES; 440 | SWIFT_VERSION = 5.0; 441 | }; 442 | name = Release; 443 | }; 444 | /* End XCBuildConfiguration section */ 445 | 446 | /* Begin XCConfigurationList section */ 447 | C402FF0D276D9CC50049E48B /* Build configuration list for PBXProject "MatchedAnimationDemo" */ = { 448 | isa = XCConfigurationList; 449 | buildConfigurations = ( 450 | C402FF25276D9CC80049E48B /* Debug */, 451 | C402FF26276D9CC80049E48B /* Release */, 452 | ); 453 | defaultConfigurationIsVisible = 0; 454 | defaultConfigurationName = Release; 455 | }; 456 | C402FF27276D9CC80049E48B /* Build configuration list for PBXNativeTarget "MatchedAnimationDemo (iOS)" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | C402FF28276D9CC80049E48B /* Debug */, 460 | C402FF29276D9CC80049E48B /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | C402FF2A276D9CC80049E48B /* Build configuration list for PBXNativeTarget "MatchedAnimationDemo (macOS)" */ = { 466 | isa = XCConfigurationList; 467 | buildConfigurations = ( 468 | C402FF2B276D9CC80049E48B /* Debug */, 469 | C402FF2C276D9CC80049E48B /* Release */, 470 | ); 471 | defaultConfigurationIsVisible = 0; 472 | defaultConfigurationName = Release; 473 | }; 474 | /* End XCConfigurationList section */ 475 | 476 | /* Begin XCSwiftPackageProductDependency section */ 477 | C402FF33276DA4300049E48B /* MatchedAnimation */ = { 478 | isa = XCSwiftPackageProductDependency; 479 | productName = MatchedAnimation; 480 | }; 481 | C402FF35276DA4390049E48B /* MatchedAnimation */ = { 482 | isa = XCSwiftPackageProductDependency; 483 | productName = MatchedAnimation; 484 | }; 485 | /* End XCSwiftPackageProductDependency section */ 486 | }; 487 | rootObject = C402FF0A276D9CC50049E48B /* Project object */; 488 | } 489 | --------------------------------------------------------------------------------