Skip to content

Commit

Permalink
Add dedicated owner for camera animation by compass view (#2082)
Browse files Browse the repository at this point in the history
  • Loading branch information
maios authored Apr 24, 2024
1 parent e50fa7b commit 864b629
Show file tree
Hide file tree
Showing 14 changed files with 132 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,17 @@ final class CameraAnimationExample: UIViewController, ExampleProtocol {
}.store(in: &cancelables)

mapView.camera
.onCameraAnimatorStarted { animator in
.onCameraAnimatorStarted
.observe { animator in
print("Animator started: \(animator.owner)")
}
.store(in: &cancelables)

mapView.camera
.onCameraAnimatorStopped { (animator, isCancelled) in
print("Animator stopped: \(animator.owner), isCancelled: \( isCancelled)")
.onCameraAnimatorFinished
.owned(by: .compass)
.observe { animator in
print("Animator finished: \(animator.owner)")
}
.store(in: &cancelables)
}
Expand Down
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ mapView.mapboxMap.setMapStyleContent {
```swift
// Observe start event of any CameraAnimator owned by AnimationOwner.cameraAnimationsManager
mapView.camera
.onCameraAnimatorStarted(with: [.cameraAnimationsManager]) { cameraAnimator in
.onCameraAnimatorStarted
.owned(by: .cameraAnimationsManager)
.observe { cameraAnimator in
// Handle camera animation started here.
}
.store(in: &cancelables)
// Observe stop events of any CameraAnimator owned by AnimationOwner.cameraAnimationsManager, either when the animator has finished animating or it is interrupted
// Observe finished events of any CameraAnimator
mapView.camera
.onCameraAnimatorStopped { (animator, isCancelled) in
.onCameraAnimatorFinished
.observe { animator in
// Handle camera animation stopped here.
}
.store(in: &cancelables)
Expand Down
2 changes: 2 additions & 0 deletions Sources/MapboxMaps/Camera/AnimationOwner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public struct AnimationOwner: RawRepresentable, Equatable {

public static let unspecified = AnimationOwner(rawValue: "com.mapbox.maps.unspecified")

public static let compass = AnimationOwner(rawValue: "com.mapbox.maps.ornaments.compass")

internal static let cameraAnimationsManager = AnimationOwner(rawValue: "com.mapbox.maps.cameraAnimationsManager")

internal static let defaultViewportTransition = AnimationOwner(rawValue: "com.mapbox.maps.viewport.defaultTransition")
Expand Down
39 changes: 23 additions & 16 deletions Sources/MapboxMaps/Camera/CameraAnimationsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,23 +175,30 @@ public final class CameraAnimationsManager {
animations: animations)
}

/// Adds an observer when a ``CameraAnimator`` has started with the given `owner`.
///
/// - Parameters
/// - owners: The list of ``AnimationOwner``s that own the starting camera animator. The default is an empty list, which observes all animation owners.
/// - handler: The handler to be invoked with a ``CameraAnimator`` as the argument when this animator is starting.
public func onCameraAnimatorStarted(with owners: [AnimationOwner] = [], handler: @escaping OnCameraAnimatorStarted) -> AnyCancelable {
let observer = CameraAnimatorStatusObserver(owners: owners, onStarted: handler)
return impl.add(cameraAnimatorStatusObserver: observer)
/// A stream that emits an event when a ``CameraAnimator`` has started
public var onCameraAnimatorStarted: Signal<CameraAnimator> {
impl.onCameraAnimatorStatusChanged
.compactMap { (animator, status) in
guard status == .started else { return nil }
return animator
}
}

/// Adds an observer when a ``CameraAnimator`` has stopped with the given `owner`.
///
/// - Parameters
/// - owners: The list of ``AnimationOwner``s that own the stopping camera animator. The default is an empty list, which observes all animation owners.
/// - handler: The handler to be invoked with a ``CameraAnimator`` as the argument when this animator is stopping.
public func onCameraAnimatorStopped(owners: [AnimationOwner] = [], handler: @escaping OnCameraAnimatorStopped) -> AnyCancelable {
let observer = CameraAnimatorStatusObserver(owners: owners, onStopped: handler)
return impl.add(cameraAnimatorStatusObserver: observer)
/// A stream that emits an event when a ``CameraAnimator`` has finished.
public var onCameraAnimatorFinished: Signal<CameraAnimator> {
impl.onCameraAnimatorStatusChanged
.compactMap { (animator, status) in
guard status == .stopped(reason: .finished) else { return nil }
return animator
}
}

/// A stream that emits an event when a ``CameraAnimator`` has cancelled.
public var onCameraAnimatorCancelled: Signal<CameraAnimator> {
impl.onCameraAnimatorStatusChanged
.compactMap { (animator, status) in
guard status == .stopped(reason: .cancelled) else { return nil }
return animator
}
}
}
37 changes: 25 additions & 12 deletions Sources/MapboxMaps/Camera/CameraAnimationsManagerImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal protocol CameraAnimationsManagerProtocol: AnyObject {
func ease(to: CameraOptions,
duration: TimeInterval,
curve: UIView.AnimationCurve,
animationOwner: AnimationOwner,
completion: AnimationCompletion?) -> Cancelable

func decelerate(location: CGPoint,
Expand Down Expand Up @@ -52,14 +53,16 @@ internal protocol CameraAnimationsManagerProtocol: AnyObject {
duration: TimeInterval,
curve: TimingCurve,
owner: AnimationOwner) -> SimpleCameraAnimatorProtocol
func add(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) -> AnyCancelable
var onCameraAnimatorStatusChanged: Signal<CameraAnimatorStatusPayload> { get }
}

internal final class CameraAnimationsManagerImpl: CameraAnimationsManagerProtocol {

private let factory: CameraAnimatorsFactoryProtocol
private let runner: CameraAnimatorsRunnerProtocol

var onCameraAnimatorStatusChanged: Signal<CameraAnimatorStatusPayload> { runner.onCameraAnimatorStatusChanged }

/// See ``CameraAnimationsManager/cameraAnimators``.
internal var cameraAnimators: [CameraAnimator] {
return runner.cameraAnimators
Expand Down Expand Up @@ -104,15 +107,18 @@ internal final class CameraAnimationsManagerImpl: CameraAnimationsManagerProtoco

/// See ``CameraAnimationsManager/ease(to:duration:curve:completion:)``.
@discardableResult
internal func ease(to: CameraOptions,
duration: TimeInterval,
curve: UIView.AnimationCurve,
completion: AnimationCompletion?) -> Cancelable {
runner.cancelAnimations(withOwners: [.cameraAnimationsManager])
func ease(
to: CameraOptions,
duration: TimeInterval,
curve: UIView.AnimationCurve,
animationOwner: AnimationOwner,
completion: AnimationCompletion?
) -> Cancelable {
runner.cancelAnimations(withOwners: [animationOwner])
let animatorImpl = factory.makeBasicCameraAnimator(
duration: duration,
curve: curve,
animationOwner: .cameraAnimationsManager,
animationOwner: animationOwner,
animations: { (transition) in
transition.center.toValue = to.center
transition.padding.toValue = to.padding
Expand Down Expand Up @@ -238,11 +244,18 @@ internal final class CameraAnimationsManagerImpl: CameraAnimationsManagerProtoco
runner.add(animator)
return animator
}
}

func add(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) -> AnyCancelable {
runner.add(cameraAnimatorStatusObserver: observer)
return AnyCancelable { [weak runner] in
runner?.remove(cameraAnimatorStatusObserver: observer)
}
extension CameraAnimationsManagerProtocol {

/// See ``CameraAnimationsManager/ease(to:duration:curve:completion:)``.
@discardableResult
func ease(
to: CameraOptions,
duration: TimeInterval,
curve: UIView.AnimationCurve,
completion: AnimationCompletion?
) -> Cancelable {
ease(to: to, duration: duration, curve: curve, animationOwner: .cameraAnimationsManager, completion: completion)
}
}
23 changes: 6 additions & 17 deletions Sources/MapboxMaps/Camera/CameraAnimatorStatusObservable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,12 @@ enum CameraAnimatorStatus: Equatable {
}
}

final class CameraAnimatorStatusObserver {
let owners: [AnimationOwner]
let onStarted: OnCameraAnimatorStarted?
let onStopped: OnCameraAnimatorStopped?
typealias CameraAnimatorStatusPayload = (CameraAnimator, CameraAnimatorStatus)

init(
owners: [AnimationOwner],
onStarted: OnCameraAnimatorStarted? = nil,
onStopped: OnCameraAnimatorStopped? = nil
) {
self.owners = owners
self.onStarted = onStarted
self.onStopped = onStopped
extension Signal where Payload == any CameraAnimator {

/// Creates new Signal from upstream filtering out ``CameraAnimator`` that is not owned by the given ``AnimationOwner``.
public func owned(by owner: AnimationOwner) -> Self {
filter { $0.owner == owner }
}
}

/// A closure to handle event when a camera animator has started.
public typealias OnCameraAnimatorStarted = (CameraAnimator) -> Void
/// A closure to handle event when a camera animator has stopped.
public typealias OnCameraAnimatorStopped = (CameraAnimator, _ isCancelled: Bool) -> Void
23 changes: 6 additions & 17 deletions Sources/MapboxMaps/Camera/CameraAnimatorsRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ internal protocol CameraAnimatorsRunnerProtocol: AnyObject {
func cancelAnimations(withOwners owners: [AnimationOwner])
func cancelAnimations(withOwners owners: [AnimationOwner], andTypes: [AnimationType])
func add(_ animator: CameraAnimatorProtocol)
func add(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver)
func remove(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver)
var onCameraAnimatorStatusChanged: Signal<CameraAnimatorStatusPayload> { get }
}

internal final class CameraAnimatorsRunner: CameraAnimatorsRunnerProtocol {
Expand All @@ -26,7 +25,8 @@ internal final class CameraAnimatorsRunner: CameraAnimatorsRunnerProtocol {
}
}

private var cameraAnimatorStatusObservers = WeakSet<CameraAnimatorStatusObserver>()
private var cameraAnimatorStatusSignal = SignalSubject<CameraAnimatorStatusPayload>()
var onCameraAnimatorStatusChanged: Signal<CameraAnimatorStatusPayload> { cameraAnimatorStatusSignal.signal }

/// See ``CameraAnimationsManager/cameraAnimators``.
internal var cameraAnimators: [CameraAnimator] {
Expand Down Expand Up @@ -94,14 +94,6 @@ internal final class CameraAnimatorsRunner: CameraAnimatorsRunnerProtocol {
animator.stopAnimation()
}
}

func add(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) {
cameraAnimatorStatusObservers.add(observer)
}

func remove(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) {
cameraAnimatorStatusObservers.remove(observer)
}
}

extension CameraAnimatorsRunner {
Expand All @@ -121,9 +113,7 @@ extension CameraAnimatorsRunner {
runningCameraAnimators.append(cameraAnimator)
mapboxMap.beginAnimation()
}
for observer in cameraAnimatorStatusObservers.allObjects where observer.owners.isEmpty || observer.owners.contains(cameraAnimator.owner) {
observer.onStarted?(cameraAnimator)
}
cameraAnimatorStatusSignal.send((cameraAnimator, .started))
}

/// When an animator stops running, `CameraAnimationsRunner` releases its strong reference to
Expand All @@ -137,9 +127,7 @@ extension CameraAnimatorsRunner {
runningCameraAnimators.removeAll { $0 === cameraAnimator }
mapboxMap.endAnimation()
}
for observer in cameraAnimatorStatusObservers.allObjects where observer.owners.isEmpty || observer.owners.contains(cameraAnimator.owner) {
observer.onStopped?(cameraAnimator, reason == .cancelled)
}
cameraAnimatorStatusSignal.send((cameraAnimator, .stopped(reason: reason)))
}

/// When an animator is paused, `CameraAnimationsRunner` releases its strong reference to
Expand All @@ -153,5 +141,6 @@ extension CameraAnimatorsRunner {
runningCameraAnimators.removeAll { $0 === cameraAnimator }
mapboxMap.endAnimation()
}
cameraAnimatorStatusSignal.send((cameraAnimator, .paused))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
- ``CameraTransition``
- ``FlyToCameraAnimator``
- ``AnimationCompletion``
- ``OnCameraAnimatorStarted``
- ``OnCameraAnimatorStopped``
- ``AnimationOwner``
- ``TimingCurve``

Expand Down
1 change: 1 addition & 0 deletions Sources/MapboxMaps/Ornaments/OrnamentsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ public final class OrnamentsManager {
to: CameraOptions(bearing: 0),
duration: 0.3,
curve: .easeOut,
animationOwner: .compass,
completion: nil)
}
view.addSubview(compassView)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,24 +106,26 @@ final class CameraAnimationsManagerImplTests: XCTestCase {
let duration = TimeInterval.random(in: 0...10)
let curve = UIView.AnimationCurve.random()
let completion = Stub<UIViewAnimatingPosition, Void>()
let animationOwner = AnimationOwner.random()

let cancelable = impl.ease(
to: camera,
duration: duration,
curve: curve,
animationOwner: animationOwner,
completion: completion.call(with:))

// cancels any existing high-level animator (identified based on the owner)
XCTAssertEqual(
runner.cancelAnimationsWithOwnersStub.invocations.map(\.parameters),
[[.cameraAnimationsManager]])
[[animationOwner]])

// creates the new animator
XCTAssertEqual(factory.makeBasicCameraAnimatorWithCurveStub.invocations.count, 1)
let factoryInvocation = try XCTUnwrap(factory.makeBasicCameraAnimatorWithCurveStub.invocations.first)
XCTAssertEqual(factoryInvocation.parameters.duration, duration)
XCTAssertEqual(factoryInvocation.parameters.curve, curve)
XCTAssertEqual(factoryInvocation.parameters.animationOwner, .cameraAnimationsManager)
XCTAssertEqual(factoryInvocation.parameters.animationOwner, animationOwner)
let animatorImpl = try XCTUnwrap(factoryInvocation.returnValue as? MockBasicCameraAnimator)

// configures the transition in the animations block
Expand Down Expand Up @@ -417,15 +419,4 @@ final class CameraAnimationsManagerImplTests: XCTestCase {
XCTAssertIdentical(runner.addStub.invocations.first?.parameters, returnedAnimator)
XCTAssertIdentical(animator, returnedAnimator)
}

func testAddCameraAnimatorStatusObserver() {
let observer = CameraAnimatorStatusObserver(owners: [], onStarted: nil, onStopped: nil)

let cancelable = impl.add(cameraAnimatorStatusObserver: observer)
XCTAssertIdentical(runner.addCameraAnimatorStatusObserverStub.invocations[0].parameters, observer)
XCTAssertTrue(runner.removeCameraAnimatorStatusObserverStub.invocations.isEmpty)

cancelable.cancel()
XCTAssertIdentical(runner.removeCameraAnimatorStatusObserverStub.invocations[0].parameters, observer)
}
}
Loading

0 comments on commit 864b629

Please sign in to comment.