-
Notifications
You must be signed in to change notification settings - Fork 140
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
stephencelis
merged 9 commits into
pointfreeco:main
from
MxIris-Library-Forks:appkit-navigation-common
Aug 26, 2024
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e2c5db4
Common
Mx-Iris 0c61b63
Remove unused code
Mx-Iris c2bdb0d
Remove unused code
Mx-Iris 82c7737
Merge branch 'main' into appkit-navigation-common
stephencelis 8e5e4e6
Integrate custom transaction
stephencelis 2b91081
address fatal error
stephencelis cad947b
Round out animation
stephencelis e863c54
Update Package.swift
mbrandonw 00ed18a
Update Package@swift-6.0.swift
mbrandonw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
#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 .appKit(animation): | ||
var result: Swift.Result<Result, Error>? | ||
NSAnimationContext.runAnimationGroup { context in | ||
context.allowsImplicitAnimation = true | ||
context.duration = animation.duration | ||
context.timingFunction = animation.timingFunction | ||
result = Swift.Result(catching: body) | ||
} completionHandler: { | ||
completion?(true) | ||
} | ||
return try result!._rethrowGet() | ||
|
||
case let .swiftUI(animation): | ||
var result: Swift.Result<Result, Error>? | ||
#if swift(>=6) | ||
if #available(macOS 15, *) { | ||
NSAnimationContext.animate(animation) { | ||
result = Swift.Result(catching: body) | ||
} completion: { | ||
completion?(true) | ||
} | ||
return try result!._rethrowGet() | ||
} | ||
#endif | ||
_ = animation | ||
fatalError() | ||
} | ||
} | ||
|
||
fileprivate enum Framework: Hashable, Sendable { | ||
case appKit(AppKit) | ||
case swiftUI(Animation) | ||
|
||
fileprivate struct AppKit: Hashable, @unchecked Sendable { | ||
fileprivate var duration: TimeInterval | ||
fileprivate var timingFunction: CAMediaTimingFunction? | ||
|
||
func hash(into hasher: inout Hasher) { | ||
hasher.combine(duration) | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension AppKitAnimation { | ||
@available(macOS 15, *) | ||
public init(_ animation: Animation) { | ||
self.init(framework: .swiftUI(animation)) | ||
} | ||
|
||
public static func animate( | ||
duration: TimeInterval = 0.25, | ||
timingFunction: CAMediaTimingFunction? = nil | ||
) -> Self { | ||
Self( | ||
framework: .appKit( | ||
Framework.AppKit( | ||
duration: duration, | ||
timingFunction: timingFunction | ||
) | ||
) | ||
) | ||
} | ||
|
||
public static var `default`: Self { | ||
.animate() | ||
} | ||
|
||
public static var linear: Self { .linear(duration: 0.25) } | ||
|
||
public static func linear(duration: TimeInterval) -> Self { | ||
.animate(duration: duration, timingFunction: CAMediaTimingFunction(name: .linear)) | ||
} | ||
|
||
public static var easeIn: Self { .easeIn(duration: 0.25) } | ||
|
||
public static func easeIn(duration: TimeInterval) -> Self { | ||
.animate(duration: duration, timingFunction: CAMediaTimingFunction(name: .easeIn)) | ||
} | ||
|
||
public static var easeOut: Self { .easeOut(duration: 0.25) } | ||
|
||
public static func easeOut(duration: TimeInterval) -> Self { | ||
.animate(duration: duration, timingFunction: CAMediaTimingFunction(name: .easeOut)) | ||
} | ||
|
||
public static var easeInOut: Self { .easeInOut(duration: 0.25) } | ||
|
||
public static func easeInOut(duration: TimeInterval) -> Self { | ||
.animate(duration: duration, timingFunction: CAMediaTimingFunction(name: .easeInEaseOut)) | ||
} | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#if canImport(AppKit) && !targetEnvironment(macCatalyst) | ||
@_exported import SwiftNavigation | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
#if canImport(AppKit) && !targetEnvironment(macCatalyst) | ||
import AppKit | ||
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: _UICustomTransactionKey { | ||
static let defaultValue = AppKit() | ||
|
||
static func perform( | ||
value: AppKit, | ||
operation: @Sendable () -> Void | ||
) { | ||
MainActor._assumeIsolated { | ||
if value.disablesAnimations { | ||
NSAnimationContext.runAnimationGroup { context in | ||
context.allowsImplicitAnimation = false | ||
operation() | ||
} | ||
for completion in value.animationCompletions { | ||
completion(true) | ||
} | ||
} else if let animation = value.animation { | ||
return animation.perform( | ||
{ operation() }, | ||
completion: value.animationCompletions.isEmpty | ||
? nil | ||
: { | ||
for completion in value.animationCompletions { | ||
completion($0) | ||
} | ||
} | ||
) | ||
} else { | ||
operation() | ||
for completion in value.animationCompletions { | ||
completion(true) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
@rethrows | ||
package protocol _ErrorMechanism { | ||
associatedtype Output | ||
func get() throws -> Output | ||
} | ||
|
||
extension _ErrorMechanism { | ||
package func _rethrowError() rethrows -> Never { | ||
_ = try _rethrowGet() | ||
fatalError() | ||
} | ||
|
||
package func _rethrowGet() rethrows -> Output { | ||
return try get() | ||
} | ||
} | ||
|
||
extension Result: _ErrorMechanism {} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
extension Bool { | ||
package struct Unit: Hashable, Identifiable { | ||
package var id: Unit { self } | ||
|
||
package init() {} | ||
} | ||
|
||
package var toOptionalUnit: Unit? { | ||
get { self ? Unit() : nil } | ||
set { self = newValue != nil } | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,7 @@ | |
|
||
@Perceptible | ||
@MainActor | ||
class Model { | ||
private class Model { | ||
var count = 0 | ||
} | ||
#endif |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Mx-Iris I've familiarized myself a bit more with this API by adding a configurable timing function, and it also occurred to me that because
withAppKitAnimation
is intended to be called from the model, whereview.animator
is not available, that it probably makes sense to always setallowsImplicitAnimation
totrue
. Does this sound right to you?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's right.
NSView
/NSWindow
implements theNSAnimatablePropertyContainer
protocol. When the view'sframe
,size
, orposition
changes, it can produce animated effects. Through its animator, these properties can be used transparently to generate animations.Setting
NSAnimationContext
'sallowsImplicitAnimation
to true, directly modifying the view's properties will produce animations. This is kind of like UIKit'sUIView.animate
method. But only theframe
,frameSize
, andframeOrigin
properties are supported. More property animations require alayer-backed
view, that is, the view'swantsLayer
= true.