-
-
Save maximkrouk/4933b2e870b6cb42b9a157a77ed91da1 to your computer and use it in GitHub Desktop.
State-Machine based SwiftUI keyframe Animations
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
import Combine | |
import Foundation | |
struct Reducer<State, Event> { | |
typealias Reduce = (inout State, Event) -> Void | |
var reduce: Reduce | |
} | |
protocol VoidObservable { | |
var publisher: AnyPublisher<Void, Never> { get } | |
} | |
protocol AsyncRunnable { | |
func run(delay: DispatchTimeInterval, completion: (() -> Void)?) | |
} | |
extension VoidObservable where Self: ObservableObject { | |
var publisher: AnyPublisher<Void, Never> { objectWillChange.map { _ in }.eraseToAnyPublisher() } | |
} | |
protocol AnyAnimationDriver: AsyncRunnable, VoidObservable {} | |
@dynamicMemberLookup | |
final class AnimationDriver<Animation: StatefulAnimation>: AnyAnimationDriver, ObservableObject { | |
@Published | |
private var animation: Animation | |
var reducer: Reducer<Animation, Animation.Status> | |
init(_ animation: Animation, reduce: @escaping Reducer<Animation, Animation.Status>.Reduce) { | |
self.animation = animation | |
self.reducer = Reducer(reduce: reduce) | |
} | |
func run(delay: DispatchTimeInterval = .none, completion: (() -> Void)? = .none) { | |
DispatchQueue.main.asyncAfter(delay) { | |
self.run(Array(Animation.Status.allCases), completion: completion) | |
} | |
} | |
private func run(_ statusCollection: [Animation.Status], completion: (() -> Void)? = .none) { | |
guard let status = statusCollection.first else { return completion?() ?? () } | |
DispatchQueue.main.asyncAfter(animation.partialDuration) { | |
self.reducer.reduce(&self.animation, status) | |
self.run(Array(statusCollection.dropFirst()), completion: completion) | |
} | |
} | |
subscript<T>(dynamicMember keyPath: WritableKeyPath<Animation, T>) -> T { | |
get { animation[keyPath: keyPath] } | |
set { animation[keyPath: keyPath] = newValue } | |
} | |
} | |
final class AnimationDriverGroup: AnyAnimationDriver, ObservableObject { | |
private var subscriptions: Set<AnyCancellable> = [] | |
private(set) var drivers: [AnyAnimationDriver] = [] | |
init(_ drivers: [AnyAnimationDriver] = []) { | |
overrideDrivers(with: drivers) | |
} | |
func overrideDrivers(with drivers: [AnyAnimationDriver]) { | |
self.drivers = drivers | |
self.subscriptions.removeAll() | |
drivers.map(\.publisher) | |
.forEach { publisher in | |
publisher | |
.sink(receiveValue: objectWillChange.send) | |
.store(in: &subscriptions) | |
} | |
} | |
func run(delay: DispatchTimeInterval = .none, completion: (() -> Void)? = .none) { | |
DispatchQueue.main.asyncAfter(delay) { | |
self.run(self.drivers, completion: completion) | |
} | |
} | |
func runInParallel(delay: DispatchTimeInterval = .none, completion: (() -> Void)? = .none) { | |
DispatchQueue.main.asyncAfter(delay) { | |
let group = DispatchGroup() | |
self.drivers.forEach { driver in | |
group.enter() | |
driver.run(delay: .none, completion: { group.leave() }) | |
} | |
group.notify(queue: .main) { completion?() } | |
} | |
} | |
private func run(_ drivers: [AnyAnimationDriver], completion: (() -> Void)?) { | |
guard let driver = drivers.first else { return completion?() ?? () } | |
driver.run(delay: .none) { | |
self.run(Array(drivers.dropFirst()), completion: completion) | |
} | |
} | |
} |
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
import Foundation | |
extension DispatchTimeInterval: ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral { | |
static var none: Self { .seconds(0) } | |
public init(integerLiteral value: Int) { | |
self.init(floatLiteral: Double(value)) | |
} | |
public init(floatLiteral value: Double) { | |
self = .microseconds(Int(value * pow(10, 6))) | |
} | |
static func seconds(_ value: Double) -> Self { .init(floatLiteral: value) } | |
} | |
extension DispatchQueue { | |
func asyncAfter(_ intervalFromNow: DispatchTimeInterval, execute: @escaping () -> Void) { | |
asyncAfter(deadline: .now() + intervalFromNow, execute: execute) | |
} | |
} |
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
import SwiftUI | |
extension _ViewModifier { | |
static func animated<T: View, Animation: StatefulAnimation>(by driver: AnimationDriver<Animation>) | |
-> _ViewModifier<T, AnyView> { | |
.init { content in | |
driver.model.apply(to: content.animation(.none)) | |
.animation(driver.template) | |
.eraseToAnyView() | |
} | |
} | |
} |
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
import SwiftUI | |
extension View { | |
func apply(_ applicable: ViewApplicable) -> some View { | |
applicable.apply(to: self) | |
} | |
} | |
protocol ViewApplicable { | |
func apply<T: View>(to view: T) -> AnyView | |
} | |
protocol AnimationPartialDispatchDurationProvider { | |
var partialDuration: DispatchTimeInterval { get } | |
} | |
extension AnimationPartialDispatchDurationProvider { | |
var partialDuration: DispatchTimeInterval { .none } | |
} | |
protocol StatefulAnimation: ViewApplicable, AnimationPartialDispatchDurationProvider { | |
associatedtype Status: CaseIterable | |
associatedtype Model: ViewApplicable | |
var model: Model { get set } | |
var template: SwiftUI.Animation { get set } | |
} | |
extension StatefulAnimation { | |
typealias Tempalte = SwiftUI.Animation | |
} | |
extension StatefulAnimation { | |
subscript<T>(dynamicMember keyPath: WritableKeyPath<Model, T>) -> T { | |
get { model[keyPath: keyPath] } | |
set { model[keyPath: keyPath] = newValue } | |
} | |
func apply<T>(to view: T) -> AnyView where T : View { | |
model.apply(to: view) | |
} | |
} | |
protocol StatefulAnimationState { | |
associatedtype Model: ViewApplicable | |
var model: Model { get set } | |
var animation: SwiftUI.Animation { get set } | |
} |
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
import SwiftUI | |
extension View { | |
func eraseToAnyView() -> AnyView { .init(self) } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
Depends on _ViewModifier
Back to index