Skip to content

Commit

Permalink
Merge pull request #16 from jtrivedi/swiftui
Browse files Browse the repository at this point in the history
This PR fixes #15 and allows Wave property animators to be used in SwiftUI.

Previously, Animation conflicted with SwiftUI.Animation, and couldn't be resolved via Wave.Animation.

It also adds a small SwiftUI + Wave demo (dragging, throwing, and animating a box).
  • Loading branch information
jtrivedi authored Nov 7, 2022
2 parents b576d9a + 73143ce commit 92a4e5e
Show file tree
Hide file tree
Showing 16 changed files with 211 additions and 80 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Wave is a spring-based animation engine for iOS and iPadOS. It makes it easy to create fluid, interactive, and interruptible animations that feel great.

Wave has no external dependencies, and can be easily dropped into existing UIKit-based projects and apps.
Wave has no external dependencies, and can be easily dropped into existing UIKit or SwiftUI based projects and apps.

The core feature of Wave is that all animations are _re-targetable_, meaning that you can change an animation’s destination value in-flight, and the animation will gracefully _redirect_ to that new value.

Expand Down Expand Up @@ -105,10 +105,10 @@ While the block-based API is often most convenient, you may want to animate some
For example, to draw the orange path of the PiP demo, we need to know the value of every `CGPoint` from the view’s initial center, to its destination center:

