diff --git a/Apps/Examples/Examples/All Examples/CameraAnimationExample.swift b/Apps/Examples/Examples/All Examples/CameraAnimationExample.swift index 9eeacf9bdaf4..adc4fcdd28a7 100644 --- a/Apps/Examples/Examples/All Examples/CameraAnimationExample.swift +++ b/Apps/Examples/Examples/All Examples/CameraAnimationExample.swift @@ -31,5 +31,17 @@ final class CameraAnimationExample: UIViewController, ExampleProtocol { self?.finish() } }.store(in: &cancelables) + + mapView.camera + .onCameraAnimatorStarted { animator in + print("Animator started: \(animator.owner)") + } + .store(in: &cancelables) + + mapView.camera + .onCameraAnimatorStopped { (animator, isCancelled) in + print("Animator stopped: \(animator.owner), isCancelled: \( isCancelled)") + } + .store(in: &cancelables) } } diff --git a/Apps/Examples/Examples/All Examples/CameraAnimatorsExample.swift b/Apps/Examples/Examples/All Examples/CameraAnimatorsExample.swift index 481e981eccb9..d7e4103b6295 100644 --- a/Apps/Examples/Examples/All Examples/CameraAnimatorsExample.swift +++ b/Apps/Examples/Examples/All Examples/CameraAnimatorsExample.swift @@ -76,7 +76,7 @@ final class CameraAnimatorsExample: UIViewController, ExampleProtocol { func startCameraAnimations() { os_log(.default, "Animating zoom from zoom to lvl 14") - // Declare an animator that changes the map's + // Declare an animator that changes the map's bearing let bearingAnimator = mapView.camera.makeAnimator(duration: 4, curve: .easeInOut) { (transition) in transition.bearing.toValue = -45 } @@ -87,6 +87,9 @@ final class CameraAnimatorsExample: UIViewController, ExampleProtocol { self.animationState = .stop } } + bearingAnimator.onStarted.observe { + os_log(.default, "Bearing animator has started") + }.store(in: &cancelables) // Declare an animator that changes the map's pitch. let pitchAnimator = mapView.camera.makeAnimator(duration: 2, curve: .easeInOut) { (transition) in diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0c2868b575..345ab4188301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,32 @@ Mapbox welcomes participation and contributions from everyone. * Allow to assign slot to 2D and 3D location indicators. +* Allow observing start/stop event of `CameraAnimator` + You can observe start/stop event of `CameraAnimator` by using new `CameraAnimationsManager` APIs as shown below + ``` + // Observe start event of any CameraAnimator owned by AnimationOwner.cameraAnimationsManager + mapView.camera + .onCameraAnimatorStarted(with: [.cameraAnimationsManager]) { 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 + mapView.camera + .onCameraAnimatorStopped { (animator, isCancelled) in + // Handle camera animation stopped here. + } + .store(in: &cancelables) + ``` + You can also observe directly on an instance of `CameraAnimator` when using low-level camera APIs to create a custom animator + ``` + // Declare an animator that changes the map's bearing + let bearingAnimator = mapView.camera.makeAnimator(duration: 4, curve: .easeInOut) { (transition) in + transition.bearing.toValue = -45 + } + bearingAnimator.onStarted.observe { + // Bearing animator has started. + }.store(in: &cancelables) + ``` * Allow to add slots at runtime. * Expose API to interact with styile imports using Declarative Styling and regular imperative API. * Expose `StyleImport` for declarative styling as `MapStyleContent`. diff --git a/Sources/MapboxMaps/Camera/BasicCameraAnimator.swift b/Sources/MapboxMaps/Camera/BasicCameraAnimator.swift index 782e0c1b20cc..56d5bef7c896 100644 --- a/Sources/MapboxMaps/Camera/BasicCameraAnimator.swift +++ b/Sources/MapboxMaps/Camera/BasicCameraAnimator.swift @@ -14,8 +14,6 @@ public final class BasicCameraAnimator: CameraAnimator, CameraAnimatorProtocol { impl.animationType } - internal weak var delegate: CameraAnimatorDelegate? - /// Defines the transition that will occur to the `CameraOptions` of the renderer due to this animator public var transition: CameraTransition? { impl.transition @@ -49,9 +47,33 @@ public final class BasicCameraAnimator: CameraAnimator, CameraAnimatorProtocol { set { impl.fractionComplete = newValue } } - internal init(impl: BasicCameraAnimatorProtocol) { + var onCameraAnimatorStatusChanged: Signal { + impl.onCameraAnimatorStatusChanged + } + + /// Emits a signal when this animator has started. + public var onStarted: Signal { + onCameraAnimatorStatusChanged + .filter { $0 == .started } + .map { _ in } + } + + /// Emits a signal when this animator has finished. + public var onFinished: Signal { + onCameraAnimatorStatusChanged + .filter { $0 == .stopped(reason: .finished) } + .map { _ in } + } + + /// Emits a signal when this animator is cancelled. + public var onCancelled: Signal { + onCameraAnimatorStatusChanged + .filter { $0 == .stopped(reason: .cancelled) } + .map { _ in } + } + + init(impl: BasicCameraAnimatorProtocol) { self.impl = impl - impl.delegate = self } /// Starts the animation if this animator is in `inactive` state. Also used to resume a "paused" @@ -101,13 +123,3 @@ public final class BasicCameraAnimator: CameraAnimator, CameraAnimatorProtocol { impl.update() } } - -extension BasicCameraAnimator: BasicCameraAnimatorDelegate { - internal func basicCameraAnimatorDidStartRunning(_ animator: BasicCameraAnimatorProtocol) { - delegate?.cameraAnimatorDidStartRunning(self) - } - - internal func basicCameraAnimatorDidStopRunning(_ animator: BasicCameraAnimatorProtocol) { - delegate?.cameraAnimatorDidStopRunning(self) - } -} diff --git a/Sources/MapboxMaps/Camera/BasicCameraAnimatorImpl.swift b/Sources/MapboxMaps/Camera/BasicCameraAnimatorImpl.swift index 3dfb98d7df57..38adeb6619dd 100644 --- a/Sources/MapboxMaps/Camera/BasicCameraAnimatorImpl.swift +++ b/Sources/MapboxMaps/Camera/BasicCameraAnimatorImpl.swift @@ -1,12 +1,6 @@ import UIKit -internal protocol BasicCameraAnimatorDelegate: AnyObject { - func basicCameraAnimatorDidStartRunning(_ animator: BasicCameraAnimatorProtocol) - func basicCameraAnimatorDidStopRunning(_ animator: BasicCameraAnimatorProtocol) -} - internal protocol BasicCameraAnimatorProtocol: AnyObject { - var delegate: BasicCameraAnimatorDelegate? { get set } var owner: AnimationOwner { get } var animationType: AnimationType { get } var transition: CameraTransition? { get } @@ -15,6 +9,7 @@ internal protocol BasicCameraAnimatorProtocol: AnyObject { var isReversed: Bool { get set } var pausesOnCompletion: Bool { get set } var fractionComplete: Double { get set } + var onCameraAnimatorStatusChanged: Signal { get } func startAnimation() func startAnimation(afterDelay delay: TimeInterval) func pauseAnimation() @@ -51,8 +46,6 @@ internal final class BasicCameraAnimatorImpl: BasicCameraAnimatorProtocol { private let mainQueue: MainQueueProtocol - internal weak var delegate: BasicCameraAnimatorDelegate? - /// Represents the animation that this animator is attempting to execute private let animation: Animation @@ -68,6 +61,9 @@ internal final class BasicCameraAnimatorImpl: BasicCameraAnimatorProtocol { } } + private let cameraAnimatorStatusSignal = SignalSubject() + var onCameraAnimatorStatusChanged: Signal { cameraAnimatorStatusSignal.signal } + /// The state from of the animator. internal var state: UIViewAnimatingState { propertyAnimator.state } @@ -75,14 +71,16 @@ internal final class BasicCameraAnimatorImpl: BasicCameraAnimatorProtocol { didSet { switch (oldValue, internalState) { case (.initial, .running), (.paused, .running): - delegate?.basicCameraAnimatorDidStartRunning(self) - case (.running, .paused), (.running, .final): - delegate?.basicCameraAnimatorDidStopRunning(self) + cameraAnimatorStatusSignal.send(.started) + case (.running, .paused): + cameraAnimatorStatusSignal.send(.paused) + case (.running, .final(let position)), (.paused, .final(let position)): + let isCancelled = position != .end + cameraAnimatorStatusSignal.send(.stopped(reason: isCancelled ? .cancelled : .finished)) default: // this matches cases where… // * oldValue and internalState are the same // * initial transitions to paused - // * paused transitions to final // * initial transitions to final // * the transition is invalid… // * running/paused/final --> initial diff --git a/Sources/MapboxMaps/Camera/CameraAnimationsManager.swift b/Sources/MapboxMaps/Camera/CameraAnimationsManager.swift index 1e9c132eeb58..8812e386f8e8 100644 --- a/Sources/MapboxMaps/Camera/CameraAnimationsManager.swift +++ b/Sources/MapboxMaps/Camera/CameraAnimationsManager.swift @@ -174,4 +174,24 @@ public final class CameraAnimationsManager { animationOwner: animationOwner, 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) + } + + /// 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) + } } diff --git a/Sources/MapboxMaps/Camera/CameraAnimationsManagerImpl.swift b/Sources/MapboxMaps/Camera/CameraAnimationsManagerImpl.swift index 6cc8c7128ad7..5ec99cdfc693 100644 --- a/Sources/MapboxMaps/Camera/CameraAnimationsManagerImpl.swift +++ b/Sources/MapboxMaps/Camera/CameraAnimationsManagerImpl.swift @@ -52,6 +52,7 @@ internal protocol CameraAnimationsManagerProtocol: AnyObject { duration: TimeInterval, curve: TimingCurve, owner: AnimationOwner) -> SimpleCameraAnimatorProtocol + func add(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) -> AnyCancelable } internal final class CameraAnimationsManagerImpl: CameraAnimationsManagerProtocol { @@ -237,4 +238,11 @@ 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) + } + } } diff --git a/Sources/MapboxMaps/Camera/CameraAnimator.swift b/Sources/MapboxMaps/Camera/CameraAnimator.swift index 430ec4f63857..76529277bb24 100644 --- a/Sources/MapboxMaps/Camera/CameraAnimator.swift +++ b/Sources/MapboxMaps/Camera/CameraAnimator.swift @@ -17,9 +17,6 @@ internal protocol CameraAnimatorProtocol: CameraAnimator { /// Type of the embeded animation var animationType: AnimationType { get } - /// Implementations must use a weak reference. - var delegate: CameraAnimatorDelegate? { get set } - /// Adds a completion block to the animator. If the animator is already complete, /// implementations should invoke the completion block asynchronously with the /// same `UIViewAnimatingPosition` value as when it completed. @@ -30,9 +27,6 @@ internal protocol CameraAnimatorProtocol: CameraAnimator { /// Called at each display link to allow animators to update the camera. func update() -} -internal protocol CameraAnimatorDelegate: AnyObject { - func cameraAnimatorDidStartRunning(_ cameraAnimator: CameraAnimatorProtocol) - func cameraAnimatorDidStopRunning(_ cameraAnimator: CameraAnimatorProtocol) + var onCameraAnimatorStatusChanged: Signal { get } } diff --git a/Sources/MapboxMaps/Camera/CameraAnimatorStatusObservable.swift b/Sources/MapboxMaps/Camera/CameraAnimatorStatusObservable.swift new file mode 100644 index 000000000000..145cfaf22525 --- /dev/null +++ b/Sources/MapboxMaps/Camera/CameraAnimatorStatusObservable.swift @@ -0,0 +1,32 @@ +import Foundation + +enum CameraAnimatorStatus: Equatable { + case started + case stopped(reason: StopReason) + case paused + + enum StopReason: Equatable { + case finished, cancelled + } +} + +final class CameraAnimatorStatusObserver { + let owners: [AnimationOwner] + let onStarted: OnCameraAnimatorStarted? + let onStopped: OnCameraAnimatorStopped? + + init( + owners: [AnimationOwner], + onStarted: OnCameraAnimatorStarted? = nil, + onStopped: OnCameraAnimatorStopped? = nil + ) { + self.owners = owners + self.onStarted = onStarted + self.onStopped = onStopped + } +} + +/// 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 diff --git a/Sources/MapboxMaps/Camera/CameraAnimatorsRunner.swift b/Sources/MapboxMaps/Camera/CameraAnimatorsRunner.swift index f2bfcd9ea4be..70bb4c6245ef 100644 --- a/Sources/MapboxMaps/Camera/CameraAnimatorsRunner.swift +++ b/Sources/MapboxMaps/Camera/CameraAnimatorsRunner.swift @@ -8,6 +8,8 @@ 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) } internal final class CameraAnimatorsRunner: CameraAnimatorsRunnerProtocol { @@ -24,6 +26,8 @@ internal final class CameraAnimatorsRunner: CameraAnimatorsRunnerProtocol { } } + private var cameraAnimatorStatusObservers = WeakSet() + /// See ``CameraAnimationsManager/cameraAnimators``. internal var cameraAnimators: [CameraAnimator] { return allCameraAnimators.allObjects @@ -34,8 +38,8 @@ internal final class CameraAnimatorsRunner: CameraAnimatorsRunnerProtocol { /// Strong references only to running camera animators private var runningCameraAnimators = [CameraAnimatorProtocol]() - private let mapboxMap: MapboxMapProtocol + private var cancelables: Set = [] internal init(mapboxMap: MapboxMapProtocol) { self.mapboxMap = mapboxMap @@ -71,16 +75,35 @@ internal final class CameraAnimatorsRunner: CameraAnimatorsRunnerProtocol { } } - internal func add(_ animator: CameraAnimatorProtocol) { - animator.delegate = self + func add(_ animator: CameraAnimatorProtocol) { allCameraAnimators.add(animator) + + animator.onCameraAnimatorStatusChanged.observe { [weak self] status in + switch status { + case .started: + self?.cameraAnimatorDidStartRunning(animator) + case .stopped(let reason): + self?.cameraAnimatorDidStopRunning(animator, reason: reason) + case .paused: + self?.cameraAnimatorDidPause(animator) + } + } + .store(in: &cancelables) if !isEnabled { animator.stopAnimation() } } + + func add(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) { + cameraAnimatorStatusObservers.add(observer) + } + + func remove(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) { + cameraAnimatorStatusObservers.remove(observer) + } } -extension CameraAnimatorsRunner: CameraAnimatorDelegate { +extension CameraAnimatorsRunner { /// When an animator starts running, `CameraAnimationsRunner` takes a strong reference to it /// so that it stays alive while it is running. It also calls `beginAnimation` on `MapboxMap`. /// @@ -92,11 +115,14 @@ extension CameraAnimatorsRunner: CameraAnimatorDelegate { /// /// Moving this responsibility to `CameraAnimationsRunner` means that if the `MapView` is /// deallocated, these strong references will be released as well. - internal func cameraAnimatorDidStartRunning(_ cameraAnimator: CameraAnimatorProtocol) { + private func cameraAnimatorDidStartRunning(_ cameraAnimator: CameraAnimatorProtocol) { if !runningCameraAnimators.contains(where: { $0 === cameraAnimator }) { runningCameraAnimators.append(cameraAnimator) mapboxMap.beginAnimation() } + for observer in cameraAnimatorStatusObservers.allObjects where observer.owners.isEmpty || observer.owners.contains(cameraAnimator.owner) { + observer.onStarted?(cameraAnimator) + } } /// When an animator stops running, `CameraAnimationsRunner` releases its strong reference to @@ -105,7 +131,23 @@ extension CameraAnimatorsRunner: CameraAnimatorDelegate { /// /// See `cameraAnimatorDidStartRunning(_:)` for further discussion of the rationale for this /// architecture. - internal func cameraAnimatorDidStopRunning(_ cameraAnimator: CameraAnimatorProtocol) { + private func cameraAnimatorDidStopRunning(_ cameraAnimator: CameraAnimatorProtocol, reason: CameraAnimatorStatus.StopReason) { + if runningCameraAnimators.contains(where: { $0 === cameraAnimator }) { + 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) + } + } + + /// When an animator is paused, `CameraAnimationsRunner` releases its strong reference to + /// it so that it can be deinited if there are no other owning references. It also calls `endAnimation` + /// on `MapboxMap`, upon resuming, it will be added back to the runner. + /// + /// See `cameraAnimatorDidStartRunning(_:)` for further discussion of the rationale for this + /// architecture. + private func cameraAnimatorDidPause(_ cameraAnimator: CameraAnimatorProtocol) { if runningCameraAnimators.contains(where: { $0 === cameraAnimator }) { runningCameraAnimators.removeAll { $0 === cameraAnimator } mapboxMap.endAnimation() diff --git a/Sources/MapboxMaps/Camera/FlyToCameraAnimator.swift b/Sources/MapboxMaps/Camera/FlyToCameraAnimator.swift index 4652f92a9acd..fda008b78c19 100644 --- a/Sources/MapboxMaps/Camera/FlyToCameraAnimator.swift +++ b/Sources/MapboxMaps/Camera/FlyToCameraAnimator.swift @@ -29,7 +29,6 @@ public final class FlyToCameraAnimator: CameraAnimator, CameraAnimatorProtocol { } let animationType: AnimationType - weak var delegate: CameraAnimatorDelegate? private let mapboxMap: MapboxMapProtocol private let mainQueue: MainQueueProtocol @@ -39,13 +38,38 @@ public final class FlyToCameraAnimator: CameraAnimator, CameraAnimatorProtocol { private let unitBezier: UnitBezier private var completionBlocks = [AnimationCompletion]() + private let cameraAnimatorStatusSignal = SignalSubject() + var onCameraAnimatorStatusChanged: Signal { cameraAnimatorStatusSignal.signal } + + /// Emits a signal when this animator has started. + public var onStarted: Signal { + onCameraAnimatorStatusChanged + .filter { $0 == .started } + .map { _ in } + } + + /// Emits a signal when this animator has finished. + public var onFinished: Signal { + onCameraAnimatorStatusChanged + .filter { $0 == .stopped(reason: .finished) } + .map { _ in } + } + + /// Emits a signal when this animator is cancelled. + public var onCancelled: Signal { + onCameraAnimatorStatusChanged + .filter { $0 == .stopped(reason: .cancelled) } + .map { _ in } + } + private var internalState = InternalState.initial { didSet { switch (oldValue, internalState) { case (.initial, .running): - delegate?.cameraAnimatorDidStartRunning(self) - case (.running, .final): - delegate?.cameraAnimatorDidStopRunning(self) + cameraAnimatorStatusSignal.send(.started) + case (.running, .final(let position)): + let isCancelled = position != .end + cameraAnimatorStatusSignal.send(.stopped(reason: isCancelled ? .cancelled : .finished)) default: // this matches cases where… // * oldValue and internalState are the same diff --git a/Sources/MapboxMaps/Camera/GestureDecelerationCameraAnimator.swift b/Sources/MapboxMaps/Camera/GestureDecelerationCameraAnimator.swift index a53059543cde..c178cca2180d 100644 --- a/Sources/MapboxMaps/Camera/GestureDecelerationCameraAnimator.swift +++ b/Sources/MapboxMaps/Camera/GestureDecelerationCameraAnimator.swift @@ -16,13 +16,17 @@ internal final class GestureDecelerationCameraAnimator: CameraAnimatorProtocol { private let dateProvider: DateProvider private var completionBlocks = [AnimationCompletion]() + var onCameraAnimatorStatusChanged: Signal { cameraAnimatorStatusSignal.signal } + private let cameraAnimatorStatusSignal = SignalSubject() + private var internalState = InternalState.initial { didSet { switch (oldValue, internalState) { case (.initial, .running): - delegate?.cameraAnimatorDidStartRunning(self) - case (.running, .final): - delegate?.cameraAnimatorDidStopRunning(self) + cameraAnimatorStatusSignal.send(.started) + case (.running, .final(let position)): + let isCancelled = position != .end + cameraAnimatorStatusSignal.send(.stopped(reason: isCancelled ? .cancelled : .finished)) default: // this matches cases where… // * oldValue and internalState are the same @@ -48,8 +52,6 @@ internal final class GestureDecelerationCameraAnimator: CameraAnimatorProtocol { internal let animationType: AnimationType - internal weak var delegate: CameraAnimatorDelegate? - internal init(location: CGPoint, velocity: CGPoint, decelerationFactor: CGFloat, diff --git a/Sources/MapboxMaps/Camera/SimpleCameraAnimator.swift b/Sources/MapboxMaps/Camera/SimpleCameraAnimator.swift index 429f65602f4a..2b80aed87ebc 100644 --- a/Sources/MapboxMaps/Camera/SimpleCameraAnimator.swift +++ b/Sources/MapboxMaps/Camera/SimpleCameraAnimator.swift @@ -55,10 +55,12 @@ internal final class SimpleCameraAnimator: SimpleCameraAnimatorProtocol { private let mainQueue: MainQueueProtocol private let cameraOptionsInterpolator: CameraOptionsInterpolatorProtocol private let dateProvider: DateProvider - internal weak var delegate: CameraAnimatorDelegate? private var completionHandlers = [AnimationCompletion]() + private let cameraAnimatorStatusSignal = SignalSubject() + var onCameraAnimatorStatusChanged: Signal { cameraAnimatorStatusSignal.signal } + /// The state of the animation. While the animation is running, the value is `.active`. Otherwise, the /// value is `.inactive`. internal var state: UIViewAnimatingState { @@ -74,9 +76,10 @@ internal final class SimpleCameraAnimator: SimpleCameraAnimatorProtocol { didSet { switch (oldValue, internalState) { case (.initial, .running): - delegate?.cameraAnimatorDidStartRunning(self) - case (.running, .final): - delegate?.cameraAnimatorDidStopRunning(self) + cameraAnimatorStatusSignal.send(.started) + case (.running, .final(let position)): + let isCancelled = position != .end + cameraAnimatorStatusSignal.send(.stopped(reason: isCancelled ? .cancelled : .finished)) default: // this matches cases where… // * oldValue and internalState are the same diff --git a/Sources/MapboxMaps/Documentation.docc/API Catalogs/Camera and coordinates.md b/Sources/MapboxMaps/Documentation.docc/API Catalogs/Camera and coordinates.md index 5375dcf88d4a..1c0e5c906f98 100644 --- a/Sources/MapboxMaps/Documentation.docc/API Catalogs/Camera and coordinates.md +++ b/Sources/MapboxMaps/Documentation.docc/API Catalogs/Camera and coordinates.md @@ -19,6 +19,8 @@ - ``CameraTransition`` - ``FlyToCameraAnimator`` - ``AnimationCompletion`` +- ``OnCameraAnimatorStarted`` +- ``OnCameraAnimatorStopped`` - ``AnimationOwner`` - ``TimingCurve`` diff --git a/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorImplIntegrationTests.swift b/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorImplIntegrationTests.swift index d8442f82e805..233a139d281f 100644 --- a/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorImplIntegrationTests.swift +++ b/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorImplIntegrationTests.swift @@ -8,8 +8,6 @@ final class BasicCameraAnimatorImplIntegrationTests: XCTestCase { var mapboxMap: MockMapboxMap! var mainQueue: MockMainQueue! var animator: BasicCameraAnimatorImpl! - // swiftlint:disable:next weak_delegate - var delegate: MockBasicCameraAnimatorDelegate! override func setUp() { super.setUp() @@ -32,12 +30,9 @@ final class BasicCameraAnimatorImplIntegrationTests: XCTestCase { transition.pitch.toValue = cameraOptionsTestValue.pitch! transition.padding.toValue = cameraOptionsTestValue.padding! } - delegate = MockBasicCameraAnimatorDelegate() - animator.delegate = delegate } override func tearDown() { - delegate = nil animator = nil mainQueue = nil mapboxMap = nil diff --git a/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorImplTests.swift b/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorImplTests.swift index ad98806bf0fd..410efadaf2da 100644 --- a/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorImplTests.swift +++ b/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorImplTests.swift @@ -31,10 +31,10 @@ final class BasicCameraAnimatorImplTests: XCTestCase { var mapboxMap: MockMapboxMap! var mainQueue: MockMainQueue! var animator: BasicCameraAnimatorImpl! - // swiftlint:disable:next weak_delegate - var delegate: MockBasicCameraAnimatorDelegate! + private var recordedCameraAnimatorStatus: [CameraAnimatorStatus] = [] var animationImpl: BasicCameraAnimatorImpl.Animation? + private var cancelables: Set = [] override func setUp() { super.setUp() @@ -52,12 +52,12 @@ final class BasicCameraAnimatorImplTests: XCTestCase { animation: { transition in self.animationImpl?(&transition) }) - delegate = MockBasicCameraAnimatorDelegate() - animator.delegate = delegate + animator.onCameraAnimatorStatusChanged + .observe { [unowned self] in self.recordedCameraAnimatorStatus.append($0) } + .store(in: &cancelables) } override func tearDown() { - delegate = nil animator = nil animationImpl = nil mainQueue = nil @@ -65,6 +65,7 @@ final class BasicCameraAnimatorImplTests: XCTestCase { cameraView = nil owner = nil propertyAnimator = nil + recordedCameraAnimatorStatus = [] super.tearDown() } @@ -101,15 +102,13 @@ final class BasicCameraAnimatorImplTests: XCTestCase { XCTAssertEqual(propertyAnimator.addCompletionStub.invocations.count, 1) XCTAssertNotNil(animator?.transition) XCTAssertEqual(animator?.transition?.toCameraOptions.zoom, 10) - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStartRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) animator.stopAnimation() XCTAssertEqual(propertyAnimator.stopAnimationStub.invocations.count, 1) XCTAssertEqual(propertyAnimator.finishAnimationStub.invocations.count, 1) XCTAssertEqual(propertyAnimator.finishAnimationStub.invocations.first?.parameters, .current) - XCTAssertEqual(delegate.basicCameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStopRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .stopped(reason: .cancelled)]) } func testStartAndStopAnimationAfterDelay() throws { @@ -122,12 +121,14 @@ final class BasicCameraAnimatorImplTests: XCTestCase { XCTAssertEqual(self.propertyAnimator.startAnimationStub.invocations.count, 1) XCTAssertEqual(self.propertyAnimator.addAnimationsStub.invocations.count, 1) XCTAssertEqual(self.propertyAnimator.addCompletionStub.invocations.count, 1) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) self.animator.stopAnimation() XCTAssertEqual(self.propertyAnimator.stopAnimationStub.invocations.count, 1) XCTAssertEqual(self.propertyAnimator.finishAnimationStub.invocations.count, 1) XCTAssertEqual(self.propertyAnimator.finishAnimationStub.invocations.first?.parameters, .current) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .stopped(reason: .cancelled)]) } func testCompletionBlockCalledForStartAndStopAfterDelay() { @@ -220,8 +221,8 @@ final class BasicCameraAnimatorImplTests: XCTestCase { XCTAssertEqual(propertyAnimator.stopAnimationStub.invocations.count, 0) XCTAssertEqual(propertyAnimator.finishAnimationStub.invocations.count, 0) - XCTAssertEqual(delegate.basicCameraAnimatorDidStopRunningStub.invocations.count, 0) XCTAssertEqual(completion.invocations.map(\.parameters), [.current]) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testStopAndStartAnimation() { @@ -233,7 +234,7 @@ final class BasicCameraAnimatorImplTests: XCTestCase { XCTAssertEqual(propertyAnimator.addAnimationsStub.invocations.count, 0) XCTAssertEqual(propertyAnimator.addCompletionStub.invocations.count, 0) XCTAssertNil(animator.transition) - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testStopAndStartAnimationAfterDelay() { @@ -245,7 +246,7 @@ final class BasicCameraAnimatorImplTests: XCTestCase { XCTAssertEqual(propertyAnimator.addAnimationsStub.invocations.count, 0) XCTAssertEqual(propertyAnimator.addCompletionStub.invocations.count, 0) XCTAssertNil(animator.transition) - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testStartAndStartAnimationAfterDelay() { @@ -255,14 +256,14 @@ final class BasicCameraAnimatorImplTests: XCTestCase { animator.startAnimation() propertyAnimator.addAnimationsStub.reset() propertyAnimator.addCompletionStub.reset() - delegate.basicCameraAnimatorDidStartRunningStub.reset() animator.startAnimation(afterDelay: .random(in: 0...10)) XCTAssertEqual(propertyAnimator.startAnimationAfterDelayStub.invocations.count, 0) XCTAssertEqual(propertyAnimator.addAnimationsStub.invocations.count, 0) XCTAssertEqual(propertyAnimator.addCompletionStub.invocations.count, 0) - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 0) + + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } func testStopAndPauseAnimation() { @@ -273,6 +274,7 @@ final class BasicCameraAnimatorImplTests: XCTestCase { XCTAssertEqual(propertyAnimator.pauseAnimationStub.invocations.count, 0) XCTAssertEqual(propertyAnimator.addAnimationsStub.invocations.count, 0) XCTAssertEqual(propertyAnimator.addCompletionStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testStartandPauseAnimationAfterDelay() throws { @@ -286,6 +288,7 @@ final class BasicCameraAnimatorImplTests: XCTestCase { XCTAssertEqual(propertyAnimator.pauseAnimationStub.invocations.count, 1) XCTAssertEqual(propertyAnimator.stopAnimationStub.invocations.count, 0) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .paused]) } func testStartAnimationAfterDelayIsRunning() { @@ -373,84 +376,80 @@ final class BasicCameraAnimatorImplTests: XCTestCase { XCTAssertNotNil(animator.transition, "The animator's transition property should not be nil after pausing the animation.") } - func testInformsDelegateWhenPausingAndStarting() { + func testSignalWhenPausingAndStarting() { animationImpl = { (transition) in transition.zoom.toValue = cameraOptionsTestValue.zoom! } animator.pauseAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) animator.startAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStartRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } - func testInformsDelegateWhenStartingAfterDelay() { + func testSignalWhenStartingAfterDelay() { animationImpl = { (transition) in transition.zoom.toValue = cameraOptionsTestValue.zoom! } animator.startAnimation(afterDelay: 1) - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStartRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } - func testInformsDelegateWhenStartingPausingAndStarting() { + func testSignalWhenStartingPausingAndStarting() { animationImpl = { (transition) in transition.zoom.toValue = cameraOptionsTestValue.zoom! } animator.startAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStartRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) animator.pauseAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStopRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .paused]) animator.startAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 2) - XCTAssertTrue(delegate.basicCameraAnimatorDidStartRunningStub.invocations.last?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .paused, .started]) } - func testInformsDelegateWhenPausingAndContinuing() { + func testSignalWhenPausingAndContinuing() { animationImpl = { (transition) in transition.zoom.toValue = cameraOptionsTestValue.zoom! } animator.pauseAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) animator.continueAnimation(withTimingParameters: nil, durationFactor: 1) - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStartRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } - func testInformsDelegateWhenStartingPausingAndContinuing() { + func testSignalWhenStartingPausingAndContinuingUntilFinished() throws { animationImpl = { (transition) in transition.zoom.toValue = cameraOptionsTestValue.zoom! } animator.startAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStartRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) animator.pauseAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStopRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .paused]) animator.continueAnimation(withTimingParameters: nil, durationFactor: 1) - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 2) - XCTAssertTrue(delegate.basicCameraAnimatorDidStartRunningStub.invocations.last?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .paused, .started]) + + let completion = try XCTUnwrap(propertyAnimator.addCompletionStub.invocations.first?.parameters) + completion(.end) + + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .paused, .started, .stopped(reason: .finished)]) } - func testInformsDelegateWhenPausingAndStopping() { + func testSignalWhenPausingAndStopping() { + animator.startAnimation() + animationImpl = { (transition) in transition.zoom.toValue = cameraOptionsTestValue.zoom! } animator.pauseAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 0) - XCTAssertEqual(delegate.basicCameraAnimatorDidStopRunningStub.invocations.count, 0) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .paused]) animator.stopAnimation() - XCTAssertEqual(delegate.basicCameraAnimatorDidStartRunningStub.invocations.count, 0) - XCTAssertEqual(delegate.basicCameraAnimatorDidStopRunningStub.invocations.count, 0) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .paused, .stopped(reason: .cancelled)]) } func testAnimatorCompletionUpdatesCameraIfAnimationCompletedAtEnd() throws { @@ -494,11 +493,10 @@ final class BasicCameraAnimatorImplTests: XCTestCase { transition.zoom.toValue = cameraOptionsTestValue.zoom! } animator.startAnimation() + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) let completion = try XCTUnwrap(propertyAnimator.addCompletionStub.invocations.first?.parameters) completion(.current) - - XCTAssertEqual(delegate.basicCameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.basicCameraAnimatorDidStopRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .stopped(reason: .cancelled)]) } } diff --git a/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorTests.swift b/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorTests.swift index 2b201fc9f3e1..3589390671f8 100644 --- a/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorTests.swift +++ b/Tests/MapboxMapsTests/Camera/BasicCameraAnimatorTests.swift @@ -6,18 +6,14 @@ final class BasicCameraAnimatorTests: XCTestCase { var impl: MockBasicCameraAnimator! var animator: BasicCameraAnimator! - var delegate: MockCameraAnimatorDelegate! override func setUp() { super.setUp() impl = MockBasicCameraAnimator() animator = BasicCameraAnimator(impl: impl) - delegate = MockCameraAnimatorDelegate() - animator.delegate = delegate } override func tearDown() { - delegate = nil animator = nil impl = nil super.tearDown() @@ -65,6 +61,24 @@ final class BasicCameraAnimatorTests: XCTestCase { XCTAssertEqual(animator.isReversed, impl.isReversed) } + func testOnCameraAnimatorStatusChanged() { + var expectedStatus: CameraAnimatorStatus? + let cancelable = animator.onCameraAnimatorStatusChanged.observe { status in + expectedStatus = status + } + + impl.$onCameraAnimatorStatusChanged.send(.started) + XCTAssertEqual(expectedStatus, .started) + + impl.$onCameraAnimatorStatusChanged.send(.stopped(reason: .finished)) + XCTAssertEqual(expectedStatus, .stopped(reason: .finished)) + + expectedStatus = nil + cancelable.cancel() + impl.$onCameraAnimatorStatusChanged.send(.stopped(reason: .finished)) + XCTAssertNil(expectedStatus) + } + func testPausesOnCompletion() { let value = Bool.random() @@ -157,17 +171,41 @@ final class BasicCameraAnimatorTests: XCTestCase { XCTAssertEqual(impl.updateStub.invocations.count, 1) } - func testBasicCameraAnimatorDidStartRunning() { - animator.basicCameraAnimatorDidStartRunning(impl) + func testOnStarted() { + var isStarted = false + let token = animator.onStarted.observe { + isStarted = true + } + + impl.$onCameraAnimatorStatusChanged.send(.started) + XCTAssertTrue(isStarted) + } + + func testOnFinished() { + var isFinished = false + var cancelables = Set() + animator.onFinished.observe { + isFinished = true + }.store(in: &cancelables) + animator.onCancelled.observe { + XCTFail("animator is not cancelled") + }.store(in: &cancelables) - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertIdentical(delegate.cameraAnimatorDidStartRunningStub.invocations.first?.parameters, animator) + impl.$onCameraAnimatorStatusChanged.send(.stopped(reason: .finished)) + XCTAssertTrue(isFinished) } - func testBasicCameraAnimatorDidStopRunning() { - animator.basicCameraAnimatorDidStopRunning(impl) + func testOnCancelled() { + var isCancelled = false + var cancelables = Set() + animator.onFinished.observe { + XCTFail("animator is not finished") + }.store(in: &cancelables) + animator.onCancelled.observe { + isCancelled = true + }.store(in: &cancelables) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertIdentical(delegate.cameraAnimatorDidStopRunningStub.invocations.first?.parameters, animator) + impl.$onCameraAnimatorStatusChanged.send(.stopped(reason: .cancelled)) + XCTAssertTrue(isCancelled) } } diff --git a/Tests/MapboxMapsTests/Camera/CameraAnimationsManagerImplTests.swift b/Tests/MapboxMapsTests/Camera/CameraAnimationsManagerImplTests.swift index 515311f2d558..cf90b66fb07e 100644 --- a/Tests/MapboxMapsTests/Camera/CameraAnimationsManagerImplTests.swift +++ b/Tests/MapboxMapsTests/Camera/CameraAnimationsManagerImplTests.swift @@ -417,4 +417,15 @@ 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) + } } diff --git a/Tests/MapboxMapsTests/Camera/CameraAnimationsManagerTests.swift b/Tests/MapboxMapsTests/Camera/CameraAnimationsManagerTests.swift index f33599f9ab56..fdcdb56a48a3 100644 --- a/Tests/MapboxMapsTests/Camera/CameraAnimationsManagerTests.swift +++ b/Tests/MapboxMapsTests/Camera/CameraAnimationsManagerTests.swift @@ -235,4 +235,34 @@ final class CameraAnimationsManagerTests: XCTestCase { XCTAssertEqual(impl.makeAnimatorWithDampingRatioStub.invocations.count, 1) XCTAssertEqual(impl.makeAnimatorWithDampingRatioStub.invocations.first?.parameters.animationOwner, .unspecified) } + + func testObservingCameraAnimatorStarted() throws { + let mockAnimator = MockCameraAnimator() + var isStarted = false + _ = cameraAnimationsManager.onCameraAnimatorStarted { animator in + XCTAssertIdentical(animator, mockAnimator) + isStarted = true + } + + let addedObserver = try XCTUnwrap(impl.addCameraAnimatorStatusObserverStub.invocations.last?.parameters) + addedObserver.onStarted?(mockAnimator) + + XCTAssertTrue(isStarted) + } + + func testObservingCameraAnimatorStopped() throws { + let mockAnimator = MockCameraAnimator() + + var isStopped = false + _ = cameraAnimationsManager.onCameraAnimatorStopped { (animator, isCancelled) in + XCTAssertIdentical(animator, mockAnimator) + XCTAssertFalse(isCancelled) + isStopped = true + } + + let addedObserver = try XCTUnwrap(impl.addCameraAnimatorStatusObserverStub.invocations.last?.parameters) + addedObserver.onStopped?(mockAnimator, false) + + XCTAssertTrue(isStopped) + } } diff --git a/Tests/MapboxMapsTests/Camera/CameraAnimatorsRunnerTests.swift b/Tests/MapboxMapsTests/Camera/CameraAnimatorsRunnerTests.swift index 1ccc402c26c7..3588d5cf3754 100644 --- a/Tests/MapboxMapsTests/Camera/CameraAnimatorsRunnerTests.swift +++ b/Tests/MapboxMapsTests/Camera/CameraAnimatorsRunnerTests.swift @@ -37,7 +37,7 @@ final class CameraAnimatorsRunnerTests: XCTestCase { func testUpdateWithAnimationsEnabled() { let animator = MockCameraAnimator() cameraAnimatorsRunner.add(animator) - cameraAnimatorsRunner.cameraAnimatorDidStartRunning(animator) + animator.$onCameraAnimatorStatusChanged.send(.started) cameraAnimatorsRunner.isEnabled = true cameraAnimatorsRunner.update() @@ -50,7 +50,7 @@ final class CameraAnimatorsRunnerTests: XCTestCase { func testUpdateWithAnimationsDisabled() { let animator = MockCameraAnimator() cameraAnimatorsRunner.add(animator) - cameraAnimatorsRunner.cameraAnimatorDidStartRunning(animator) + animator.$onCameraAnimatorStatusChanged.send(.started) cameraAnimatorsRunner.isEnabled = false @@ -226,46 +226,61 @@ final class CameraAnimatorsRunnerTests: XCTestCase { XCTAssertEqual(unspecifiedAnimatorThirdOwner.stopAnimationStub.invocations.count, 0) } - func testCameraAnimatorDelegate() { + func testCameraAnimatorStatus() { let animator1 = MockCameraAnimator() let animator2 = MockCameraAnimator() + cameraAnimatorsRunner.add(animator1) + cameraAnimatorsRunner.add(animator2) + // stopping before starting should have no effect - cameraAnimatorsRunner.cameraAnimatorDidStopRunning(animator1) + animator1.$onCameraAnimatorStatusChanged.send(.stopped(reason: .cancelled)) XCTAssertEqual(mapboxMap.beginAnimationStub.invocations.count, 0) XCTAssertEqual(mapboxMap.endAnimationStub.invocations.count, 0) // start once - cameraAnimatorsRunner.cameraAnimatorDidStartRunning(animator1) + animator1.$onCameraAnimatorStatusChanged.send(.started) XCTAssertEqual(mapboxMap.beginAnimationStub.invocations.count, 1) XCTAssertEqual(mapboxMap.endAnimationStub.invocations.count, 0) // start twice - cameraAnimatorsRunner.cameraAnimatorDidStartRunning(animator1) + animator1.$onCameraAnimatorStatusChanged.send(.started) XCTAssertEqual(mapboxMap.beginAnimationStub.invocations.count, 1) XCTAssertEqual(mapboxMap.endAnimationStub.invocations.count, 0) // start a second - cameraAnimatorsRunner.cameraAnimatorDidStartRunning(animator2) + animator2.$onCameraAnimatorStatusChanged.send(.started) XCTAssertEqual(mapboxMap.beginAnimationStub.invocations.count, 2) XCTAssertEqual(mapboxMap.endAnimationStub.invocations.count, 0) // end the first - cameraAnimatorsRunner.cameraAnimatorDidStopRunning(animator1) + animator1.$onCameraAnimatorStatusChanged.send(.stopped(reason: .finished)) XCTAssertEqual(mapboxMap.beginAnimationStub.invocations.count, 2) XCTAssertEqual(mapboxMap.endAnimationStub.invocations.count, 1) // end the first again - cameraAnimatorsRunner.cameraAnimatorDidStopRunning(animator1) + animator1.$onCameraAnimatorStatusChanged.send(.stopped(reason: .finished)) XCTAssertEqual(mapboxMap.beginAnimationStub.invocations.count, 2) XCTAssertEqual(mapboxMap.endAnimationStub.invocations.count, 1) // end the second - cameraAnimatorsRunner.cameraAnimatorDidStopRunning(animator2) + animator2.$onCameraAnimatorStatusChanged.send(.stopped(reason: .finished)) XCTAssertEqual(mapboxMap.beginAnimationStub.invocations.count, 2) XCTAssertEqual(mapboxMap.endAnimationStub.invocations.count, 2) } + func testCameraAnimatorPaused() { + var mockCameraAnimator = MockCameraAnimator() + cameraAnimatorsRunner.isEnabled = true + cameraAnimatorsRunner.add(mockCameraAnimator) + + mockCameraAnimator.$onCameraAnimatorStatusChanged.send(.started) + XCTAssertEqual(mapboxMap.beginAnimationStub.invocations.count, 1) + + mockCameraAnimator.$onCameraAnimatorStatusChanged.send(.paused) + XCTAssertEqual(mapboxMap.endAnimationStub.invocations.count, 1) + } + func testAddWithAnimationsEnabled() { cameraAnimatorsRunner.isEnabled = true @@ -273,7 +288,6 @@ final class CameraAnimatorsRunnerTests: XCTestCase { cameraAnimatorsRunner.add(animator) - XCTAssertIdentical(animator.delegate, cameraAnimatorsRunner) XCTAssertEqual(animator.stopAnimationStub.invocations.count, 0) } @@ -284,7 +298,39 @@ final class CameraAnimatorsRunnerTests: XCTestCase { cameraAnimatorsRunner.add(animator) - XCTAssertIdentical(animator.delegate, cameraAnimatorsRunner) XCTAssertEqual(animator.stopAnimationStub.invocations.count, 1) } + + func testCameraAnimatorStatusObserver() { + let mockCameraAnimator = MockCameraAnimator() + cameraAnimatorsRunner.isEnabled = true + cameraAnimatorsRunner.add(mockCameraAnimator) + + let observedExpectation = self.expectation(description: "animator status observer should be notified") + observedExpectation.expectedFulfillmentCount = 2 + observedExpectation.assertForOverFulfill = true + let observer1 = CameraAnimatorStatusObserver(owners: []) { animator in + XCTAssertIdentical(mockCameraAnimator, animator) + observedExpectation.fulfill() + } onStopped: { animator, isCancelled in + XCTAssertIdentical(mockCameraAnimator, animator) + XCTAssertFalse(isCancelled) + observedExpectation.fulfill() + } + cameraAnimatorsRunner.add(cameraAnimatorStatusObserver: observer1) + + let notObservedExpectation = self.expectation(description: "animator status observer should not be notified about animator of different owner") + notObservedExpectation.isInverted = true + let observer2 = CameraAnimatorStatusObserver(owners: [.cameraAnimationsManager]) { _ in + notObservedExpectation.fulfill() + } onStopped: { _, _ in + notObservedExpectation.fulfill() + } + cameraAnimatorsRunner.add(cameraAnimatorStatusObserver: observer2) + + mockCameraAnimator.$onCameraAnimatorStatusChanged.send(.started) + mockCameraAnimator.$onCameraAnimatorStatusChanged.send(.stopped(reason: .finished)) + + waitForExpectations(timeout: 0.3) + } } diff --git a/Tests/MapboxMapsTests/Camera/FlyToCameraAnimatorTests.swift b/Tests/MapboxMapsTests/Camera/FlyToCameraAnimatorTests.swift index 7be43ada0fe6..bcb36a1cd564 100644 --- a/Tests/MapboxMapsTests/Camera/FlyToCameraAnimatorTests.swift +++ b/Tests/MapboxMapsTests/Camera/FlyToCameraAnimatorTests.swift @@ -32,8 +32,9 @@ final class FlyToCameraAnimatorTests: XCTestCase { var mainQueue: MockMainQueue! var dateProvider: MockDateProvider! var flyToCameraAnimator: FlyToCameraAnimator! - // swiftlint:disable:next weak_delegate - var delegate: MockCameraAnimatorDelegate! + + var recordedCameraAnimatorStatus: [CameraAnimatorStatus] = [] + var cancelables: Set = [] override func setUp() { super.setUp() @@ -52,17 +53,18 @@ final class FlyToCameraAnimatorTests: XCTestCase { mapboxMap: mapboxMap, mainQueue: mainQueue, dateProvider: dateProvider) - delegate = MockCameraAnimatorDelegate() - flyToCameraAnimator.delegate = delegate + flyToCameraAnimator.onCameraAnimatorStatusChanged.observe { [unowned self] status in + self.recordedCameraAnimatorStatus.append(status) + }.store(in: &cancelables) } override func tearDown() { - delegate = nil flyToCameraAnimator = nil dateProvider = nil mainQueue = nil mapboxMap = nil owner = nil + recordedCameraAnimatorStatus = [] super.tearDown() } @@ -72,29 +74,25 @@ final class FlyToCameraAnimatorTests: XCTestCase { XCTAssertEqual(flyToCameraAnimator.state, .inactive) } - func testStartAnimationChangesStateToActiveAndInformsDelegate() { + func testStartAnimationChangesStateToActiveAndChangeAnimatorStatus() { flyToCameraAnimator.startAnimation() XCTAssertEqual(flyToCameraAnimator.state, .active) - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.cameraAnimatorDidStartRunningStub.invocations.first?.parameters === flyToCameraAnimator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } func testStartAnimationMoreThanOnceHasNoEffect() { flyToCameraAnimator.startAnimation() - delegate.cameraAnimatorDidStartRunningStub.reset() - flyToCameraAnimator.startAnimation() - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 0) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } func testStartAnimationAfterCompletionHasNoEffect() { flyToCameraAnimator.stopAnimation() - flyToCameraAnimator.startAnimation() - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testAnimationCompletion() { @@ -107,8 +105,6 @@ final class FlyToCameraAnimatorTests: XCTestCase { XCTAssertEqual(flyToCameraAnimator.state, .inactive) XCTAssertEqual(completion.invocations.map(\.parameters), [.end]) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.cameraAnimatorDidStartRunningStub.invocations.first?.parameters === flyToCameraAnimator) } func testStopAnimation() { @@ -120,8 +116,7 @@ final class FlyToCameraAnimatorTests: XCTestCase { XCTAssertEqual(completion.invocations.map(\.parameters), [.current]) XCTAssertEqual(flyToCameraAnimator.state, .inactive) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.cameraAnimatorDidStartRunningStub.invocations.first?.parameters === flyToCameraAnimator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .stopped(reason: .cancelled)]) } func testStopAnimationThatHasNotStarted() { @@ -132,7 +127,7 @@ final class FlyToCameraAnimatorTests: XCTestCase { XCTAssertEqual(completion.invocations.map(\.parameters), [.current]) XCTAssertEqual(flyToCameraAnimator.state, .inactive) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testStopAnimationThatHasAlreadyCompleted() { @@ -145,7 +140,7 @@ final class FlyToCameraAnimatorTests: XCTestCase { XCTAssertEqual(completion.invocations.count, 0) XCTAssertEqual(flyToCameraAnimator.state, .inactive) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testAddCompletionToRunningAnimator() { @@ -206,4 +201,47 @@ final class FlyToCameraAnimatorTests: XCTestCase { XCTAssertFalse(mapboxMap.setCameraStub.invocations.isEmpty) } + + func testOnStarted() { + var isStarted = false + let token = flyToCameraAnimator.onStarted.observe { + isStarted = true + } + + flyToCameraAnimator.startAnimation() + XCTAssertTrue(isStarted) + } + + func testOnFinished() { + var isFinished = false + var cancelables = Set() + flyToCameraAnimator.onFinished.observe { + isFinished = true + }.store(in: &cancelables) + flyToCameraAnimator.onCancelled.observe { + XCTFail("animator is not cancelled") + }.store(in: &cancelables) + + flyToCameraAnimator.startAnimation() + dateProvider.nowStub.defaultReturnValue = Date(timeIntervalSinceReferenceDate: 20) + flyToCameraAnimator.update() + + XCTAssertTrue(isFinished) + } + + func testOnCancelled() { + var isCancelled = false + var cancelables = Set() + flyToCameraAnimator.onFinished.observe { + XCTFail("animator is not finished") + }.store(in: &cancelables) + flyToCameraAnimator.onCancelled.observe { + isCancelled = true + }.store(in: &cancelables) + + flyToCameraAnimator.startAnimation() + flyToCameraAnimator.stopAnimation() + + XCTAssertTrue(isCancelled) + } } diff --git a/Tests/MapboxMapsTests/Camera/GestureDecelerationCameraAnimatorTests.swift b/Tests/MapboxMapsTests/Camera/GestureDecelerationCameraAnimatorTests.swift index 74ffd9159840..5d5656dcab6b 100644 --- a/Tests/MapboxMapsTests/Camera/GestureDecelerationCameraAnimatorTests.swift +++ b/Tests/MapboxMapsTests/Camera/GestureDecelerationCameraAnimatorTests.swift @@ -11,9 +11,9 @@ final class GestureDecelerationCameraAnimatorTests: XCTestCase { var mainQueue: MockMainQueue! var dateProvider: MockDateProvider! var animator: GestureDecelerationCameraAnimator! - // swiftlint:disable:next weak_delegate - var delegate: MockCameraAnimatorDelegate! var completion: Stub! + var recordedCameraAnimatorStatus: [CameraAnimatorStatus] = [] + var cancelables: Set = [] override func setUp() { super.setUp() @@ -32,15 +32,15 @@ final class GestureDecelerationCameraAnimatorTests: XCTestCase { locationChangeHandler: locationChangeHandler.call(withFromLocation:toLocation:), mainQueue: mainQueue, dateProvider: dateProvider) - delegate = MockCameraAnimatorDelegate() - animator.delegate = delegate completion = Stub() animator.addCompletion(completion.call(with:)) + animator.onCameraAnimatorStatusChanged.observe { [unowned self] status in + recordedCameraAnimatorStatus.append(status) + }.store(in: &cancelables) } override func tearDown() { completion = nil - delegate = nil animator = nil dateProvider = nil mainQueue = nil @@ -49,6 +49,7 @@ final class GestureDecelerationCameraAnimatorTests: XCTestCase { decelerationFactor = nil velocity = nil location = nil + recordedCameraAnimatorStatus = [] super.tearDown() } @@ -60,25 +61,19 @@ final class GestureDecelerationCameraAnimatorTests: XCTestCase { animator.startAnimation() XCTAssertEqual(animator.state, .active) - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.cameraAnimatorDidStartRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } func testStartAnimationWhenAlreadyRunning() { animator.startAnimation() - delegate.cameraAnimatorDidStartRunningStub.reset() - animator.startAnimation() - - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 0) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } func testStartAnimationWhenAlreadyComplete() { animator.stopAnimation() - animator.startAnimation() - - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testStopAnimation() { @@ -88,8 +83,7 @@ final class GestureDecelerationCameraAnimatorTests: XCTestCase { XCTAssertEqual(animator.state, .inactive) XCTAssertEqual(completion.invocations.map(\.parameters), [.current]) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.cameraAnimatorDidStopRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .stopped(reason: .cancelled)]) } func testStopAnimationWithoutStarting() { @@ -97,7 +91,7 @@ final class GestureDecelerationCameraAnimatorTests: XCTestCase { XCTAssertEqual(animator.state, .inactive) XCTAssertEqual(completion.invocations.map(\.parameters), [.current]) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testStopAnimationWhenAlreadyComplete() { @@ -108,7 +102,7 @@ final class GestureDecelerationCameraAnimatorTests: XCTestCase { XCTAssertEqual(animator.state, .inactive) XCTAssertEqual(completion.invocations.count, 0) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 0) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testUpdate() { @@ -149,8 +143,7 @@ final class GestureDecelerationCameraAnimatorTests: XCTestCase { // to be sufficiently low (< 35 in both x and y) to end the animation. XCTAssertEqual(animator.state, .inactive) XCTAssertEqual(completion.invocations.map(\.parameters), [.end]) - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertTrue(delegate.cameraAnimatorDidStopRunningStub.invocations.first?.parameters === animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .stopped(reason: .finished)]) } func testAddCompletionToRunningAnimator() { diff --git a/Tests/MapboxMapsTests/Camera/Mocks/MockBasicCameraAnimator.swift b/Tests/MapboxMapsTests/Camera/Mocks/MockBasicCameraAnimator.swift index 2dba2da90410..701b426782a6 100644 --- a/Tests/MapboxMapsTests/Camera/Mocks/MockBasicCameraAnimator.swift +++ b/Tests/MapboxMapsTests/Camera/Mocks/MockBasicCameraAnimator.swift @@ -2,7 +2,8 @@ import UIKit @testable import MapboxMaps final class MockBasicCameraAnimator: BasicCameraAnimatorProtocol { - @Stubbed var delegate: BasicCameraAnimatorDelegate? + @TestSignal var onCameraAnimatorStatusChanged: MapboxMaps.Signal + @Stubbed var owner: AnimationOwner = .unspecified @Stubbed var animationType: AnimationType = .unspecified @Stubbed var transition: CameraTransition? diff --git a/Tests/MapboxMapsTests/Camera/Mocks/MockBasicCameraAnimatorDelegate.swift b/Tests/MapboxMapsTests/Camera/Mocks/MockBasicCameraAnimatorDelegate.swift deleted file mode 100644 index 77a7d847f694..000000000000 --- a/Tests/MapboxMapsTests/Camera/Mocks/MockBasicCameraAnimatorDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -@testable import MapboxMaps - -final class MockBasicCameraAnimatorDelegate: BasicCameraAnimatorDelegate { - let basicCameraAnimatorDidStartRunningStub = Stub() - func basicCameraAnimatorDidStartRunning(_ animator: BasicCameraAnimatorProtocol) { - basicCameraAnimatorDidStartRunningStub.call(with: animator) - } - - let basicCameraAnimatorDidStopRunningStub = Stub() - func basicCameraAnimatorDidStopRunning(_ animator: BasicCameraAnimatorProtocol) { - basicCameraAnimatorDidStopRunningStub.call(with: animator) - } -} diff --git a/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimationsManager.swift b/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimationsManager.swift index b1594ef1d587..6f52e1523ad6 100644 --- a/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimationsManager.swift +++ b/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimationsManager.swift @@ -177,4 +177,9 @@ final class MockCameraAnimationsManager: CameraAnimationsManagerProtocol { curve: curve, owner: owner)) } + + let addCameraAnimatorStatusObserverStub = Stub(defaultReturnValue: .empty) + func add(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) -> AnyCancelable { + addCameraAnimatorStatusObserverStub.call(with: observer) + } } diff --git a/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimatorDelegate.swift b/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimatorDelegate.swift deleted file mode 100644 index f42a1bf6a73f..000000000000 --- a/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimatorDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -@testable import MapboxMaps - -final class MockCameraAnimatorDelegate: CameraAnimatorDelegate { - let cameraAnimatorDidStartRunningStub = Stub() - func cameraAnimatorDidStartRunning(_ cameraAnimator: CameraAnimatorProtocol) { - cameraAnimatorDidStartRunningStub.call(with: cameraAnimator) - } - - let cameraAnimatorDidStopRunningStub = Stub() - func cameraAnimatorDidStopRunning(_ cameraAnimator: CameraAnimatorProtocol) { - cameraAnimatorDidStopRunningStub.call(with: cameraAnimator) - } -} diff --git a/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimatorsRunner.swift b/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimatorsRunner.swift index 87c38a0a674f..e3826a7846d5 100644 --- a/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimatorsRunner.swift +++ b/Tests/MapboxMapsTests/Camera/Mocks/MockCameraAnimatorsRunner.swift @@ -29,4 +29,14 @@ final class MockCameraAnimatorsRunner: CameraAnimatorsRunnerProtocol { func add(_ animator: CameraAnimatorProtocol) { addStub.call(with: animator) } + + let addCameraAnimatorStatusObserverStub = Stub() + func add(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) { + addCameraAnimatorStatusObserverStub.call(with: observer) + } + + let removeCameraAnimatorStatusObserverStub = Stub() + func remove(cameraAnimatorStatusObserver observer: CameraAnimatorStatusObserver) { + removeCameraAnimatorStatusObserverStub.call(with: observer) + } } diff --git a/Tests/MapboxMapsTests/Camera/Mocks/MockSimpleCameraAnimator.swift b/Tests/MapboxMapsTests/Camera/Mocks/MockSimpleCameraAnimator.swift index 27e24c72e2fc..23b35e29366b 100644 --- a/Tests/MapboxMapsTests/Camera/Mocks/MockSimpleCameraAnimator.swift +++ b/Tests/MapboxMapsTests/Camera/Mocks/MockSimpleCameraAnimator.swift @@ -8,10 +8,10 @@ final class MockSimpleCameraAnimator: SimpleCameraAnimatorProtocol { @Stubbed var animationType: AnimationType = .unspecified - @Stubbed var delegate: CameraAnimatorDelegate? - @Stubbed var to: CameraOptions = .random() + @TestSignal var onCameraAnimatorStatusChanged: Signal + let cancelStub = Stub() func cancel() { cancelStub.call() diff --git a/Tests/MapboxMapsTests/Camera/SimpleCameraAnimatorTests.swift b/Tests/MapboxMapsTests/Camera/SimpleCameraAnimatorTests.swift index 1672e7147787..c3d3d2d28b0e 100644 --- a/Tests/MapboxMapsTests/Camera/SimpleCameraAnimatorTests.swift +++ b/Tests/MapboxMapsTests/Camera/SimpleCameraAnimatorTests.swift @@ -13,7 +13,8 @@ final class SimpleCameraAnimatorTests: XCTestCase { var cameraOptionsInterpolator: MockCameraOptionsInterpolator! var dateProvider: MockDateProvider! var animator: SimpleCameraAnimator! - var delegate: MockCameraAnimatorDelegate! + var recordedCameraAnimatorStatus: [CameraAnimatorStatus] = [] + var cancelables: Set = [] override func setUp() { super.setUp() @@ -36,12 +37,12 @@ final class SimpleCameraAnimatorTests: XCTestCase { mainQueue: mainQueue, cameraOptionsInterpolator: cameraOptionsInterpolator, dateProvider: dateProvider) - delegate = MockCameraAnimatorDelegate() - animator.delegate = delegate + animator.onCameraAnimatorStatusChanged.observe { [unowned self] status in + self.recordedCameraAnimatorStatus.append(status) + }.store(in: &cancelables) } override func tearDown() { - delegate = nil animator = nil dateProvider = nil cameraOptionsInterpolator = nil @@ -52,6 +53,7 @@ final class SimpleCameraAnimatorTests: XCTestCase { duration = nil to = nil from = nil + recordedCameraAnimatorStatus = [] super.tearDown() } @@ -66,7 +68,6 @@ final class SimpleCameraAnimatorTests: XCTestCase { mainQueue: mainQueue, cameraOptionsInterpolator: cameraOptionsInterpolator, dateProvider: dateProvider) - animator.delegate = delegate } func testOwner() { @@ -85,13 +86,7 @@ final class SimpleCameraAnimatorTests: XCTestCase { animator.startAnimation() XCTAssertEqual(animator.state, .active) - } - - func testStartAnimationCallsDelegate() { - animator.startAnimation() - - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertIdentical(delegate.cameraAnimatorDidStartRunningStub.invocations.first?.parameters, animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } func testStartAnimationAgainWhileItIsRunningDoesNotChangeTheUpdateFraction() { @@ -113,23 +108,20 @@ final class SimpleCameraAnimatorTests: XCTestCase { func testStartAnimationWhileItIsCompleteDoesNotChangeTheState() { animator.startAnimation() animator.cancel() + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .stopped(reason: .cancelled)]) + recordedCameraAnimatorStatus = [] animator.startAnimation() XCTAssertEqual(animator.state, .inactive) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testStartAnimationAfterDelaySetsStateToActive() { animator.startAnimation(afterDelay: .random(in: 0...10)) XCTAssertEqual(animator.state, .active) - } - - func testStartAnimationAfterDelayCallsDelegate() { - animator.startAnimation(afterDelay: .random(in: 0...10)) - - XCTAssertEqual(delegate.cameraAnimatorDidStartRunningStub.invocations.count, 1) - XCTAssertIdentical(delegate.cameraAnimatorDidStartRunningStub.invocations.first?.parameters, animator) + XCTAssertEqual(recordedCameraAnimatorStatus, [.started]) } func testStartAnimationAfterDelayAgainWhileItIsRunningDoesNotChangeTheUpdateFraction() { @@ -151,10 +143,13 @@ final class SimpleCameraAnimatorTests: XCTestCase { func testStartAnimationAfterDelayWhileItIsCompleteDoesNotChangeTheState() { animator.startAnimation() animator.cancel() + XCTAssertEqual(recordedCameraAnimatorStatus, [.started, .stopped(reason: .cancelled)]) + recordedCameraAnimatorStatus = [] animator.startAnimation(afterDelay: .random(in: 1...10)) XCTAssertEqual(animator.state, .inactive) + XCTAssertTrue(recordedCameraAnimatorStatus.isEmpty) } func testUpdateAnimationWhenItHasNotYetStartedDoesNotSetCamera() { @@ -215,8 +210,6 @@ final class SimpleCameraAnimatorTests: XCTestCase { animator.update() - XCTAssertEqual(delegate.cameraAnimatorDidStopRunningStub.invocations.count, 1) - XCTAssertIdentical(delegate.cameraAnimatorDidStopRunningStub.invocations.first?.parameters, animator) } func testUpdateAnimationWhenTimeElapsedIsDurationSetsStateToInactive() { diff --git a/Tests/MapboxMapsTests/Foundation/Mocks/MockCameraAnimator.swift b/Tests/MapboxMapsTests/Foundation/Mocks/MockCameraAnimator.swift index 2870df8871b3..f75bf6c320c8 100644 --- a/Tests/MapboxMapsTests/Foundation/Mocks/MockCameraAnimator.swift +++ b/Tests/MapboxMapsTests/Foundation/Mocks/MockCameraAnimator.swift @@ -2,6 +2,8 @@ import UIKit @testable import MapboxMaps final class MockCameraAnimator: NSObject, CameraAnimatorProtocol { + @TestSignal var onCameraAnimatorStatusChanged: Signal + let cancelStub = Stub() func cancel() { cancelStub.call() @@ -18,8 +20,6 @@ final class MockCameraAnimator: NSObject, CameraAnimatorProtocol { @Stubbed var animationType: AnimationType = .unspecified - @Stubbed var delegate: CameraAnimatorDelegate? - let addCompletionStub = Stub() func addCompletion(_ completion: @escaping AnimationCompletion) { addCompletionStub.call(with: completion)