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 2 commits
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
11 changes: 11 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 Down Expand Up @@ -73,6 +77,13 @@ let package = Package(
.target(
name: "UIKitNavigationShim"
),
.target(
name: "AppKitNavigation",
dependencies: [
"SwiftNavigation",
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
]
),
.testTarget(
name: "UIKitNavigationTests",
dependencies: [
Expand Down
11 changes: 11 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 Down Expand Up @@ -73,6 +77,13 @@ let package = Package(
.target(
name: "UIKitNavigationShim"
),
.target(
name: "AppKitNavigation",
dependencies: [
"SwiftNavigation",
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
]
),
.testTarget(
name: "UIKitNavigationTests",
dependencies: [
Expand Down
80 changes: 80 additions & 0 deletions Sources/AppKitNavigation/AppKitAnimation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit

#if canImport(SwiftUI)
import SwiftUI
#endif

import SwiftNavigation

@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)
}

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 {
public static func animate(
withDuration duration: TimeInterval = 0.25
) -> Self {
Self(
framework: .appKit(
Framework.AppKit(
duration: duration
)
)
)
}

public static var `default`: Self {
return .animate()
}
}
#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
73 changes: 73 additions & 0 deletions Sources/AppKitNavigation/Observe.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
@_spi(Internals) import SwiftNavigation
import AppKit

@MainActor
extension NSObject {
@discardableResult
public func observe(_ apply: @escaping @MainActor @Sendable () -> Void) -> ObservationToken {
observe { _ in apply() }
}

@discardableResult
public func observe(
_ apply: @escaping @MainActor @Sendable (_ transaction: UITransaction) -> Void
) -> ObservationToken {
let token = SwiftNavigation.observe { transaction in
MainActor._assumeIsolated {
withUITransaction(transaction) {
if transaction.appKit.disablesAnimations {
NSView.performWithoutAnimation { apply(transaction) }
for completion in transaction.appKit.animationCompletions {
completion(true)
}
} else if let animation = transaction.appKit.animation {
return animation.perform(
{ apply(transaction) },
completion: transaction.appKit.animationCompletions.isEmpty
? nil
: {
for completion in transaction.appKit.animationCompletions {
completion($0)
}
}
)
} else {
apply(transaction)
for completion in transaction.appKit.animationCompletions {
completion(true)
}
}
}
}
} task: { transaction, work in
DispatchQueue.main.async {
withUITransaction(transaction, work)
}
}
tokens.append(token)
return token
}

fileprivate var tokens: [Any] {
get {
objc_getAssociatedObject(self, Self.tokensKey) as? [Any] ?? []
}
set {
objc_setAssociatedObject(self, Self.tokensKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

private static let tokensKey = malloc(1)!
}

extension NSView {
fileprivate static func performWithoutAnimation(_ block: () -> Void) {
NSAnimationContext.runAnimationGroup { context in
context.allowsImplicitAnimation = false
block()
}
}
}

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

extension UIBinding {
public func animation(_ animation: AppKitAnimation? = .default) -> Self {
var binding = self
binding.transaction.appKit.animation = animation
return binding
}
}
#endif
42 changes: 42 additions & 0 deletions Sources/AppKitNavigation/UITransaction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#if canImport(AppKit) && !targetEnvironment(macCatalyst)

import SwiftNavigation

extension UITransaction {
public init(animation: AppKitAnimation? = nil) {
self.init()
appKit.animation = animation
}

public var appKit: AppKit {
get { self[AppKitKey.self] }
set { self[AppKitKey.self] = newValue }
}

private enum AppKitKey: UITransactionKey {
static let defaultValue = AppKit()
}

public struct AppKit: Sendable {
public var animation: AppKitAnimation?

public var disablesAnimations = false

var animationCompletions: [@Sendable (Bool?) -> Void] = []

public mutating func addAnimationCompletion(
_ completion: @escaping @Sendable (Bool?) -> Void
) {
animationCompletions.append(completion)
}
}
}

private enum AnimationCompletionsKey: UITransactionKey {
static let defaultValue: [@Sendable (Bool?) -> Void] = []
}

private enum DisablesAnimationsKey: UITransactionKey {
static let defaultValue = false
}
#endif
25 changes: 25 additions & 0 deletions Sources/AppKitNavigationShim/include/shim.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#if __has_include(<TargetConditionals.h>)
#include <TargetConditionals.h>

#if __has_include(<AppKit/AppKit.h>) && !TARGET_OS_MACCATALYST
@import AppKit;

NS_ASSUME_NONNULL_BEGIN

@interface NSViewController (AppKitNavigation)

@property BOOL hasViewAppeared;
@property (nullable) void (^ onDismiss)();
@property NSArray<void (^)()> *onViewAppear;

@end

@interface NSSavePanel (AppKitNavigation)
@property (nullable) void (^ AppKitNavigation_onFinalURL)(NSURL *_Nullable);
@property (nullable) void (^ AppKitNavigation_onFinalURLs)(NSArray<NSURL *> *);
@end


NS_ASSUME_NONNULL_END
#endif
#endif /* if __has_include(<TargetConditionals.h>) */
Loading