```swift
// When the gesture ends, create a `CGPoint` animation from the PiP view's initial center, to its target.
// When the gesture ends, create a `CGPoint` animator from the PiP view's initial center, to its target.
// The `valueChanged` callback provides the intermediate locations of the callback, allowing us to draw the path.

let positionAnimator = Animation<CGPoint>(spring: animatedSpring)
let positionAnimator = Animator<CGPoint>(spring: animatedSpring)
positionAnimator.value = pipView.center // The presentation value
positionAnimator.target = pipViewDestination // The target value
positionAnimator.velocity = gestureVelocity
Expand Down
20 changes: 14 additions & 6 deletions Sample App/Wave-Sample/Wave-Sample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
F702B19928470D3100F8D848 /* CGRect+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F702B19828470D3100F8D848 /* CGRect+Extensions.swift */; };
F706E35C2845F58C00ADD288 /* Wave in Frameworks */ = {isa = PBXBuildFile; productRef = F706E35B2845F58C00ADD288 /* Wave */; };
F72189FD27EE5485001A5CCF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F72189FC27EE5485001A5CCF /* Assets.xcassets */; };
F7238435291310B300BA6402 /* SwitUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7238434291310B300BA6402 /* SwitUIViewController.swift */; };
F731390827E68AB100DCC56C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F731390727E68AB100DCC56C /* AppDelegate.swift */; };
F731390A27E68AB100DCC56C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F731390927E68AB100DCC56C /* SceneDelegate.swift */; };
F731392C27E68C1F00DCC56C /* PictureInPictureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F731392627E68C1F00DCC56C /* PictureInPictureViewController.swift */; };
Expand All @@ -18,11 +19,13 @@
F731393027E68C6900DCC56C /* InstantPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F731392F27E68C6900DCC56C /* InstantPanGestureRecognizer.swift */; };
F731393427E68CAE00DCC56C /* PathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F731393327E68CAE00DCC56C /* PathView.swift */; };
F76AE81127E6905B00A332E8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F76AE80F27E6905B00A332E8 /* LaunchScreen.storyboard */; };
F7F2B2BF2917543100E17E44 /* DragGesture+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F2B2BE2917543100E17E44 /* DragGesture+Extensions.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
F702B19828470D3100F8D848 /* CGRect+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Extensions.swift"; sourceTree = "<group>"; };
F72189FC27EE5485001A5CCF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F7238434291310B300BA6402 /* SwitUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitUIViewController.swift; sourceTree = "<group>"; };
F731390427E68AB100DCC56C /* Wave-Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Wave-Sample.app"; sourceTree = BUILT_PRODUCTS_DIR; };
F731390727E68AB100DCC56C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F731390927E68AB100DCC56C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
Expand All @@ -34,6 +37,7 @@
F731392F27E68C6900DCC56C /* InstantPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPanGestureRecognizer.swift; sourceTree = "<group>"; };
F731393327E68CAE00DCC56C /* PathView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathView.swift; sourceTree = "<group>"; };
F76AE81027E6905B00A332E8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
F7F2B2BE2917543100E17E44 /* DragGesture+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DragGesture+Extensions.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -101,6 +105,7 @@
F731392627E68C1F00DCC56C /* PictureInPictureViewController.swift */,
F731392827E68C1F00DCC56C /* GridViewController.swift */,
F731392727E68C1F00DCC56C /* SheetViewController.swift */,
F7238434291310B300BA6402 /* SwitUIViewController.swift */,
);
name = "View Controllers";
sourceTree = "<group>";
Expand All @@ -111,6 +116,7 @@
F731392F27E68C6900DCC56C /* InstantPanGestureRecognizer.swift */,
F731393327E68CAE00DCC56C /* PathView.swift */,
F702B19828470D3100F8D848 /* CGRect+Extensions.swift */,
F7F2B2BE2917543100E17E44 /* DragGesture+Extensions.swift */,
);
name = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -215,9 +221,11 @@
F731390A27E68AB100DCC56C /* SceneDelegate.swift in Sources */,
F731393027E68C6900DCC56C /* InstantPanGestureRecognizer.swift in Sources */,
F731393427E68CAE00DCC56C /* PathView.swift in Sources */,
F7238435291310B300BA6402 /* SwitUIViewController.swift in Sources */,
F731392D27E68C1F00DCC56C /* SheetViewController.swift in Sources */,
F702B19928470D3100F8D848 /* CGRect+Extensions.swift in Sources */,
F731392E27E68C1F00DCC56C /* GridViewController.swift in Sources */,
F7F2B2BF2917543100E17E44 /* DragGesture+Extensions.swift in Sources */,
F731392C27E68C1F00DCC56C /* PictureInPictureViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -358,10 +366,10 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = GV9FX44FW5;
DEVELOPMENT_TEAM = QPSD3KKBMU;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Wave-Sample/Info.plist";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = NO;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
Expand All @@ -371,7 +379,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.janumtrivedi.Wave-Sample";
PRODUCT_BUNDLE_IDENTIFIER = "com.janumtrivedi.Wave-Sample-Test";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
Expand All @@ -386,10 +394,10 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GV9FX44FW5;
DEVELOPMENT_TEAM = QPSD3KKBMU;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Wave-Sample/Info.plist";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = NO;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
Expand All @@ -399,7 +407,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.janumtrivedi.Wave-Sample";
PRODUCT_BUNDLE_IDENTIFIER = "com.janumtrivedi.Wave-Sample-Test";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
Expand Down
29 changes: 29 additions & 0 deletions Sample App/Wave-Sample/Wave-Sample/DragGesture+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// DragGesture+Extensions.swift
// Wave-Sample
//
// Created by Janum Trivedi on 11/5/22.
//

import SwiftUI

extension DragGesture.Value {

internal var velocity: CGSize {
let valueMirror = Mirror(reflecting: self)
for valueChild in valueMirror.children {
if valueChild.label == "velocity" {
let velocityMirror = Mirror(reflecting: valueChild.value)
for velocityChild in velocityMirror.children {
if velocityChild.label == "valuePerSecond" {
if let velocity = velocityChild.value as? CGSize {
return velocity
}
}
}
}
}
fatalError("Unable to retrieve velocity from \(Self.self)")
}

}
2 changes: 2 additions & 0 deletions Sample App/Wave-Sample/Wave-Sample/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
Expand Down
3 changes: 2 additions & 1 deletion Sample App/Wave-Sample/Wave-Sample/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
tabViewController.viewControllers = [
PictureInPictureViewController(),
GridViewController(),
SheetViewController()
SheetViewController(),
SwiftUIViewController()
]

tabViewController.selectedIndex = 0
Expand Down
91 changes: 91 additions & 0 deletions Sample App/Wave-Sample/Wave-Sample/SwitUIViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// SwitUIViewController.swift
// Wave-Sample
//
// Created by Janum Trivedi on 11/2/22.
//

import SwiftUI

import Wave

struct SwiftUIView: View {

let offsetAnimator = SpringAnimator<CGPoint>(spring: Spring(dampingRatio: 0.72, response: 0.7))

@State var boxOffset: CGPoint = .zero

var body: some View {
let size = 80.0
ZStack {
RoundedRectangle(cornerRadius: size * 0.22, style: .continuous)
.fill(.blue)
.frame(width: size, height: size)
VStack {
Text("SwiftUI")
.foregroundColor(.white)
}

}.onAppear {
offsetAnimator.value = .zero

// The offset animator's callback will update the `offset` state variable.
offsetAnimator.valueChanged = { newValue in
boxOffset = newValue
}
}
.offset(x: boxOffset.x, y: boxOffset.y)
.gesture(
DragGesture()
.onChanged { value in
// Update the animator's target to the new drag translation.
offsetAnimator.target = CGPoint(x: value.translation.width, y: value.translation.height)

// Don't animate the box's position when we're dragging it.
offsetAnimator.mode = .nonAnimated
offsetAnimator.start()
}
.onEnded { value in
// Animate the box to its original location (i.e. with zero translation).
offsetAnimator.target = .zero

// We want the box to animate to its original location, so use an `animated` mode.
// This is different than the
offsetAnimator.mode = .animated

// Take the velocity of the gesture, and give it to the animator.
// This makes the throw animation feel natural and continuous.
offsetAnimator.velocity = CGPoint(x: value.velocity.width, y: value.velocity.height)
offsetAnimator.start()
}
)
}
}

struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}

class SwiftUIViewController: UIViewController {

override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

title = "SwiftUI"
tabBarItem.image = UIImage(systemName: "swift")
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
let hostingController = UIHostingController(rootView: SwiftUIView())
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
hostingController.view.frame = view.bounds
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class PictureInPictureViewController: UIViewController {

/// In order to draw the path that the PiP view takes when animating to its final destination,
/// we need the intermediate spring values. Use a separate `CGPoint` animator to get these values.
lazy var positionAnimator = Animation<CGPoint>(spring: animatedSpring)
lazy var positionAnimator = SpringAnimator<CGPoint>(spring: animatedSpring)

/// The view that draws the path of the PiP view.
lazy var pathView = PathView(frame: view.bounds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class SheetViewController: UIViewController {
let interactiveSpring = Spring(dampingRatio: 0.8, response: 0.2)
let animatedSpring = Spring(dampingRatio: 0.68, response: 0.8)

lazy var sheetPresentationAnimator = Animation<CGFloat>(spring: animatedSpring)
lazy var sheetPresentationAnimator = SpringAnimator<CGFloat>(spring: animatedSpring)

var sheetPresentationProgress: CGFloat = 0 {
didSet {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Wave/AnimationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal class AnimationController {

private var displayLink: CADisplayLink?

private var animations: [UUID: AnimationProviding] = [:]
private var animations: [UUID: AnimatorProviding] = [:]
private var animationSettingsStack = SettingsStack()

typealias CompletionBlock = ((_ finished: Bool, _ retargeted: Bool) -> Void)
Expand All @@ -35,7 +35,7 @@ internal class AnimationController {
animationSettingsStack.pop()
}

func runPropertyAnimation(_ animation: AnimationProviding) {
func runPropertyAnimation(_ animation: AnimatorProviding) {
if animations.isEmpty {
startDisplayLink()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

import Foundation

internal protocol AnimationProviding {
internal protocol AnimatorProviding {
var id: UUID { get }
var groupUUID: UUID? { get }

var state: AnimationState { get }
var state: AnimatorState { get }

func updateAnimation(dt: TimeInterval)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/**
The current state of an `Animation`.
*/
public enum AnimationState {
public enum AnimatorState {
/**
The animation is not currently running, but is ready.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation
import UIKit

public class Animation<T: SpringInterpolatable>: AnimationProviding {
public class SpringAnimator<T: SpringInterpolatable>: AnimatorProviding {

public enum Event {
/**
Expand All @@ -33,7 +33,7 @@ public class Animation<T: SpringInterpolatable>: AnimationProviding {
/**
The execution state of the animation (`inactive`, `running`, or `ended`).
*/
public private(set) var state: AnimationState = .inactive {
public private(set) var state: AnimatorState = .inactive {
didSet {
switch (oldValue, state) {
case (.inactive, .running):
Expand Down Expand Up @@ -248,10 +248,10 @@ public class Animation<T: SpringInterpolatable>: AnimationProviding {
}
}

extension Animation: CustomStringConvertible {
extension SpringAnimator: CustomStringConvertible {
public var description: String {
"""
Animation<\(T.self)>(
SpringAnimator<\(T.self)>(
uuid: \(id)
groupUUID: \(String(describing: groupUUID))
Expand Down
4 changes: 2 additions & 2 deletions Sources/Wave/UIView+ViewAnimator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ public extension UIView {
}
}

internal var animations: [ViewAnimator.AnimatableProperty: AnimationProviding] {
internal var animators: [ViewAnimator.AnimatableProperty: AnimatorProviding] {
get {
objc_getAssociatedObject(self, &ViewAnimationsAssociatedObjectHandle) as? [ViewAnimator.AnimatableProperty: AnimationProviding] ?? [:]
objc_getAssociatedObject(self, &ViewAnimationsAssociatedObjectHandle) as? [ViewAnimator.AnimatableProperty: AnimatorProviding] ?? [:]
}
set {
objc_setAssociatedObject(self, &ViewAnimationsAssociatedObjectHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
Expand Down
Loading

0 comments on commit 92a4e5e

Please sign in to comment.