diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index d812429f..c369321a 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -8,7 +8,7 @@ on: jobs: create_release: name: Create Release - runs-on: macos-14 + runs-on: macos-15 steps: - uses: maxim-lobanov/setup-xcode@v1 with: diff --git a/.github/workflows/docc-docs.yml b/.github/workflows/docc-docs.yml index 3da15a0a..6fb52a53 100644 --- a/.github/workflows/docc-docs.yml +++ b/.github/workflows/docc-docs.yml @@ -4,15 +4,10 @@ on: [push] jobs: deploy_docs: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v2 - - name: Setup Swift 5.9 - uses: swift-actions/setup-swift@v1 - with: - swift-version: '5.9' - - name: Build Docs uses: sersoft-gmbh/swifty-docs-action@v3.0.0 with: diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index c10f9eae..b1b7a8bc 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v2 diff --git a/Benchmark/Sources/MotionBenchmark/MotionBenchmark.swift b/Benchmark/Sources/MotionBenchmark/MotionBenchmark.swift index e041cb43..4b1cd4da 100644 --- a/Benchmark/Sources/MotionBenchmark/MotionBenchmark.swift +++ b/Benchmark/Sources/MotionBenchmark/MotionBenchmark.swift @@ -10,6 +10,7 @@ import Benchmark import Motion import QuartzCore +@MainActor func createAndAnimateSpringAnimations(to toValue: Value, count: Int, state: inout BenchmarkState) { autoreleasepool { let springAnimations = (0.. SpringAnimation in @@ -26,6 +27,7 @@ func createAndAnimateSpringAnimations(to toValue: Valu } } +@MainActor func createAndAnimateBasicAnimations(to toValue: Value, count: Int, state: inout BenchmarkState) { autoreleasepool { let basicAnimations = (0.. BasicAnimation in @@ -43,7 +45,7 @@ func createAndAnimateBasicAnimations(to toValue: Value } } - +@MainActor func createAndAnimateDecayAnimations(velocity: Value, count: Int, state: inout BenchmarkState) { autoreleasepool { let decayAnimations = (0.. DecayAnimation in @@ -65,6 +67,7 @@ let ToValue = 320.0 // Measure execution of 5000 animations of each type serially for each supported SIMD type. // SIMD go brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr +@MainActor public func RunBenchmark() { let springAnimationSuite = BenchmarkSuite(name: "SIMD SpringAnimations", settings: TimeUnit(.ms)) { suite in suite.benchmark("Execute 5000 CGFloat SpringAnimations") { state in diff --git a/Example/MotionExample-iOS/MotionExample-iOS.xcodeproj/project.pbxproj b/Example/MotionExample-iOS/MotionExample-iOS.xcodeproj/project.pbxproj index 490dd81e..d6725004 100644 --- a/Example/MotionExample-iOS/MotionExample-iOS.xcodeproj/project.pbxproj +++ b/Example/MotionExample-iOS/MotionExample-iOS.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -275,6 +275,8 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -329,6 +331,8 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -347,7 +351,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "ca.adambell.MotionExample-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -366,7 +369,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "ca.adambell.MotionExample-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/Example/MotionExample-iOS/MotionExample-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/MotionExample-iOS/MotionExample-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e20fff4a..b16cb0a3 100644 --- a/Example/MotionExample-iOS/MotionExample-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/MotionExample-iOS/MotionExample-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/apple/swift-argument-parser", "state": { "branch": null, - "revision": "92646c0cdbaca076c8d3d0207891785b3379cbff", - "version": "0.3.1" + "revision": "41982a3656a71c768319979febd796c6fd111d5c", + "version": "1.5.0" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/google/swift-benchmark", "state": { "branch": null, - "revision": "8e0ef8bb7482ab97dcd2cd1d6855bd38921c345d", - "version": "0.1.0" + "revision": "8163295f6fe82356b0bcf8e1ab991645de17d096", + "version": "0.1.2" } }, { @@ -24,7 +24,16 @@ "repositoryURL": "https://github.com/apple/swift-docc-plugin", "state": { "branch": null, - "revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6", + "revision": "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", + "version": "1.4.3" + } + }, + { + "package": "SymbolKit", + "repositoryURL": "https://github.com/swiftlang/swift-docc-symbolkit", + "state": { + "branch": null, + "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", "version": "1.0.0" } }, diff --git a/Example/MotionExample-iOS/MotionExample-iOS/SwiftUIDemoView.swift b/Example/MotionExample-iOS/MotionExample-iOS/SwiftUIDemoView.swift index c1458500..db330209 100644 --- a/Example/MotionExample-iOS/MotionExample-iOS/SwiftUIDemoView.swift +++ b/Example/MotionExample-iOS/MotionExample-iOS/SwiftUIDemoView.swift @@ -14,7 +14,6 @@ class AnimationWrapper: ObservableObject { self.animation = animation } - var animation: T } @@ -48,7 +47,7 @@ struct SwiftUIDemoView: View { // this allows subsequent drags to also work correctly. animationWrapper.animation.updateValue(to: CGPoint(x: position.x + totalTranslation.x, y: position.y + totalTranslation.y), postValueChanged: true) animationWrapper.animation.toValue = .zero - animationWrapper.animation.velocity = dragGesture.velocity + animationWrapper.animation.velocity = CGPoint(x: dragGesture.velocity.width, y: dragGesture.velocity.height) animationWrapper.animation.start() } ) @@ -67,29 +66,6 @@ struct SwiftUIDemoView: View { } -extension DragGesture.Value { - - /// h/t @lukaskubanek https://stackoverflow.com/questions/62906109/what-is-the-best-way-to-get-drag-velocity/73426600#73426600 - internal var velocity: CGPoint { - 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 CGPoint(x: velocity.width, y: velocity.height) - } - } - } - } - } - assertionFailure("Unable to retrieve velocity from \(Self.self)") - return .zero - } - -} - struct SwiftUIDemoView_Previews: PreviewProvider { static var previews: some View { SwiftUIDemoView() diff --git a/Package.swift b/Package.swift index 9f9615df..6c7279cc 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -8,7 +8,7 @@ let package = Package( platforms: [ .iOS(.v13), .tvOS(.v13), - .macOS(.v10_14), + .macOS(.v10_15), .visionOS(.v1) ], products: [ @@ -42,5 +42,5 @@ let package = Package( "Motion", ]), ], - swiftLanguageVersions: [.v5] + swiftLanguageVersions: [.v5, .version("6")] ) diff --git a/Sources/Graphing/ValueAnimationGraph.swift b/Sources/Graphing/ValueAnimationGraph.swift index 6f03abb7..68f404e7 100644 --- a/Sources/Graphing/ValueAnimationGraph.swift +++ b/Sources/Graphing/ValueAnimationGraph.swift @@ -11,9 +11,10 @@ import Motion import SwiftUI @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) +@MainActor public struct ValueAnimationShape: Shape { - public enum GraphType { + public enum GraphType: Sendable { case position case velocity } @@ -36,52 +37,55 @@ public struct ValueAnimationShape: Shape { } public func path(in rect: CGRect) -> Path { - let dt = 1.0 / 60.0 - - animation.stop() - animation.updateValue(to: 0.0) - - let height = rect.size.height / 2.0 - - let points: [CGPoint] = stride(from: 0.0, to: duration, by: dt).map { (i) -> CGPoint in - let percent: CGFloat = CGFloat(i / duration) - - let point: CGPoint - - let position = { () -> CGPoint in - if let decayAnimation = animation as? DecayAnimation { - return CGPoint(x: rect.width * percent, y: height - decayAnimation.value) - } else { - return CGPoint(x: rect.width * percent, y: height + ((animation.toValue - animation.value) * height)) + // lol. + MainActor.assumeIsolated { + let dt = 1.0 / 60.0 + + animation.stop() + animation.updateValue(to: 0.0) + + let height = rect.size.height / 2.0 + + let points: [CGPoint] = stride(from: 0.0, to: duration, by: dt).map { (i) -> CGPoint in + let percent: CGFloat = CGFloat(i / duration) + + let point: CGPoint + + let position = { () -> CGPoint in + if let decayAnimation = animation as? DecayAnimation { + return CGPoint(x: rect.width * percent, y: height - decayAnimation.value) + } else { + return CGPoint(x: rect.width * percent, y: height + ((animation.toValue - animation.value) * height)) + } } - } - - let velocity = { () -> CGPoint in - let velocity: CGFloat - if type(of: animation).supportsVelocity { - velocity = animation.velocity - } else { - return position() + + let velocity = { () -> CGPoint in + let velocity: CGFloat + if type(of: animation).supportsVelocity { + velocity = animation.velocity + } else { + return position() + } + + return CGPoint(x: rect.width * percent, y: height + velocity * height / 3.0) } - - return CGPoint(x: rect.width * percent, y: height + velocity * height / 3.0) - } - - switch graphType { - case .position: - point = position() - case .velocity: - point = velocity() + + switch graphType { + case .position: + point = position() + case .velocity: + point = velocity() + } + + animation.tick(frame: .init(timestamp: 0, targetTimestamp: dt)) + + return point } - - animation.tick(frame: .init(timestamp: 0, targetTimestamp: dt)) - - return point + + var path = Path() + path.addLines(points) + return path } - - var path = Path() - path.addLines(points) - return path } } diff --git a/Sources/Motion/Animations/BasicAnimation.swift b/Sources/Motion/Animations/BasicAnimation.swift index ed98bbd3..a88811e6 100644 --- a/Sources/Motion/Animations/BasicAnimation.swift +++ b/Sources/Motion/Animations/BasicAnimation.swift @@ -83,7 +83,7 @@ public final class BasicAnimation: ValueAnimation(easingFunction: EasingFunction, range: inout ClosedRange, value: inout SIMDType) -> CFTimeInterval? { + nonisolated internal func solveAccumulatedTime(easingFunction: EasingFunction, range: inout ClosedRange, value: inout SIMDType) -> CFTimeInterval? { /* Must Be Mirrored Below */ if !range.contains(value) { @@ -107,7 +107,7 @@ public final class BasicAnimation: ValueAnimation) @_specialize(kind: partial, where SIMDType == SIMD64) @_specialize(kind: partial, where SIMDType == SIMD64) - internal func solveAccumulatedTime(easingFunction: EasingFunction, range: inout ClosedRange, value: inout SIMDType) -> CFTimeInterval? { + nonisolated internal func solveAccumulatedTime(easingFunction: EasingFunction, range: inout ClosedRange, value: inout SIMDType) -> CFTimeInterval? { /* Must Be Mirrored Above */ if !range.contains(value) { @@ -231,7 +231,7 @@ public final class BasicAnimation: ValueAnimation(easingFunction: EasingFunction, range: inout ClosedRange, fraction: SIMDType.Scalar, value: inout SIMDType) where SIMDType.Scalar == SIMDType.SIMDType.Scalar { + nonisolated internal func tickOptimized(easingFunction: EasingFunction, range: inout ClosedRange, fraction: SIMDType.Scalar, value: inout SIMDType) where SIMDType.Scalar == SIMDType.SIMDType.Scalar { /* Must Be Mirrored Below */ value = easingFunction.solveInterpolatedValueSIMD(range, fraction: fraction) @@ -251,7 +251,7 @@ public final class BasicAnimation: ValueAnimation) @_specialize(kind: partial, where SIMDType == SIMD64) @_specialize(kind: partial, where SIMDType == SIMD64) - internal func tickOptimized(easingFunction: EasingFunction, range: inout ClosedRange, fraction: SIMDType.Scalar, value: inout SIMDType) where SIMDType.Scalar == SIMDType.SIMDType.Scalar { + nonisolated internal func tickOptimized(easingFunction: EasingFunction, range: inout ClosedRange, fraction: SIMDType.Scalar, value: inout SIMDType) where SIMDType.Scalar == SIMDType.SIMDType.Scalar { /* Must Be Mirrored Above */ value = easingFunction.solveInterpolatedValueSIMD(range, fraction: fraction) diff --git a/Sources/Motion/Animations/DecayAnimation.swift b/Sources/Motion/Animations/DecayAnimation.swift index 37f02d55..9760c36c 100644 --- a/Sources/Motion/Animations/DecayAnimation.swift +++ b/Sources/Motion/Animations/DecayAnimation.swift @@ -190,7 +190,7 @@ public final class DecayAnimation: ValueAnimation(_ dt: SIMDType.SIMDType.Scalar, decay: DecayFunction, value: inout SIMDType, velocity: inout SIMDType) where SIMDType.SIMDType == SIMDType { + nonisolated internal func tickOptimized(_ dt: SIMDType.SIMDType.Scalar, decay: DecayFunction, value: inout SIMDType, velocity: inout SIMDType) where SIMDType.SIMDType == SIMDType { /* Must Be Mirrored Below */ value = decay.solveSIMD(dt: dt, x0: value, velocity: &velocity) @@ -210,7 +210,7 @@ public final class DecayAnimation: ValueAnimation) @_specialize(kind: partial, where SIMDType == SIMD64) @_specialize(kind: partial, where SIMDType == SIMD64) - internal func tickOptimized(_ dt: SIMDType.SIMDType.Scalar, decay: DecayFunction, value: inout SIMDType, velocity: inout SIMDType) where SIMDType.SIMDType == SIMDType { + nonisolated internal func tickOptimized(_ dt: SIMDType.SIMDType.Scalar, decay: DecayFunction, value: inout SIMDType, velocity: inout SIMDType) where SIMDType.SIMDType == SIMDType { /* Must Be Mirrored Above */ value = decay.solveSIMD(dt: dt, x0: value, velocity: &velocity) diff --git a/Sources/Motion/Animations/Functions/DecayFunction.swift b/Sources/Motion/Animations/Functions/DecayFunction.swift index 9d01063c..d8098d66 100644 --- a/Sources/Motion/Animations/Functions/DecayFunction.swift +++ b/Sources/Motion/Animations/Functions/DecayFunction.swift @@ -20,7 +20,7 @@ public let UIScrollViewDecayConstant: Double = 0.998 - Note: This can be used on its own, but it's mainly used by `DecayAnimation`'s `tick` method. - SeeAlso: `DecayAnimation` */ -public struct DecayFunction { +public struct DecayFunction: Sendable { /// The rate at which the velocity decays over time. Defaults to `UIKitDecayConstant`. public var decayConstant: Value.SIMDType.Scalar { diff --git a/Sources/Motion/Animations/Functions/EasingFunctions.swift b/Sources/Motion/Animations/Functions/EasingFunctions.swift index e5c5d3b7..628913a2 100644 --- a/Sources/Motion/Animations/Functions/EasingFunctions.swift +++ b/Sources/Motion/Animations/Functions/EasingFunctions.swift @@ -13,7 +13,7 @@ import Foundation - Note: This can be used on its own, but it's mainly used by `BasicAnimation`'s `tick` method. - SeeAlso: `BasicAnimation` */ -public struct EasingFunction: Hashable { +public struct EasingFunction: Hashable, Sendable { /// An easing function with a linear bezier curve. public static var linear: Self { @@ -146,7 +146,7 @@ extension EasingFunction where Value: SupportedSIMD { /** A Swift adaptation of UnitBezier from WebKit: https://opensource.apple.com/source/WebCore/WebCore-955.66/platform/graphics/UnitBezier.h */ -public struct Bezier: Hashable { +public struct Bezier: Hashable, Sendable { /// The x value of the first point. public let x1: Scalar diff --git a/Sources/Motion/Animations/Functions/SpringFunction.swift b/Sources/Motion/Animations/Functions/SpringFunction.swift index ecee9446..1f2191e0 100644 --- a/Sources/Motion/Animations/Functions/SpringFunction.swift +++ b/Sources/Motion/Animations/Functions/SpringFunction.swift @@ -16,7 +16,7 @@ import simd - SeeAlso: `SpringAnimation` */ -public struct SpringFunction { +public struct SpringFunction: Sendable { /** The stiffness coefficient of the string. diff --git a/Sources/Motion/Animations/SpringAnimation.swift b/Sources/Motion/Animations/SpringAnimation.swift index 220e339d..86cd43df 100644 --- a/Sources/Motion/Animations/SpringAnimation.swift +++ b/Sources/Motion/Animations/SpringAnimation.swift @@ -354,7 +354,7 @@ public final class SpringAnimation: ValueAnimation(_ dt: SIMDType.Scalar, spring: SpringFunction, value: inout SIMDType, toValue: inout SIMDType, velocity: inout SIMDType, clampingRange: inout ClosedRange?) where SIMDType.Scalar == SIMDType.SIMDType.Scalar, SIMDType == SIMDType.SIMDType { + nonisolated internal func tickOptimized(_ dt: SIMDType.Scalar, spring: SpringFunction, value: inout SIMDType, toValue: inout SIMDType, velocity: inout SIMDType, clampingRange: inout ClosedRange?) where SIMDType.Scalar == SIMDType.SIMDType.Scalar, SIMDType == SIMDType.SIMDType { /* Must Be Mirrored Below */ let x0 = toValue - value @@ -382,7 +382,7 @@ public final class SpringAnimation: ValueAnimation) @_specialize(kind: partial, where SIMDType == SIMD64) @_specialize(kind: partial, where SIMDType == SIMD64) - internal func tickOptimized(_ dt: SIMDType.Scalar, spring: SpringFunction, value: inout SIMDType, toValue: inout SIMDType, velocity: inout SIMDType, clampingRange: inout ClosedRange?) where SIMDType.Scalar == SIMDType.SIMDType.Scalar, SIMDType == SIMDType.SIMDType { + nonisolated internal func tickOptimized(_ dt: SIMDType.Scalar, spring: SpringFunction, value: inout SIMDType, toValue: inout SIMDType, velocity: inout SIMDType, clampingRange: inout ClosedRange?) where SIMDType.Scalar == SIMDType.SIMDType.Scalar, SIMDType == SIMDType.SIMDType { /* Must Be Mirrored Above */ let x0 = toValue - value diff --git a/Sources/Motion/Animations/ValueAnimation.swift b/Sources/Motion/Animations/ValueAnimation.swift index 065b822b..f782ac8b 100644 --- a/Sources/Motion/Animations/ValueAnimation.swift +++ b/Sources/Motion/Animations/ValueAnimation.swift @@ -16,6 +16,7 @@ import QuartzCore - Note: This class is **not** thread-safe. It is meant to be run on the **main thread** only (much like any AppKit / UIKit operations should be main threaded). - SeeAlso: `ValueAnimation` */ +@MainActor open class Animation: AnimationDriverObserver { /** @@ -67,7 +68,11 @@ open class Animation: AnimationDriverObserver { } deinit { - animator?.unobserve(self) + // TODO: This is "unsafe", but there's no better way to do this. + // Class is MainActor-only anyways, so it should be fine. + MainActor.assumeIsolated { + animator?.unobserve(self) + } } /// Starts the animation if `hasResolved` is false. @@ -122,6 +127,7 @@ open class Animation: AnimationDriverObserver { - Note: This class is **not** thread-safe. It is meant to be run on the **main thread** only (much like any AppKit / UIKit operations should be main threaded). - SeeAlso: `BasicAnimation`, `DecayAnimation`, `SpringAnimation`. */ +@MainActor public class ValueAnimation: Animation { /** @@ -256,19 +262,19 @@ public class ValueAnimation: Animation { extension ValueAnimation: Hashable, Equatable { - public static func == (lhs: ValueAnimation, rhs: ValueAnimation) -> Bool { + nonisolated public static func == (lhs: ValueAnimation, rhs: ValueAnimation) -> Bool { return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) } - public func hash(into hasher: inout Hasher) { + nonisolated public func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self).hashValue) } } /// Timestamps for the current animation frame. -public struct AnimationFrame : Equatable { - +public struct AnimationFrame : Equatable, Sendable { + /// The timestamp that the frame started. public let timestamp: CFTimeInterval diff --git a/Sources/Motion/Protocols/AnimationEnvironment.swift b/Sources/Motion/Protocols/AnimationEnvironment.swift index 8e11b506..90ab93ea 100644 --- a/Sources/Motion/Protocols/AnimationEnvironment.swift +++ b/Sources/Motion/Protocols/AnimationEnvironment.swift @@ -15,14 +15,27 @@ public typealias CGDirectDisplayID = UInt32 */ public protocol AnimationEnvironment: AnyObject { /// The ``Animator`` type to be used for animations in this environment. - var animator: Animator { get } + @MainActor var animator: Animator { get } #if os(macOS) /// Identifier for the CoreGraphics display represented by this environment. - var displayID: CGDirectDisplayID? { get } + @MainActor var displayID: CGDirectDisplayID? { get } /// The preferred FPS for animations running in this environment. - var preferredFramesPerSecond: Int { get } + @MainActor var preferredFramesPerSecond: Int { get } + #endif +} + +public protocol AsyncAnimationEnvironment: AnimationEnvironment { + /// The ``Animator`` type to be used for animations in this environment. + nonisolated var animator: Animator { get } + + #if os(macOS) + /// Identifier for the CoreGraphics display represented by this environment. + nonisolated var displayID: CGDirectDisplayID? { get } + + /// The preferred FPS for animations running in this environment. + nonisolated var preferredFramesPerSecond: Int { get } #endif } @@ -47,10 +60,10 @@ public extension AnimationEnvironment where Self == DefaultAnimationEnvironment #if canImport(UIKit) public extension AnimationEnvironment { - var animator: Animator { Animator.shared } + @MainActor var animator: Animator { Animator.shared } } -public final class DefaultAnimationEnvironment: AnimationEnvironment { +public final class DefaultAnimationEnvironment: AnimationEnvironment, Sendable { public static let shared = DefaultAnimationEnvironment() } #endif diff --git a/Sources/Motion/Protocols/EquatableEnough.swift b/Sources/Motion/Protocols/EquatableEnough.swift index 5105aeae..c8731f01 100644 --- a/Sources/Motion/Protocols/EquatableEnough.swift +++ b/Sources/Motion/Protocols/EquatableEnough.swift @@ -19,7 +19,7 @@ import simd It really only exists so that when working with Scalars and SIMD, one can convert to / from them using Doubles or Floating point numbers. Technically there shouldn't be any overhead by this, it's just to make the compiler believe that it's ok to convert types. */ -public protocol FloatingPointInitializable: FloatingPoint & ExpressibleByFloatLiteral & Comparable { +public protocol FloatingPointInitializable: FloatingPoint & ExpressibleByFloatLiteral & Comparable & Sendable { init(_ value: Float) init(_ value: Double) diff --git a/Sources/Motion/Protocols/SIMDRepresentable.swift b/Sources/Motion/Protocols/SIMDRepresentable.swift index 361830ba..3dd1e526 100644 --- a/Sources/Motion/Protocols/SIMDRepresentable.swift +++ b/Sources/Motion/Protocols/SIMDRepresentable.swift @@ -7,7 +7,7 @@ import CoreGraphics import Foundation -import simd +@preconcurrency import simd import RealModule // MARK: - Supported Types diff --git a/Sources/Motion/Utilities/AnimationDriver.swift b/Sources/Motion/Utilities/AnimationDriver.swift index 841ded1c..f4a7f11f 100644 --- a/Sources/Motion/Utilities/AnimationDriver.swift +++ b/Sources/Motion/Utilities/AnimationDriver.swift @@ -3,16 +3,29 @@ protocol AnimationDriver { /// A Boolean value that indicates whether the system suspends the display link’s /// notifications to the target. - var isPaused: Bool { get set } + @MainActor var isPaused: Bool { get set } /// The preferred frame rate for the display link callback. - var preferredFramesPerSecond: Int { get } - - var observer: AnimationDriverObserver? { get set } + @MainActor var preferredFramesPerSecond: Int { get } + + @MainActor var observer: AnimationDriverObserver? { get set } +} + +protocol AsyncAnimationDriver: AnimationDriver { + nonisolated var isPaused: Bool { get set } + + /// The preferred frame rate for the display link callback. + nonisolated var preferredFramesPerSecond: Int { get } + + nonisolated var observer: AnimationDriverObserver? { get set } } protocol AnimationDriverObserver: AnyObject { - func tick(frame: AnimationFrame) + @MainActor func tick(frame: AnimationFrame) +} + +protocol AsyncAnimationDriverObserver: AnimationDriverObserver { + nonisolated func tick(frame: AnimationFrame) } #if os(visionOS) @@ -24,6 +37,7 @@ import UIKit typealias SystemAnimationDriver = CoreAnimationDriver +@MainActor final class CoreAnimationDriver: AnimationDriver { private var displayLink: CADisplayLink! @@ -69,12 +83,17 @@ final class CoreAnimationDriver: AnimationDriver { displayLink.preferredFrameRateRange = Self.defaultPreferredFrameRateRange } } - - deinit { - isPaused = true - displayLink.invalidate() + + nonisolated func invalidateDisplayLink() { + Task { @MainActor in + displayLink.isPaused = true + displayLink.invalidate() + } } + deinit { + invalidateDisplayLink() + } var isPaused: Bool = true { didSet { @@ -117,6 +136,7 @@ import Cocoa typealias SystemAnimationDriver = CoreVideoDriver +@MainActor final class CoreVideoDriver: AnimationDriver { private var displaylink: CVDisplayLink! @@ -235,7 +255,7 @@ extension CVTimeStamp { #if targetEnvironment(simulator) // lol, calling private C-functions from Swift is definitely something // We also don't want to be doing this dlopen at 60+fps so we just cache the function pointer. -internal var SimulatorSlowAnimationsCoefficient_: (@convention(c) () -> Float) = { +nonisolated(unsafe) internal var SimulatorSlowAnimationsCoefficient_: (@convention(c) () -> Float) = { let handle = dlopen("/System/Library/Frameworks/UIKit.framework/UIKit", RTLD_NOW) let symbol = dlsym(handle, "UIAnimationDragCoefficient") let function = unsafeBitCast(symbol, to: (@convention(c) () -> Float).self) diff --git a/Sources/Motion/Utilities/AnimationEnvironmentProxy.swift b/Sources/Motion/Utilities/AnimationEnvironmentProxy.swift index add6edb0..599a9cc5 100644 --- a/Sources/Motion/Utilities/AnimationEnvironmentProxy.swift +++ b/Sources/Motion/Utilities/AnimationEnvironmentProxy.swift @@ -31,6 +31,7 @@ import AppKit it wouldn't be safe to start the animation in `viewDidLoad`. Generally, a safe place to start an animation tied to a view or view controller is any time after `viewDidAppear` and before `viewDidDisappear`, which is when the view is in a window, and its window is on screen. */ +@MainActor public protocol AnimationEnvironmentProxy: AnimationEnvironment { var proxiedAnimationEnvironment: AnimationEnvironment { get } } diff --git a/Sources/Motion/Utilities/Animator.swift b/Sources/Motion/Utilities/Animator.swift index ad71dbcf..2d9c40fe 100644 --- a/Sources/Motion/Utilities/Animator.swift +++ b/Sources/Motion/Utilities/Animator.swift @@ -11,9 +11,9 @@ import QuartzCore import UIKit #endif - /// The default Animator that executes all of Motion's animations. -public class Animator: NSObject, AnimationDriverObserver { +@MainActor +public class Animator: NSObject, AnimationDriverObserver, Sendable { private var animationDriver: AnimationDriver? { get { if let _animationDriverStore = _animationDriverStore { diff --git a/Sources/Motion/Utilities/CoreAnimationHelpers.swift b/Sources/Motion/Utilities/CoreAnimationHelpers.swift index e81c6cc5..b1ec0d78 100644 --- a/Sources/Motion/Utilities/CoreAnimationHelpers.swift +++ b/Sources/Motion/Utilities/CoreAnimationHelpers.swift @@ -14,6 +14,7 @@ import QuartzCore - Parameters: - block: The block that, when run, will have a new `CATransaction` created for it and will disable actions, which will be committed after the block completes. */ +@MainActor public func CADisableActions(_ block: () -> Void) { CATransaction.begin() CATransaction.setDisableActions(true) @@ -35,6 +36,7 @@ public extension CALayer { - key: The key to be associated with the generated `CAKeyframeAnimation` when added to the layer. - keyPath: The key path to animate. The key path is relative to the layer. */ + @MainActor func add(_ animation: CAKeyframeAnimationEmittable, forKey key: String, keyPath: String) { if keyPath.isEmpty { assertionFailure("The keyPath must not be nil.") diff --git a/Sources/Motion/Utilities/NSScreen+AnimationEnvironment.swift b/Sources/Motion/Utilities/NSScreen+AnimationEnvironment.swift index 0fd1832e..4b852b26 100644 --- a/Sources/Motion/Utilities/NSScreen+AnimationEnvironment.swift +++ b/Sources/Motion/Utilities/NSScreen+AnimationEnvironment.swift @@ -10,6 +10,7 @@ public final class DefaultAnimationEnvironment: NSScreen { } } +@MainActor extension NSScreen: AnimationEnvironment { private var environmentStorage: AnimationEnvironmentStorage { AnimationEnvironmentStorage.shared } @@ -35,6 +36,7 @@ private extension NSDeviceDescriptionKey { static let screenNumber = NSDeviceDescriptionKey("NSScreenNumber") } +@MainActor private final class AnimationEnvironmentStorage { static let shared = AnimationEnvironmentStorage() diff --git a/Tests/MotionTests/BasicAnimationTests.swift b/Tests/MotionTests/BasicAnimationTests.swift index e3ea0a16..252fb644 100644 --- a/Tests/MotionTests/BasicAnimationTests.swift +++ b/Tests/MotionTests/BasicAnimationTests.swift @@ -2,6 +2,7 @@ import XCTest @testable import Motion +@MainActor final class BasicAnimationTests: XCTestCase { // MARK: - BasicAnimation Tests @@ -125,8 +126,10 @@ final class BasicAnimationTests: XCTestCase { } override class func tearDown() { - // All the animations should be deallocated by now. Hopefully NSMapTable plays nice. - XCTAssert(Animator.shared.runningAnimations.allObjects.count == 0) + Task { @MainActor in + // All the animations should be deallocated by now. Hopefully NSMapTable plays nice. + XCTAssert(Animator.shared.runningAnimations.allObjects.count == 0) + } } } diff --git a/Tests/MotionTests/DecayAnimationTests.swift b/Tests/MotionTests/DecayAnimationTests.swift index c2dc72d6..78d3b060 100644 --- a/Tests/MotionTests/DecayAnimationTests.swift +++ b/Tests/MotionTests/DecayAnimationTests.swift @@ -2,6 +2,7 @@ import XCTest @testable import Motion +@MainActor final class DecayAnimationTests: XCTestCase { // MARK: - DecayAnimation Tests @@ -137,8 +138,10 @@ final class DecayAnimationTests: XCTestCase { } override class func tearDown() { - // All the animations should be deallocated by now. Hopefully NSMapTable plays nice. - XCTAssert(Animator.shared.runningAnimations.allObjects.count == 0) + Task { @MainActor in + // All the animations should be deallocated by now. Hopefully NSMapTable plays nice. + XCTAssert(Animator.shared.runningAnimations.allObjects.count == 0) + } } } diff --git a/Tests/MotionTests/MotionTests.swift b/Tests/MotionTests/MotionTests.swift index 69be6921..6b133aa5 100644 --- a/Tests/MotionTests/MotionTests.swift +++ b/Tests/MotionTests/MotionTests.swift @@ -2,6 +2,7 @@ import XCTest @testable import Motion +@MainActor final class MotionTests: XCTestCase { // MARK: - EquatableEnough Tests @@ -221,16 +222,20 @@ final class MotionTests: XCTestCase { } override class func tearDown() { - // All the animations should be deallocated by now. Hopefully NSMapTable plays nice. - XCTAssert(Animator.shared.runningAnimations.allObjects.count == 0) + Task { @MainActor in + // All the animations should be deallocated by now. Hopefully NSMapTable plays nice. + XCTAssert(Animator.shared.runningAnimations.allObjects.count == 0) + } } } +@MainActor internal func tickAnimationOnce(_ animation: Animation, dt: CFTimeInterval = 0.016) { animation.tick(frame: .init(timestamp: 0, targetTimestamp: dt)) } +@MainActor internal func tickAnimationUntilResolved(_ animation: Animation, dt: CFTimeInterval = 0.016, maxDuration: CFTimeInterval = 10.0) { for _ in stride(from: 0.0, through: maxDuration, by: dt) { animation.tick(frame: .init(timestamp: 0, targetTimestamp: dt)) @@ -240,6 +245,7 @@ internal func tickAnimationUntilResolved(_ animation: Animation, dt: CFTimeInter } } +@MainActor internal func tickAnimationForDuration(_ animation: Animation, dt: CFTimeInterval = 0.016, maxDuration: CFTimeInterval = 10.0) { for _ in stride(from: 0.0, through: maxDuration, by: dt) { animation.tick(frame: .init(timestamp: 0, targetTimestamp: dt)) diff --git a/Tests/MotionTests/SpringAnimationTests.swift b/Tests/MotionTests/SpringAnimationTests.swift index 6781250d..b2a9ec58 100644 --- a/Tests/MotionTests/SpringAnimationTests.swift +++ b/Tests/MotionTests/SpringAnimationTests.swift @@ -2,6 +2,7 @@ import XCTest @testable import Motion +@MainActor final class SpringAnimationTests: XCTestCase { // MARK: - SpringAnimation Tests @@ -266,8 +267,10 @@ final class SpringAnimationTests: XCTestCase { } override class func tearDown() { - // All the animations should be deallocated by now. Hopefully NSMapTable plays nice. - XCTAssert(Animator.shared.runningAnimations.allObjects.count == 0) + Task { @MainActor in + // All the animations should be deallocated by now. Hopefully NSMapTable plays nice. + XCTAssert(Animator.shared.runningAnimations.allObjects.count == 0) + } } }