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
96 changes: 96 additions & 0 deletions Sources/AnimatedAssignSubscriber.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// 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))
/// ```
///
/// You may also provide a custom animation block, as follows:
///
/// ```
/// myPublisher
/// .assign(to: \.text, on: myLabel, animation: .animation(duration: 0.33, options: .curveEaseIn, animations: { _ in
/// myLabel.center.x += 10.0
/// }, completion: nil))
/// ```
public func assign<Root: UIView>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root, animation: AssignTransition) -> AnyCancellable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this looks misaligned... tabs and spaces together?
image

var transition: UIView.AnimationOptions
var duration: TimeInterval

switch animation {
case .fade(let interval):
duration = interval
transition = .transitionCrossDissolve
case let .flip(dir, interval):
duration = interval
switch dir {
case .bottom: transition = .transitionFlipFromBottom
case .top: transition = .transitionFlipFromTop
case .left: transition = .transitionFlipFromLeft
case .right: transition = .transitionFlipFromRight
}
case let .animation(interval, options, animations, completion):
// Use a custom animation.
return 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another question that popped to my head,
You're already setting the value inside the animation block, and the assign will do so again.

Maybe it's worth swapping the assign with something like sink(receiveValue: { _ in }). Just to active the chain? (same for below)

}

// Use one of the built-in transitions like flip or crossfade.
return self
.handleEvents(receiveOutput: { value in
UIView.transition(with: object,
duration: duration,
options: transition,
animations: {
object[keyPath: keyPath] = value
},
completion: nil)
})
.assign(to: keyPath, on: object)
}
}
#endif