Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AppKitNavigation - Common #209

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ let package = Package(
name: "UIKitNavigation",
targets: ["UIKitNavigation"]
),
.library(
name: "AppKitNavigation",
targets: ["AppKitNavigation"]
),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),
Expand All @@ -31,6 +35,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -73,6 +78,18 @@ let package = Package(
.target(
name: "UIKitNavigationShim"
),
.target(
name: "AppKitNavigation",
dependencies: [
"SwiftNavigation",
"AppKitNavigationShim",
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
.product(name: "IdentifiedCollections", package: "swift-identified-collections"),
]
),
.target(
name: "AppKitNavigationShim"
),
.testTarget(
name: "UIKitNavigationTests",
dependencies: [
Expand Down
17 changes: 17 additions & 0 deletions Package@swift-6.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ let package = Package(
name: "UIKitNavigation",
targets: ["UIKitNavigation"]
),
.library(
name: "AppKitNavigation",
targets: ["AppKitNavigation"]
),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),
Expand All @@ -31,6 +35,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -73,6 +78,18 @@ let package = Package(
.target(
name: "UIKitNavigationShim"
),
.target(
name: "AppKitNavigation",
dependencies: [
"SwiftNavigation",
"AppKitNavigationShim",
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
.product(name: "IdentifiedCollections", package: "swift-identified-collections"),
]
),
.target(
name: "AppKitNavigationShim"
),
.testTarget(
name: "UIKitNavigationTests",
dependencies: [
Expand Down
102 changes: 102 additions & 0 deletions Sources/AppKitNavigation/AppKitAnimation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit

#if canImport(SwiftUI)
import SwiftUI
#endif

import SwiftNavigation

/// Executes a closure with the specified animation and returns the result.
///
/// - Parameters:
/// - animation: An animation, set in the ``UITransaction/appKit`` property of the thread's
/// current transaction.
/// - body: A closure to execute.
/// - completion: A completion to run when the animation is complete.
/// - Returns: The result of executing the closure with the specified animation.
@MainActor
public func withAppKitAnimation<Result>(
_ animation: AppKitAnimation? = .default,
_ body: () throws -> Result,
completion: (@Sendable (Bool?) -> Void)? = nil
) rethrows -> Result {
var transaction = UITransaction()
transaction.appKit.animation = animation
if let completion {
transaction.appKit.addAnimationCompletion(completion)
}
return try withUITransaction(transaction, body)
}

/// The way a view changes over time to create a smooth visual transition from one state to
/// another.
public struct AppKitAnimation: Hashable, Sendable {
fileprivate let framework: Framework

@MainActor
func perform<Result>(
_ body: () throws -> Result,
completion: ((Bool?) -> Void)? = nil
) rethrows -> Result {
switch framework {
case let .swiftUI(animation):
_ = animation
fatalError()
case let .appKit(animation):
var result: Swift.Result<Result, Error>?
NSAnimationContext.runAnimationGroup { context in
context.duration = animation.duration
result = Swift.Result(catching: body)
} completionHandler: {
completion?(true)
}

return try result!._rethrowGet()
}
}

fileprivate enum Framework: Hashable, Sendable {
case appKit(AppKit)
case swiftUI(Animation)

fileprivate struct AppKit: Hashable, Sendable {
fileprivate var duration: TimeInterval

func hash(into hasher: inout Hasher) {
hasher.combine(duration)
}
}
}
}

extension AppKitAnimation {
/// Performs am animation using a timing curve corresponding to the motion of a physical spring.
///
/// A value description of
/// `UIView.animate(withDuration:delay:dampingRatio:velocity:options:animations:completion:)`
/// that can be used with ``withAppKitAnimation(_:_:completion:)``.
///
/// - Parameters:
/// - duration: The total duration of the animations, measured in seconds. If you specify a
/// negative value or `0`, the changes are made without animating them.
/// - Returns: An animation using a timing curve corresponding to the motion of a physical
/// spring.
public static func animate(
withDuration duration: TimeInterval = 0.25
) -> Self {
Self(
framework: .appKit(
Framework.AppKit(
duration: duration
)
)
)
}

/// A default animation instance.
public static var `default`: Self {
return .animate()
}
}
#endif
36 changes: 36 additions & 0 deletions Sources/AppKitNavigation/Internal/AssociatedKeys.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import AppKit

struct AssociatedKeys {
var keys: [AnyHashableMetatype: UnsafeMutableRawPointer] = [:]

mutating func key<T>(of type: T.Type) -> UnsafeMutableRawPointer {
let key = AnyHashableMetatype(type)
if let associatedKey = keys[key] {
return associatedKey
} else {
let associatedKey = malloc(1)!
keys[key] = associatedKey
return associatedKey
}
}
}

struct AnyHashableMetatype: Hashable {
static func == (lhs: AnyHashableMetatype, rhs: AnyHashableMetatype) -> Bool {
return lhs.base == rhs.base
}

let base: Any.Type

init(_ base: Any.Type) {
self.base = base
}

func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(base))
}
}

#endif
35 changes: 35 additions & 0 deletions Sources/AppKitNavigation/Internal/AssumeIsolated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import Foundation

extension MainActor {
// NB: This functionality was not back-deployed in Swift 5.9
static func _assumeIsolated<T: Sendable>(
_ operation: @MainActor () throws -> T,
file: StaticString = #fileID,
line: UInt = #line
) rethrows -> T {
#if swift(<5.10)
typealias YesActor = @MainActor () throws -> T
typealias NoActor = () throws -> T

guard Thread.isMainThread else {
fatalError(
"Incorrect actor executor assumption; Expected same executor as \(self).",
file: file,
line: line
)
}

return try withoutActuallyEscaping(operation) { (_ fn: @escaping YesActor) throws -> T in
let rawFn = unsafeBitCast(fn, to: NoActor.self)
return try rawFn()
}
#else
return try assumeIsolated(operation, file: file, line: line)
#endif
}
}


#endif
20 changes: 20 additions & 0 deletions Sources/AppKitNavigation/Internal/ErrorMechanism.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
@rethrows
protocol _ErrorMechanism {
associatedtype Output
func get() throws -> Output
}

extension _ErrorMechanism {
func _rethrowError() rethrows -> Never {
_ = try _rethrowGet()
fatalError()
}

func _rethrowGet() rethrows -> Output {
return try get()
}
}

extension Result: _ErrorMechanism {}
#endif
3 changes: 3 additions & 0 deletions Sources/AppKitNavigation/Internal/Exports.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
@_exported import SwiftNavigation
#endif
12 changes: 12 additions & 0 deletions Sources/AppKitNavigation/Internal/ToOptionalUnit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
extension Bool {
struct Unit: Hashable, Identifiable {
var id: Unit { self }
}

var toOptionalUnit: Unit? {
get { self ? Unit() : nil }
set { self = newValue != nil }
}
}
#endif
Loading