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

Animated assign subscriber #17

Merged
merged 8 commits into from
Jun 25, 2020
93 changes: 93 additions & 0 deletions Sources/AnimatedAssignSubscriber.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// AnimatedAssignSubscriber.swift
//
// Created by Marin Todorov on 5/3/20.
//

import Foundation
import Combine

#if canImport(UIKit)
import UIKit

/// A list of animations that can be used with `Publisher.assign(to:on:animation:)`
public enum AssignTransition {
public enum Direction {
case top, bottom, left, right
}

/// Flip from either bottom, top, left, or right.
case flip(direction: Direction, duration: TimeInterval)

/// Cross fade with previous value.
case fade(duration: TimeInterval)

/// A custom animation. Do not include your own code to update the target of the assign subscriber.
case animation(duration: TimeInterval, options: UIView.AnimationOptions, animations: () -> Void, completion: ((Bool) -> Void)?)
}

extension Publisher where Self.Failure == Never {

/// Behaves identically to `Publisher.assign(to:on:)` except that it allows the user to
/// "wrap" emitting output in an animation transition.
///
/// For example if you assign values to a
/// `UILabel` on screen you can make it flip over when each new value is set:
/// ```
/// myPublisher
/// .assign(to: \.text, on: myLabel, animation: .flip(direction: .bottom, duration: 0.33))
/// ```
/// Or you can make an image view crossfade any new images it displays:
/// ```
/// myImagePublisher
/// .assign(to: \.image, on: myImageView, animation: .fade(duration: 0.5))
/// ```
/// Finally you can set any custom animation including choice side effects as so. For example
/// this is how you can move the label each time you set a new value via `assign(to:on:animation:)`:
/// ```
/// myPublisher
/// .assign(to: \.text, on: myLabel, animation: .animation(duration: 0.33, options: .curveEaseIn, animations: { _ in
/// myLabel.center.x += 10.0
/// }, completion: nil))
/// ```
icanzilb marked this conversation as resolved.
Show resolved Hide resolved
public func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root, animation: AssignTransition) -> AnyCancellable {
icanzilb marked this conversation as resolved.
Show resolved Hide resolved
guard let view = object as? UIView else {
return assign(to: keyPath, on: object)
}

var transition: UIView.AnimationOptions
var duration: TimeInterval

switch animation {
case .fade(let interval):
duration = interval
transition = .transitionCrossDissolve
case .flip(let dir, let interval):
icanzilb marked this conversation as resolved.
Show resolved Hide resolved
duration = interval
switch dir {
case .bottom: transition = .transitionFlipFromBottom
case .top: transition = .transitionFlipFromTop
case .left: transition = .transitionFlipFromLeft
case .right: transition = .transitionFlipFromRight
}
case .animation(let interval, let options, let animations, let completion):
return self
.handleEvents(receiveOutput: { value in
UIView.animate(withDuration: interval, delay: 0, options: options, animations: {
object[keyPath: keyPath] = value
animations()
}, completion: completion)
})
.assign(to: keyPath, on: object)
icanzilb marked this conversation as resolved.
Show resolved Hide resolved
}

return self
.handleEvents(receiveOutput: { value in
UIView.transition(with: view, duration: duration, options: transition, animations: {
object[keyPath: keyPath] = value
}, completion: nil)
icanzilb marked this conversation as resolved.
Show resolved Hide resolved
})
.assign(to: keyPath, on: object)
}
}
#endif