From 167cc03f433655fa7f993be6083fbf6c114580b1 Mon Sep 17 00:00:00 2001 From: Andrew Hershberger Date: Tue, 28 Sep 2021 21:13:23 -0500 Subject: [PATCH] Lock out animations during any touch events (#712) * Removes CameraAnimationsManager.options * Transfers comments from internal protocols * Renames setCameraBounds(for:) to setCameraBounds(with:) * Exposes BasicCameraAnimator.isReversed setter --- .../AnimateImageLayerExample.swift | 3 +- .../RestrictCoordinateBoundsExample.swift | 9 +- CHANGELOG.md | 6 +- .../Camera/BasicCameraAnimator.swift | 13 +- .../Camera/CameraAnimationsManager.swift | 20 +- .../Foundation/CameraManagerProtocol.swift | 180 ---------------- .../Foundation/MapFeatureQueryable.swift | 54 +---- .../Foundation/MapProjectionDelegate.swift | 48 ++--- .../Foundation/MapTransformDelegate.swift | 31 --- .../Foundation/MapView+Supportable.swift | 36 ---- Sources/MapboxMaps/Foundation/MapView.swift | 7 +- .../MapViewDependencyProvider.swift | 56 ++--- Sources/MapboxMaps/Foundation/MapboxMap.swift | 200 +++++++++++++++++- .../AnimationLockoutGestureHandler.swift | 25 +++ .../DoubleTapToZoomInGestureHandler.swift | 12 +- .../DoubleTouchToZoomOutGestureHandler.swift | 11 +- .../GestureHandlers/GestureHandler.swift | 10 +- .../GestureHandlers/PanGestureHandler.swift | 13 +- .../GestureHandlers/PinchGestureHandler.swift | 14 +- .../GestureHandlers/PitchGestureHandler.swift | 12 +- .../QuickZoomGestureHandler.swift | 11 +- .../SingleTapGestureHandler.swift | 10 +- .../MapboxMaps/Gestures/GestureManager.swift | 5 +- .../AnyTouchGestureRecognizer.swift | 42 ++++ .../Location/LocationStyleProtocol.swift | 2 + .../Compass/MapboxCompassOrnamentView.swift | 2 +- .../Ornaments/OrnamentSupportableView.swift | 18 -- .../Ornaments/OrnamentsManager.swift | 33 ++- .../Camera/BasicCameraAnimatorTests.swift | 47 ++++ .../Camera/CameraAnimationsManagerTests.swift | 59 ++++++ .../Foundation/Camera/MockMapboxMap.swift | 12 ++ .../Camera/MockPropertyAnimator.swift | 14 +- .../Mocks/MockCameraAnimationsManager.swift | 2 + .../Foundation/MapboxMapTests.swift | 2 - .../Foundation/Mocks/MockCancelable.swift | 8 + .../Mocks/MockMapViewDependencyProvider.swift | 12 +- .../Projection/ProjectionTests.swift | 33 ++- .../AnimationLockoutGestureHandlerTests.swift | 56 +++++ .../PanGestureHandlerTests.swift | 14 +- .../PinchGestureHandlerTests.swift | 12 +- .../PitchGestureHandlerTests.swift | 7 +- .../QuickZoomGestureHandlerTests.swift | 7 +- .../SingleTapGestureHandlerTests.swift | 12 +- .../Gestures/GestureManagerTests.swift | 19 +- .../AnyTouchGestureRecognizerTests.swift | 46 ++++ .../Mocks/MockGestureRecognizer.swift | 26 +++ .../MigrationGuideIntegrationTests.swift | 15 +- .../CompassMapViewIntegrationTests.swift | 64 +----- .../MockInfoButtonOrnamentDelegate.swift | 6 + .../Ornaments/OrnamentManagerTests.swift | 99 ++++++--- .../OrnamentSupportableViewMock.swift | 17 -- 51 files changed, 798 insertions(+), 674 deletions(-) delete mode 100644 Sources/MapboxMaps/Foundation/CameraManagerProtocol.swift delete mode 100644 Sources/MapboxMaps/Foundation/MapTransformDelegate.swift delete mode 100644 Sources/MapboxMaps/Foundation/MapView+Supportable.swift create mode 100644 Sources/MapboxMaps/Gestures/GestureHandlers/AnimationLockoutGestureHandler.swift create mode 100644 Sources/MapboxMaps/Gestures/GestureRecognizers/AnyTouchGestureRecognizer.swift delete mode 100644 Sources/MapboxMaps/Ornaments/OrnamentSupportableView.swift create mode 100644 Tests/MapboxMapsTests/Foundation/Camera/CameraAnimationsManagerTests.swift create mode 100644 Tests/MapboxMapsTests/Foundation/Mocks/MockCancelable.swift create mode 100644 Tests/MapboxMapsTests/Gestures/GestureHandlers/AnimationLockoutGestureHandlerTests.swift create mode 100644 Tests/MapboxMapsTests/Gestures/GestureRecognizers/AnyTouchGestureRecognizerTests.swift create mode 100644 Tests/MapboxMapsTests/Ornaments/Mocks/MockInfoButtonOrnamentDelegate.swift delete mode 100644 Tests/MapboxMapsTests/Ornaments/OrnamentSupportableViewMock.swift diff --git a/Apps/Examples/Examples/All Examples/AnimateImageLayerExample.swift b/Apps/Examples/Examples/All Examples/AnimateImageLayerExample.swift index 767b17b8f29..3aedd5edd6b 100644 --- a/Apps/Examples/Examples/All Examples/AnimateImageLayerExample.swift +++ b/Apps/Examples/Examples/All Examples/AnimateImageLayerExample.swift @@ -23,8 +23,7 @@ class AnimateImageLayerExample: UIViewController, ExampleProtocol { mapView.tintColor = .lightGray // Set the map's `CameraBoundsOptions` to limit the map's zoom level. - mapView.camera.options.maxZoom = 5.99 - mapView.camera.options.minZoom = 4 + try? mapView.mapboxMap.setCameraBounds(with: CameraBoundsOptions(maxZoom: 5.99, minZoom: 4)) view.addSubview(mapView) diff --git a/Apps/Examples/Examples/All Examples/RestrictCoordinateBoundsExample.swift b/Apps/Examples/Examples/All Examples/RestrictCoordinateBoundsExample.swift index d5b3ba37cb5..931ac114371 100644 --- a/Apps/Examples/Examples/All Examples/RestrictCoordinateBoundsExample.swift +++ b/Apps/Examples/Examples/All Examples/RestrictCoordinateBoundsExample.swift @@ -3,10 +3,9 @@ import MapboxMaps import MapboxCoreMaps @objc(RestrictCoordinateBoundsExample) +final class RestrictCoordinateBoundsExample: UIViewController, ExampleProtocol { -public class RestrictCoordinateBoundsExample: UIViewController, ExampleProtocol { - - override public func viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() let mapView = MapView(frame: view.bounds) @@ -17,7 +16,7 @@ public class RestrictCoordinateBoundsExample: UIViewController, ExampleProtocol northeast: CLLocationCoordinate2D(latitude: 66.61, longitude: -13.47)) // Restrict the camera to `bounds`. - mapView.camera.options = CameraBoundsOptions(bounds: bounds) + try? mapView.mapboxMap.setCameraBounds(with: CameraBoundsOptions(bounds: bounds)) // Center the camera on the bounds let camera = mapView.mapboxMap.camera(for: bounds, padding: .zero, bearing: 0, pitch: 0) @@ -26,7 +25,7 @@ public class RestrictCoordinateBoundsExample: UIViewController, ExampleProtocol mapView.mapboxMap.setCamera(to: camera) } - override public func viewDidAppear(_ animated: Bool) { + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // The below line is used for internal testing purposes only. finish() diff --git a/CHANGELOG.md b/CHANGELOG.md index 1955364ebe7..adf9a9930e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Mapbox welcomes participation and contributions from everyone. * `public func layerProperty(for layerId: String, property: String) -> Any` has been renamed to `public func layerPropertyValue(for layerId: String, property: String) -> Any` to avoid ambiguity. ([#708](https://github.com/mapbox/mapbox-maps-ios/pull/708)) * `MapboxCommon.Geometry` extension methods are now marked as internal. ([#683](https://github.com/mapbox/mapbox-maps-ios/pull/683)) * `TileRegionLoadOptions` init now takes a `Geometry` instead of a `MapboxCommon.Geometry`. ([#711](https://github.com/mapbox/mapbox-maps-ios/pull/711)) +* `CameraAnimationsManager.options` has been removed. Use `MapboxMap.cameraBounds` and `MapboxMap.setCameraBounds(with:)` instead. ([#712](https://github.com/mapbox/mapbox-maps-ios/pull/712)) +* `MapboxMap.setCameraBounds(for:)` has been renamed to `.setCameraBounds(with:)` ([#712](https://github.com/mapbox/mapbox-maps-ios/pull/712)) ### Features ✨ and improvements 🏁 @@ -27,6 +29,9 @@ Mapbox welcomes participation and contributions from everyone. * Adds `FeatureExtensionValue.features: [Feature]?` that works with Turf. ([#717](https://github.com/mapbox/mapbox-maps-ios/pull/717)) * APIs that accept Turf `Feature` now allow `Feature.identifier` and `.properties` to be `nil`. ([#717](https://github.com/mapbox/mapbox-maps-ios/pull/717)) * APIs that accept Turf `Feature` now ignore `Feature.properties` instead of crashing if it cannot be converted to `[String: NSObject]`. ([#717](https://github.com/mapbox/mapbox-maps-ios/pull/717)) +* Any touch event in the map now immedately disables camera animation. Temporarily disable user interaction on the `MapView` to disable this behavior as needed. ([#712](https://github.com/mapbox/mapbox-maps-ios/pull/712)) +* `BasicCameraAnimator` no longer updates the camera a final time after being stopped or canceled prior to running to completion. ([#712](https://github.com/mapbox/mapbox-maps-ios/pull/712)) +* `BasicCameraAnimator.isReversed` is now settable. ([#712](https://github.com/mapbox/mapbox-maps-ios/pull/712)) ## 10.0.0-rc.9 - Sept 22, 2021 @@ -59,7 +64,6 @@ Mapbox welcomes participation and contributions from everyone. * `GestureManagerDelegate.gestureBegan(for:)` has been renamed to `GestureManagerDelegate.gestureManager(_:didBegin:)`. ([#697](https://github.com/mapbox/mapbox-maps-ios/pull/697)) * Added the public delegate methods `GestureManagerDelegate.gestureManager(_:didEnd:willAnimate:)` and `GestureManagerDelegate.gestureManager(_:didEndAnimatingFor:)`. ([#697](https://github.com/mapbox/mapbox-maps-ios/pull/697)) - ### Features ✨ and improvements 🏁 * Allow users to set the map's `MapDebugOptions`. ([#648](https://github.com/mapbox/mapbox-maps-ios/pull/648)) diff --git a/Sources/MapboxMaps/Foundation/Camera/BasicCameraAnimator.swift b/Sources/MapboxMaps/Foundation/Camera/BasicCameraAnimator.swift index d79b38a1295..1720aaba43e 100644 --- a/Sources/MapboxMaps/Foundation/Camera/BasicCameraAnimator.swift +++ b/Sources/MapboxMaps/Foundation/Camera/BasicCameraAnimator.swift @@ -50,7 +50,10 @@ public class BasicCameraAnimator: NSObject, CameraAnimator, CameraAnimatorInterf public var isRunning: Bool { propertyAnimator.isRunning } /// Boolean that represents if the animation is running normally or in reverse. - public var isReversed: Bool { propertyAnimator.isReversed } + public var isReversed: Bool { + get { propertyAnimator.isReversed } + set { propertyAnimator.isReversed = newValue } + } /// A Boolean value that indicates whether a completed animation remains in the active state. public var pausesOnCompletion: Bool { @@ -226,8 +229,12 @@ public class BasicCameraAnimator: NSObject, CameraAnimator, CameraAnimatorInterf propertyAnimator.addCompletion { [weak self] (animatingPosition) in guard let self = self else { return } self.internalState = .final - let finalCamera = self.cameraOptions(with: transition, cameraViewCameraOptions: self.cameraView.cameraOptions) - self.mapboxMap.setCamera(to: finalCamera) + // if the animation was stopped/canceled before finishing, + // do not update the camera again. + if animatingPosition != .current { + let finalCamera = self.cameraOptions(with: transition, cameraViewCameraOptions: self.cameraView.cameraOptions) + self.mapboxMap.setCamera(to: finalCamera) + } for completion in self.completions { completion(animatingPosition) } diff --git a/Sources/MapboxMaps/Foundation/Camera/CameraAnimationsManager.swift b/Sources/MapboxMaps/Foundation/Camera/CameraAnimationsManager.swift index afb40eabbb4..d7a1324a24b 100644 --- a/Sources/MapboxMaps/Foundation/Camera/CameraAnimationsManager.swift +++ b/Sources/MapboxMaps/Foundation/Camera/CameraAnimationsManager.swift @@ -29,18 +29,13 @@ internal protocol CameraAnimationsManagerProtocol: AnyObject { completion: @escaping () -> Void) func cancelAnimations() + + var animationsEnabled: Bool { get set } } /// An object that manages a camera's view lifecycle. public class CameraAnimationsManager: CameraAnimationsManagerProtocol { - /// Used to set up camera specific configuration - public var options: CameraBoundsOptions { - didSet { - try? mapboxMap.setCameraBounds(for: options) - } - } - /// List of animators currently alive public var cameraAnimators: [CameraAnimator] { return cameraAnimatorsSet.allObjects @@ -55,17 +50,22 @@ public class CameraAnimationsManager: CameraAnimationsManagerProtocol { /// May want to convert to an enum. fileprivate let northBearing: CGFloat = 0 + internal var animationsEnabled: Bool = true + private let cameraViewContainerView: UIView - private let mapboxMap: MapboxMap + private let mapboxMap: MapboxMapProtocol - internal init(cameraViewContainerView: UIView, mapboxMap: MapboxMap) { + internal init(cameraViewContainerView: UIView, mapboxMap: MapboxMapProtocol) { self.cameraViewContainerView = cameraViewContainerView self.mapboxMap = mapboxMap - self.options = CameraBoundsOptions(cameraBounds: mapboxMap.cameraBounds) } internal func update() { + guard animationsEnabled else { + cancelAnimations() + return + } for animator in cameraAnimatorsSet.allObjects { animator.update() } diff --git a/Sources/MapboxMaps/Foundation/CameraManagerProtocol.swift b/Sources/MapboxMaps/Foundation/CameraManagerProtocol.swift deleted file mode 100644 index f0193ac3698..00000000000 --- a/Sources/MapboxMaps/Foundation/CameraManagerProtocol.swift +++ /dev/null @@ -1,180 +0,0 @@ -internal protocol CameraManagerProtocol { - - /// Calculates a `CameraOptions` to fit a `CoordinateBounds` - /// - /// - Parameters: - /// - coordinateBounds: The coordinate bounds that will be displayed within the viewport. - /// - padding: The new padding to be used by the camera. - /// - bearing: The new bearing to be used by the camera. - /// - pitch: The new pitch to be used by the camera. - /// - Returns: A `CameraOptions` that fits the provided constraints - func camera(for coordinateBounds: CoordinateBounds, - padding: UIEdgeInsets, - bearing: Double?, - pitch: Double?) -> CameraOptions - - /// Calculates a `CameraOptions` to fit a list of coordinates. - /// - /// - Parameters: - /// - coordinates: Array of coordinates that should fit within the new viewport. - /// - padding: The new padding to be used by the camera. - /// - bearing: The new bearing to be used by the camera. - /// - pitch: The new pitch to be used by the camera. - /// - Returns: A `CameraOptions` that fits the provided constraints - func camera(for coordinates: [CLLocationCoordinate2D], - padding: UIEdgeInsets, - bearing: Double?, - pitch: Double?) -> CameraOptions - - /// Calculates a `CameraOptions` to fit a list of coordinates into a sub-rect of the map. - /// - /// Adjusts the zoom of `camera` to fit `coordinates` into `rect`. - /// - /// Returns the provided camera with zoom adjusted to fit coordinates into - /// `rect`, so that the coordinates on the left, top and right of the effective - /// camera center at the principal point of the projection (defined by padding) - /// fit into the rect. - /// - /// - Note: - /// This method may fail if the principal point of the projection is not - /// inside `rect` or if there is insufficient screen space, defined by - /// principal point and rect, to fit the geometry. - /// - /// - Parameters: - /// - coordinates: The coordinates to frame within `rect`. - /// - camera: The camera for which the zoom should be adjusted to fit `coordinates`. `camera.center` must be non-nil. - /// - rect: The rectangle inside of the map that should be used to frame `coordinates`. - /// - Returns: A `CameraOptions` that fits the provided constraints, or `cameraOptions` if an error occurs. - func camera(for coordinates: [CLLocationCoordinate2D], - camera: CameraOptions, - rect: CGRect) -> CameraOptions - - /// Calculates a `CameraOptions` to fit a geometry - /// - /// - Parameters: - /// - geometry: The geoemtry that will be displayed within the viewport. - /// - padding: The new padding to be used by the camera. - /// - bearing: The new bearing to be used by the camera. - /// - pitch: The new pitch to be used by the camera. - /// - Returns: A `CameraOptions` that fits the provided constraints - func camera(for geometry: Turf.Geometry, - padding: UIEdgeInsets, - bearing: CGFloat?, - pitch: CGFloat?) -> CameraOptions - - // MARK: - CameraOptions to CoordinateBounds - - /// Returns the coordinate bounds corresponding to a given `CameraOptions` - /// - /// - Parameter camera: The camera for which the coordinate bounds will be returned. - /// - Returns: `CoordinateBounds` for the given `CameraOptions` - func coordinateBounds(for camera: CameraOptions) -> CoordinateBounds - - /// Returns the coordinate bounds and zoom for a given `CameraOptions`. - /// - /// - Parameter camera: The camera for which the `CoordinateBoundsZoom` will be returned. - /// - Returns: `CoordinateBoundsZoom` for the given `CameraOptions` - func coordinateBoundsZoom(for camera: CameraOptions) -> CoordinateBoundsZoom - - /// Returns the unwrapped coordinate bounds and zoom for a given `CameraOptions`. - /// - /// This function is particularly useful, if the camera shows the antimeridian. - /// - /// - Parameter camera: The camera for which the `CoordinateBoundsZoom` will - /// be returned. - /// - Returns: `CoordinateBoundsZoom` for the given `CameraOptions` - func coordinateBoundsZoomUnwrapped(for camera: CameraOptions) -> CoordinateBoundsZoom - - // MARK: - Screen coordinate conversion - - /// Converts a map coordinate to a `CGPoint`, relative to the `MapView`. - /// - Parameter coordinate: The coordinate to convert. - /// - Returns: A `CGPoint` relative to the `UIView`. - func point(for coordinate: CLLocationCoordinate2D) -> CGPoint - - /// Converts a point in the mapView's coordinate system to a geographic coordinate. - /// The point must exist in the coordinate space of the `MapView` - /// - /// - Parameter point: The point to convert. Must exist in the coordinate space - /// of the `MapView` - /// - Returns: A `CLLocationCoordinate` that represents the geographic location - /// of the point. - func coordinate(for point: CGPoint) -> CLLocationCoordinate2D - - /// Converts map coordinates to an array of `CGPoint`, relative to the `MapView`. - /// - /// - Parameter coordinates: The coordinate to convert. - /// - Returns: An array of `CGPoint` relative to the `UIView`. - func points(for coordinates: [CLLocationCoordinate2D]) -> [CGPoint] - - /// Converts points in the mapView's coordinate system to geographic coordinates. - /// The points must exist in the coordinate space of the `MapView`. - /// - /// - Parameter point: The point to convert. Must exist in the coordinate space - /// of the `MapView` - /// - Returns: A `CLLocationCoordinate` that represents the geographic location - /// of the point. - func coordinates(for points: [CGPoint]) -> [CLLocationCoordinate2D] - - // MARK: - Camera getters/setters - - /// Changes the map view by any combination of center, zoom, bearing, and pitch, - /// without an animated transition. The map will retain its current values - /// for any details not passed via the camera options argument. It is not - /// guaranteed that the provided `CameraOptions` will be set, the map may apply - /// constraints resulting in a different `CameraState`. - /// - /// - Important: - /// This method does not cancel existing animations. Call - /// `CameraAnimationsManager.cancelAnimations()`to cancel existing animations. - /// - /// - Parameter cameraOptions: New camera options - func setCamera(to cameraOptions: CameraOptions) - - /// Returns the current camera state - var cameraState: CameraState { get } - - /// Sets/get the map view with the free camera options. - /// - /// FreeCameraOptions provides more direct access to the underlying camera entity. - /// For backwards compatibility the state set using this API must be representable - /// with `CameraOptions` as well. Parameters are clamped to a valid range or - /// discarded as invalid if the conversion to the pitch and bearing presentation - /// is ambiguous. For example orientation can be invalid if it leads to the - /// camera being upside down or the quaternion has zero length. - /// - /// - Parameter freeCameraOptions: The free camera options to set. - var freeCameraOptions: FreeCameraOptions { get set } - - /// Returns the bounds of the map. - var cameraBounds: CameraBounds { get } - - /// Sets the camera bounds using a `CameraBoundsOptions` - /// - Parameter options: `CameraBoundsOptions` - `nil` parameters take no effect. - func setCameraBounds(for options: CameraBoundsOptions) throws - - // MARK: - Drag API - - /// Prepares the drag gesture to use the provided screen coordinate as a pivot - /// point. This function should be called each time when user starts a - /// dragging action (e.g. by clicking on the map). The following dragging - /// will be relative to the pivot. - /// - /// - Parameter point: Screen point - func dragStart(for point: CGPoint) - - /// Calculates target point where camera should move after drag. The method - /// should be called after `dragStart` and before `dragEnd`. - /// - /// - Parameters: - /// - fromPoint: The point from which the map is dragged. - /// - toPoint: The point to which the map is dragged. - /// - /// - Returns: - /// The camera options object showing end point. - func dragCameraOptions(from: CGPoint, to: CGPoint) -> CameraOptions - - /// Ends the ongoing drag gesture. This function should be called always after - /// the user has ended a drag gesture initiated by `dragStart`. - func dragEnd() -} diff --git a/Sources/MapboxMaps/Foundation/MapFeatureQueryable.swift b/Sources/MapboxMaps/Foundation/MapFeatureQueryable.swift index 8328e94fdd7..d96b5f5ac17 100644 --- a/Sources/MapboxMaps/Foundation/MapFeatureQueryable.swift +++ b/Sources/MapboxMaps/Foundation/MapFeatureQueryable.swift @@ -1,79 +1,27 @@ @_implementationOnly import MapboxCoreMaps_Private internal protocol MapFeatureQueryable: AnyObject { - /// Queries the map for rendered features. - /// - /// - Parameters: - /// - shape: Screen point coordinates (point, line string or box) to query - /// for rendered features. - /// - options: Options for querying rendered features. - /// - completion: Callback called when the query completes func queryRenderedFeatures(for shape: [CGPoint], options: RenderedQueryOptions?, completion: @escaping (Result<[QueriedFeature], Error>) -> Void) - /// Queries the map for rendered features. - /// - /// - Parameters: - /// - rect: Screen rect to query for rendered features. - /// - options: Options for querying rendered features. - /// - completion: Callback called when the query completes func queryRenderedFeatures(in rect: CGRect, options: RenderedQueryOptions?, completion: @escaping (Result<[QueriedFeature], Error>) -> Void) - /// Queries the map for rendered features. - /// - /// - Parameters: - /// - point: Screen point at which to query for rendered features. - /// - options: Options for querying rendered features. - /// - completion: Callback called when the query completes func queryRenderedFeatures(at point: CGPoint, options: RenderedQueryOptions?, completion: @escaping (Result<[QueriedFeature], Error>) -> Void) - /// Queries the map for source features. - /// - /// - Parameters: - /// - sourceId: Style source identifier used to query for source features. - /// - options: Options for querying source features. - /// - completion: Callback called when the query completes func querySourceFeatures(for sourceId: String, options: SourceQueryOptions, completion: @escaping (Result<[QueriedFeature], Error>) -> Void) - //swiftlint:disable function_parameter_count - - /// Queries for feature extension values in a GeoJSON source. - /// - /// - Parameters: - /// - sourceId: The identifier of the source to query. - /// - feature: Feature to look for in the query. - /// - extension: Currently supports keyword `supercluster`. - /// - extensionField: Currently supports following three extensions: - /// - /// 1. `children`: returns the children of a cluster (on the next zoom - /// level). - /// 2. `leaves`: returns all the leaves of a cluster (given its cluster_id) - /// 3. `expansion-zoom`: returns the zoom on which the cluster expands - /// into several children (useful for "click to zoom" feature). - /// - /// - args: Used for further query specification when using 'leaves' - /// extensionField. Now only support following two args: - /// - /// 1. `limit`: the number of points to return from the query (must - /// use type 'UInt64', set to maximum for all points) - /// 2. `offset`: the amount of points to skip (for pagination, must - /// use type 'UInt64') - /// - /// - completion: The result could be a feature extension value containing - /// either a value (expansion-zoom) or a feature collection (children - /// or leaves). An error is passed if the operation was not successful. + //swiftlint:disable:next function_parameter_count func queryFeatureExtension(for sourceId: String, feature: Turf.Feature, extension: String, extensionField: String, args: [String: Any]?, completion: @escaping (Result) -> Void) - //swiftlint:enable function_parameter_count } diff --git a/Sources/MapboxMaps/Foundation/MapProjectionDelegate.swift b/Sources/MapboxMaps/Foundation/MapProjectionDelegate.swift index 58b8704c6d8..f7c2d973972 100644 --- a/Sources/MapboxMaps/Foundation/MapProjectionDelegate.swift +++ b/Sources/MapboxMaps/Foundation/MapProjectionDelegate.swift @@ -1,6 +1,11 @@ import CoreLocation -internal protocol MapProjectionDelegate: AnyObject { +public final class Projection { + public static let latitudeMax: CLLocationDegrees = +85.051128779806604 + public static let latitudeMin: CLLocationDegrees = -85.051128779806604 + + internal init() {} + /// Calculate distance spanned by one pixel at the specified latitude and /// zoom level. /// @@ -9,14 +14,18 @@ internal protocol MapProjectionDelegate: AnyObject { /// - zoom: The zoom level /// /// - Returns: Meters - static func metersPerPoint(for latitude: CLLocationDegrees, zoom: CGFloat) -> Double + public static func metersPerPoint(for latitude: CLLocationDegrees, zoom: CGFloat) -> Double { + return MapboxCoreMaps.Projection.getMetersPerPixelAtLatitude(forLatitude: latitude, zoom: Double(zoom)) + } /// Calculate Spherical Mercator ProjectedMeters coordinates. /// - Parameter coordinate: Coordinate at which to calculate the projected /// meters /// /// - Returns: Spherical Mercator ProjectedMeters coordinates - static func projectedMeters(for coordinate: CLLocationCoordinate2D) -> ProjectedMeters + public static func projectedMeters(for coordinate: CLLocationCoordinate2D) -> ProjectedMeters { + return MapboxCoreMaps.Projection.projectedMetersForCoordinate(for: coordinate) + } /// Calculate a coordinate for a Spherical Mercator projected /// meters. @@ -24,7 +33,9 @@ internal protocol MapProjectionDelegate: AnyObject { /// - Parameter projectedMeters: Spherical Mercator ProjectedMeters coordinates /// /// - Returns: A coordinate - static func coordinate(for projectedMeters: ProjectedMeters) -> CLLocationCoordinate2D + public static func coordinate(for projectedMeters: ProjectedMeters) -> CLLocationCoordinate2D { + return MapboxCoreMaps.Projection.coordinateForProjectedMeters(for: projectedMeters) + } /// Calculate a point on the map in Mercator Projection for a given /// coordinate at the specified zoom scale. @@ -39,7 +50,9 @@ internal protocol MapProjectionDelegate: AnyObject { /// /// - Note: Coordinate latitudes will be clamped to /// [Projection.latitudeMin, Projection.latitudeMax] - static func project(_ coordinate: CLLocationCoordinate2D, zoomScale: CGFloat) -> MercatorCoordinate + public static func project(_ coordinate: CLLocationCoordinate2D, zoomScale: CGFloat) -> MercatorCoordinate { + return MapboxCoreMaps.Projection.project(for: coordinate, zoomScale: Double(zoomScale)) + } /// Calculate a coordinate for a given point on the map in Mercator Projection. /// @@ -50,31 +63,6 @@ internal protocol MapProjectionDelegate: AnyObject { /// 512 * 2 ^ Zoom level) where tileSize is the width of a tile in /// points. /// - Returns: Unprojected coordinate - static func unproject(_ mercatorCoordinate: MercatorCoordinate, zoomScale: CGFloat) -> CLLocationCoordinate2D -} - -public class Projection: MapProjectionDelegate { - public static let latitudeMax: CLLocationDegrees = +85.051128779806604 - public static let latitudeMin: CLLocationDegrees = -85.051128779806604 - - internal init() {} - - public static func metersPerPoint(for latitude: CLLocationDegrees, zoom: CGFloat) -> Double { - return MapboxCoreMaps.Projection.getMetersPerPixelAtLatitude(forLatitude: latitude, zoom: Double(zoom)) - } - - public static func projectedMeters(for coordinate: CLLocationCoordinate2D) -> ProjectedMeters { - return MapboxCoreMaps.Projection.projectedMetersForCoordinate(for: coordinate) - } - - public static func coordinate(for projectedMeters: ProjectedMeters) -> CLLocationCoordinate2D { - return MapboxCoreMaps.Projection.coordinateForProjectedMeters(for: projectedMeters) - } - - public static func project(_ coordinate: CLLocationCoordinate2D, zoomScale: CGFloat) -> MercatorCoordinate { - return MapboxCoreMaps.Projection.project(for: coordinate, zoomScale: Double(zoomScale)) - } - public static func unproject(_ mercatorCoordinate: MercatorCoordinate, zoomScale: CGFloat) -> CLLocationCoordinate2D { return MapboxCoreMaps.Projection.unproject(for: mercatorCoordinate, zoomScale: Double(zoomScale)) } diff --git a/Sources/MapboxMaps/Foundation/MapTransformDelegate.swift b/Sources/MapboxMaps/Foundation/MapTransformDelegate.swift deleted file mode 100644 index 4ab2b81d4b8..00000000000 --- a/Sources/MapboxMaps/Foundation/MapTransformDelegate.swift +++ /dev/null @@ -1,31 +0,0 @@ -internal protocol MapTransformDelegate: AnyObject { - /// Gets the size of the map in points - var size: CGSize { get set } - - /// Notify map about gesture being in progress. - var isGestureInProgress: Bool { get set } - - /// Tells the map rendering engine that the animation is currently performed - /// by the user (e.g. with a `setCamera()` calls series). It adjusts the - /// engine for the animation use case. - /// In particular, it brings more stability to symbol placement and rendering. - var isUserAnimationInProgress: Bool { get set } - - /// Returns the map's options - var options: MapOptions { get } - - /// Set the map north orientation - /// - /// - Parameter northOrientation: The map north orientation to set - func setNorthOrientation(northOrientation: NorthOrientation) - - /// Set the map constrain mode - /// - /// - Parameter constrainMode: The map constraint mode to set - func setConstrainMode(_ constrainMode: ConstrainMode) - - /// Set the map viewport mode - /// - /// - Parameter viewportMode: The map viewport mode to set - func setViewportMode(_ viewportMode: ViewportMode) -} diff --git a/Sources/MapboxMaps/Foundation/MapView+Supportable.swift b/Sources/MapboxMaps/Foundation/MapView+Supportable.swift deleted file mode 100644 index b93294f1549..00000000000 --- a/Sources/MapboxMaps/Foundation/MapView+Supportable.swift +++ /dev/null @@ -1,36 +0,0 @@ -import UIKit -import CoreLocation - -@available(iOSApplicationExtension, unavailable) -extension MapView: OrnamentSupportableView { - // User has tapped on an ornament - internal func tapped() { - - } - - internal func compassTapped() { - camera.cancelAnimations() - - var animator: BasicCameraAnimator? - animator = camera.makeAnimator(duration: 0.3, curve: .easeOut, animations: { (transition) in - transition.bearing.toValue = 0 - }) - - animator?.addCompletion { (_) in - animator = nil - } - - animator?.startAnimation() - } - - internal func subscribeCameraChangeHandler(_ handler: @escaping (CameraState) -> Void) { - mapboxMap.onEvery(.cameraChanged) { [weak self] _ in - guard let self = self else { - return - } - handler(self.cameraState) - } - } -} - -extension Style: LocationStyleProtocol { } diff --git a/Sources/MapboxMaps/Foundation/MapView.swift b/Sources/MapboxMaps/Foundation/MapView.swift index 11c09f8d8e8..c83e271a860 100644 --- a/Sources/MapboxMaps/Foundation/MapView.swift +++ b/Sources/MapboxMaps/Foundation/MapView.swift @@ -228,7 +228,12 @@ open class MapView: UIView { attributionDialogManager = AttributionDialogManager(dataSource: mapboxMap, delegate: self) // Initialize/Configure ornaments manager - ornaments = OrnamentsManager(view: self, options: OrnamentOptions(), infoButtonOrnamentDelegate: attributionDialogManager) + ornaments = OrnamentsManager( + options: OrnamentOptions(), + view: self, + mapboxMap: mapboxMap, + cameraAnimationsManager: camera, + infoButtonOrnamentDelegate: attributionDialogManager) // Initialize/Configure location manager location = LocationManager(style: mapboxMap.style) diff --git a/Sources/MapboxMaps/Foundation/MapViewDependencyProvider.swift b/Sources/MapboxMaps/Foundation/MapViewDependencyProvider.swift index 3a7a0896ba7..14cf5bd5b84 100644 --- a/Sources/MapboxMaps/Foundation/MapViewDependencyProvider.swift +++ b/Sources/MapboxMaps/Foundation/MapViewDependencyProvider.swift @@ -30,25 +30,21 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco } func makePinchGestureHandler(view: UIView, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureHandler { + mapboxMap: MapboxMapProtocol) -> GestureHandler { let gestureRecognizer = UIPinchGestureRecognizer() view.addGestureRecognizer(gestureRecognizer) return PinchGestureHandler( gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + mapboxMap: mapboxMap) } func makePitchGestureHandler(view: UIView, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureHandler { + mapboxMap: MapboxMapProtocol) -> GestureHandler { let gestureRecognizer = UIPanGestureRecognizer() view.addGestureRecognizer(gestureRecognizer) return PitchGestureHandler( gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + mapboxMap: mapboxMap) } func makeDoubleTapToZoomInGestureHandler(view: UIView, @@ -62,17 +58,6 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco cameraAnimationsManager: cameraAnimationsManager) } - func makeSingleTapGestureHandler(view: UIView, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureHandler { - let gestureRecognizer = UITapGestureRecognizer() - view.addGestureRecognizer(gestureRecognizer) - return SingleTapGestureHandler( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) - } - func makeDoubleTouchToZoomOutGestureHandler(view: UIView, mapboxMap: MapboxMapProtocol, cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureHandler { @@ -85,13 +70,28 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco } func makeQuickZoomGestureHandler(view: UIView, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureHandler { + mapboxMap: MapboxMapProtocol) -> GestureHandler { let gestureRecognizer = UILongPressGestureRecognizer() view.addGestureRecognizer(gestureRecognizer) return QuickZoomGestureHandler( gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, + mapboxMap: mapboxMap) + } + + func makeSingleTapGestureHandler(view: UIView, + mapboxMap: MapboxMapProtocol) -> GestureHandler { + let gestureRecognizer = UITapGestureRecognizer() + view.addGestureRecognizer(gestureRecognizer) + return SingleTapGestureHandler(gestureRecognizer: gestureRecognizer) + } + + func makeAnimationLockoutGestureHandler(view: UIView, + mapboxMap: MapboxMapProtocol, + cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureHandler { + let gestureRecognizer = AnyTouchGestureRecognizer() + view.addGestureRecognizer(gestureRecognizer) + return AnimationLockoutGestureHandler( + gestureRecognizer: gestureRecognizer, cameraAnimationsManager: cameraAnimationsManager) } @@ -105,12 +105,10 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco cameraAnimationsManager: cameraAnimationsManager), pinchGestureHandler: makePinchGestureHandler( view: view, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager), + mapboxMap: mapboxMap), pitchGestureHandler: makePitchGestureHandler( view: view, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager), + mapboxMap: mapboxMap), doubleTapToZoomInGestureHandler: makeDoubleTapToZoomInGestureHandler( view: view, mapboxMap: mapboxMap, @@ -121,9 +119,11 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco cameraAnimationsManager: cameraAnimationsManager), quickZoomGestureHandler: makeQuickZoomGestureHandler( view: view, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager), + mapboxMap: mapboxMap), singleTapGestureHandler: makeSingleTapGestureHandler( + view: view, + mapboxMap: mapboxMap), + animationLockoutGestureHandler: makeAnimationLockoutGestureHandler( view: view, mapboxMap: mapboxMap, cameraAnimationsManager: cameraAnimationsManager)) diff --git a/Sources/MapboxMaps/Foundation/MapboxMap.swift b/Sources/MapboxMaps/Foundation/MapboxMap.swift index a7e4e0d6d88..a2398c548f1 100644 --- a/Sources/MapboxMaps/Foundation/MapboxMap.swift +++ b/Sources/MapboxMaps/Foundation/MapboxMap.swift @@ -5,6 +5,7 @@ import UIKit @_implementationOnly import MapboxCoreMaps_Private internal protocol MapboxMapProtocol: AnyObject { + var size: CGSize { get } var cameraBounds: CameraBounds { get } var cameraState: CameraState { get } var anchor: CGPoint { get } @@ -12,6 +13,9 @@ internal protocol MapboxMapProtocol: AnyObject { func dragStart(for point: CGPoint) func dragCameraOptions(from: CGPoint, to: CGPoint) -> CameraOptions func dragEnd() + + @discardableResult + func onEvery(_ eventType: MapEvents.EventKind, handler: @escaping (Event) -> Void) -> Cancelable } public final class MapboxMap: MapboxMapProtocol { @@ -208,9 +212,8 @@ public final class MapboxMap: MapboxMapProtocol { __map.setDebugForDebugOptions(options, value: true) } } -} -extension MapboxMap: MapTransformDelegate { + /// Gets the size of the map in points internal var size: CGSize { get { CGSize(__map.getSize()) @@ -220,6 +223,7 @@ extension MapboxMap: MapTransformDelegate { } } + /// Notify map about gesture being in progress. internal var isGestureInProgress: Bool { get { return __map.isGestureInProgress() @@ -229,6 +233,10 @@ extension MapboxMap: MapTransformDelegate { } } + /// Tells the map rendering engine that the animation is currently performed + /// by the user (e.g. with a `setCamera()` calls series). It adjusts the + /// engine for the animation use case. + /// In particular, it brings more stability to symbol placement and rendering. internal var isUserAnimationInProgress: Bool { get { return __map.isUserAnimationInProgress() @@ -238,27 +246,40 @@ extension MapboxMap: MapTransformDelegate { } } + /// Returns the map's options public var options: MapOptions { return __map.getOptions() } + /// Set the map north orientation + /// + /// - Parameter northOrientation: The map north orientation to set internal func setNorthOrientation(northOrientation: NorthOrientation) { __map.setNorthOrientationFor(northOrientation) } + /// Set the map constrain mode + /// + /// - Parameter constrainMode: The map constraint mode to set internal func setConstrainMode(_ constrainMode: ConstrainMode) { __map.setConstrainModeFor(constrainMode) } + /// Set the map viewport mode + /// + /// - Parameter viewportMode: The map viewport mode to set internal func setViewportMode(_ viewportMode: ViewportMode) { __map.setViewportModeFor(viewportMode) } -} - -// MARK: - CameraManagerProtocol - - -extension MapboxMap: CameraManagerProtocol { + /// Calculates a `CameraOptions` to fit a `CoordinateBounds` + /// + /// - Parameters: + /// - coordinateBounds: The coordinate bounds that will be displayed within the viewport. + /// - padding: The new padding to be used by the camera. + /// - bearing: The new bearing to be used by the camera. + /// - pitch: The new pitch to be used by the camera. + /// - Returns: A `CameraOptions` that fits the provided constraints public func camera(for coordinateBounds: CoordinateBounds, padding: UIEdgeInsets, bearing: Double?, @@ -271,6 +292,14 @@ extension MapboxMap: CameraManagerProtocol { pitch: pitch?.NSNumber)) } + /// Calculates a `CameraOptions` to fit a list of coordinates. + /// + /// - Parameters: + /// - coordinates: Array of coordinates that should fit within the new viewport. + /// - padding: The new padding to be used by the camera. + /// - bearing: The new bearing to be used by the camera. + /// - pitch: The new pitch to be used by the camera. + /// - Returns: A `CameraOptions` that fits the provided constraints public func camera(for coordinates: [CLLocationCoordinate2D], padding: UIEdgeInsets, bearing: Double?, @@ -283,6 +312,25 @@ extension MapboxMap: CameraManagerProtocol { pitch: pitch?.NSNumber)) } + /// Calculates a `CameraOptions` to fit a list of coordinates into a sub-rect of the map. + /// + /// Adjusts the zoom of `camera` to fit `coordinates` into `rect`. + /// + /// Returns the provided camera with zoom adjusted to fit coordinates into + /// `rect`, so that the coordinates on the left, top and right of the effective + /// camera center at the principal point of the projection (defined by padding) + /// fit into the rect. + /// + /// - Note: + /// This method may fail if the principal point of the projection is not + /// inside `rect` or if there is insufficient screen space, defined by + /// principal point and rect, to fit the geometry. + /// + /// - Parameters: + /// - coordinates: The coordinates to frame within `rect`. + /// - camera: The camera for which the zoom should be adjusted to fit `coordinates`. `camera.center` must be non-nil. + /// - rect: The rectangle inside of the map that should be used to frame `coordinates`. + /// - Returns: A `CameraOptions` that fits the provided constraints, or `cameraOptions` if an error occurs. public func camera(for coordinates: [CLLocationCoordinate2D], camera: CameraOptions, rect: CGRect) -> CameraOptions { @@ -293,6 +341,14 @@ extension MapboxMap: CameraManagerProtocol { box: ScreenBox(rect))) } + /// Calculates a `CameraOptions` to fit a geometry + /// + /// - Parameters: + /// - geometry: The geoemtry that will be displayed within the viewport. + /// - padding: The new padding to be used by the camera. + /// - bearing: The new bearing to be used by the camera. + /// - pitch: The new pitch to be used by the camera. + /// - Returns: A `CameraOptions` that fits the provided constraints public func camera(for geometry: Turf.Geometry, padding: UIEdgeInsets, bearing: CGFloat?, @@ -307,33 +363,71 @@ extension MapboxMap: CameraManagerProtocol { // MARK: - CameraOptions to CoordinateBounds + /// Returns the coordinate bounds corresponding to a given `CameraOptions` + /// + /// - Parameter camera: The camera for which the coordinate bounds will be returned. + /// - Returns: `CoordinateBounds` for the given `CameraOptions` public func coordinateBounds(for camera: CameraOptions) -> CoordinateBounds { return __map.coordinateBoundsForCamera( forCamera: MapboxCoreMaps.CameraOptions(camera)) } + /// Returns the coordinate bounds and zoom for a given `CameraOptions`. + /// + /// - Parameter camera: The camera for which the `CoordinateBoundsZoom` will be returned. + /// - Returns: `CoordinateBoundsZoom` for the given `CameraOptions` public func coordinateBoundsZoom(for camera: CameraOptions) -> CoordinateBoundsZoom { return __map.coordinateBoundsZoomForCamera(forCamera: MapboxCoreMaps.CameraOptions(camera)) } + /// Returns the unwrapped coordinate bounds and zoom for a given `CameraOptions`. + /// + /// This function is particularly useful, if the camera shows the antimeridian. + /// + /// - Parameter camera: The camera for which the `CoordinateBoundsZoom` will + /// be returned. + /// - Returns: `CoordinateBoundsZoom` for the given `CameraOptions` public func coordinateBoundsZoomUnwrapped(for camera: CameraOptions) -> CoordinateBoundsZoom { return __map.coordinateBoundsZoomForCameraUnwrapped(forCamera: MapboxCoreMaps.CameraOptions(camera)) } + // MARK: - Screen coordinate conversion + + /// Converts a point in the mapView's coordinate system to a geographic coordinate. + /// The point must exist in the coordinate space of the `MapView` + /// + /// - Parameter point: The point to convert. Must exist in the coordinate space + /// of the `MapView` + /// - Returns: A `CLLocationCoordinate` that represents the geographic location + /// of the point. public func coordinate(for point: CGPoint) -> CLLocationCoordinate2D { return __map.coordinateForPixel(forPixel: point.screenCoordinate) } + /// Converts a map coordinate to a `CGPoint`, relative to the `MapView`. + /// - Parameter coordinate: The coordinate to convert. + /// - Returns: A `CGPoint` relative to the `UIView`. public func point(for coordinate: CLLocationCoordinate2D) -> CGPoint { return __map.pixelForCoordinate(for: coordinate).point } + /// Converts map coordinates to an array of `CGPoint`, relative to the `MapView`. + /// + /// - Parameter coordinates: The coordinate to convert. + /// - Returns: An array of `CGPoint` relative to the `UIView`. public func points(for coordinates: [CLLocationCoordinate2D]) -> [CGPoint] { let locations = coordinates.map { CLLocation(latitude: $0.latitude, longitude: $0.longitude) } let screenCoords = __map.pixelsForCoordinates(forCoordinates: locations) return screenCoords.map { $0.point } } + /// Converts points in the mapView's coordinate system to geographic coordinates. + /// The points must exist in the coordinate space of the `MapView`. + /// + /// - Parameter point: The point to convert. Must exist in the coordinate space + /// of the `MapView` + /// - Returns: A `CLLocationCoordinate` that represents the geographic location + /// of the point. public func coordinates(for points: [CGPoint]) -> [CLLocationCoordinate2D] { let screenCoords = points.map { $0.screenCoordinate } let locations = __map.coordinatesForPixels(forPixels: screenCoords) @@ -342,10 +436,22 @@ extension MapboxMap: CameraManagerProtocol { // MARK: - Camera options setters/getters + /// Changes the map view by any combination of center, zoom, bearing, and pitch, + /// without an animated transition. The map will retain its current values + /// for any details not passed via the camera options argument. It is not + /// guaranteed that the provided `CameraOptions` will be set, the map may apply + /// constraints resulting in a different `CameraState`. + /// + /// - Important: + /// This method does not cancel existing animations. Call + /// `CameraAnimationsManager.cancelAnimations()`to cancel existing animations. + /// + /// - Parameter cameraOptions: New camera options public func setCamera(to cameraOptions: CameraOptions) { __map.setCameraFor(MapboxCoreMaps.CameraOptions(cameraOptions)) } + /// Returns the current camera state public var cameraState: CameraState { return CameraState(__map.getCameraState()) } @@ -356,6 +462,16 @@ extension MapboxMap: CameraManagerProtocol { return CGPoint(x: rect.midX, y: rect.midY) } + /// Sets/get the map view with the free camera options. + /// + /// FreeCameraOptions provides more direct access to the underlying camera entity. + /// For backwards compatibility the state set using this API must be representable + /// with `CameraOptions` as well. Parameters are clamped to a valid range or + /// discarded as invalid if the conversion to the pitch and bearing presentation + /// is ambiguous. For example orientation can be invalid if it leads to the + /// camera being upside down or the quaternion has zero length. + /// + /// - Parameter freeCameraOptions: The free camera options to set. public var freeCameraOptions: FreeCameraOptions { get { return __map.getFreeCameraOptions() @@ -374,7 +490,7 @@ extension MapboxMap: CameraManagerProtocol { /// /// - Parameter options: New camera bounds. Nil values will not take effect. /// - Throws: `MapError` - public func setCameraBounds(for options: CameraBoundsOptions) throws { + public func setCameraBounds(with options: CameraBoundsOptions) throws { let expected = __map.setBoundsFor(MapboxCoreMaps.CameraBoundsOptions(options)) if expected.isError() { @@ -386,16 +502,33 @@ extension MapboxMap: CameraManagerProtocol { // MARK: - Drag API + /// Prepares the drag gesture to use the provided screen coordinate as a pivot + /// point. This function should be called each time when user starts a + /// dragging action (e.g. by clicking on the map). The following dragging + /// will be relative to the pivot. + /// + /// - Parameter point: Screen point public func dragStart(for point: CGPoint) { __map.dragStart(forPoint: point.screenCoordinate) } + /// Calculates target point where camera should move after drag. The method + /// should be called after `dragStart` and before `dragEnd`. + /// + /// - Parameters: + /// - fromPoint: The point from which the map is dragged. + /// - toPoint: The point to which the map is dragged. + /// + /// - Returns: + /// The camera options object showing end point. public func dragCameraOptions(from: CGPoint, to: CGPoint) -> CameraOptions { let options = __map.getDragCameraOptionsFor(fromPoint: from.screenCoordinate, toPoint: to.screenCoordinate) return CameraOptions(options) } + /// Ends the ongoing drag gesture. This function should be called always after + /// the user has ended a drag gesture initiated by `dragStart`. public func dragEnd() { __map.dragEnd() } @@ -404,6 +537,13 @@ extension MapboxMap: CameraManagerProtocol { // MARK: - MapFeatureQueryable - extension MapboxMap: MapFeatureQueryable { + /// Queries the map for rendered features. + /// + /// - Parameters: + /// - shape: Screen point coordinates (point, line string or box) to query + /// for rendered features. + /// - options: Options for querying rendered features. + /// - completion: Callback called when the query completes public func queryRenderedFeatures(for shape: [CGPoint], options: RenderedQueryOptions? = nil, completion: @escaping (Result<[QueriedFeature], Error>) -> Void) { __map.queryRenderedFeatures(forShape: shape.map { $0.screenCoordinate }, options: options ?? RenderedQueryOptions(layerIds: nil, filter: nil), @@ -412,6 +552,12 @@ extension MapboxMap: MapFeatureQueryable { concreteErrorType: MapError.self)) } + /// Queries the map for rendered features. + /// + /// - Parameters: + /// - rect: Screen rect to query for rendered features. + /// - options: Options for querying rendered features. + /// - completion: Callback called when the query completes public func queryRenderedFeatures(in rect: CGRect, options: RenderedQueryOptions? = nil, completion: @escaping (Result<[QueriedFeature], Error>) -> Void) { __map.queryRenderedFeatures(for: ScreenBox(rect), options: options ?? RenderedQueryOptions(layerIds: nil, filter: nil), @@ -420,6 +566,12 @@ extension MapboxMap: MapFeatureQueryable { concreteErrorType: MapError.self)) } + /// Queries the map for rendered features. + /// + /// - Parameters: + /// - point: Screen point at which to query for rendered features. + /// - options: Options for querying rendered features. + /// - completion: Callback called when the query completes public func queryRenderedFeatures(at point: CGPoint, options: RenderedQueryOptions? = nil, completion: @escaping (Result<[QueriedFeature], Error>) -> Void) { __map.queryRenderedFeatures(forPixel: point.screenCoordinate, options: options ?? RenderedQueryOptions(layerIds: nil, filter: nil), @@ -428,6 +580,12 @@ extension MapboxMap: MapFeatureQueryable { concreteErrorType: MapError.self)) } + /// Queries the map for source features. + /// + /// - Parameters: + /// - sourceId: Style source identifier used to query for source features. + /// - options: Options for querying source features. + /// - completion: Callback called when the query completes public func querySourceFeatures(for sourceId: String, options: SourceQueryOptions, completion: @escaping (Result<[QueriedFeature], Error>) -> Void) { @@ -438,6 +596,31 @@ extension MapboxMap: MapFeatureQueryable { concreteErrorType: MapError.self)) } + /// Queries for feature extension values in a GeoJSON source. + /// + /// - Parameters: + /// - sourceId: The identifier of the source to query. + /// - feature: Feature to look for in the query. + /// - extension: Currently supports keyword `supercluster`. + /// - extensionField: Currently supports following three extensions: + /// + /// 1. `children`: returns the children of a cluster (on the next zoom + /// level). + /// 2. `leaves`: returns all the leaves of a cluster (given its cluster_id) + /// 3. `expansion-zoom`: returns the zoom on which the cluster expands + /// into several children (useful for "click to zoom" feature). + /// + /// - args: Used for further query specification when using 'leaves' + /// extensionField. Now only support following two args: + /// + /// 1. `limit`: the number of points to return from the query (must + /// use type 'UInt64', set to maximum for all points) + /// 2. `offset`: the amount of points to skip (for pagination, must + /// use type 'UInt64') + /// + /// - completion: The result could be a feature extension value containing + /// either a value (expansion-zoom) or a feature collection (children + /// or leaves). An error is passed if the operation was not successful. public func queryFeatureExtension(for sourceId: String, feature: Feature, extension: String, @@ -582,7 +765,6 @@ extension MapboxMap { featureId: featureId, stateKey: stateKey) } - } // MARK: - Testing only! - diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/AnimationLockoutGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/AnimationLockoutGestureHandler.swift new file mode 100644 index 00000000000..a914d3fff60 --- /dev/null +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/AnimationLockoutGestureHandler.swift @@ -0,0 +1,25 @@ +import UIKit + +internal final class AnimationLockoutGestureHandler: GestureHandler { + + private let cameraAnimationsManager: CameraAnimationsManagerProtocol + + internal init(gestureRecognizer: UIGestureRecognizer, + cameraAnimationsManager: CameraAnimationsManagerProtocol) { + gestureRecognizer.cancelsTouchesInView = false + self.cameraAnimationsManager = cameraAnimationsManager + super.init(gestureRecognizer: gestureRecognizer) + gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:))) + } + + @objc private func handleGesture(_ gestureRecognizer: AnyTouchGestureRecognizer) { + switch gestureRecognizer.state { + case .began: + cameraAnimationsManager.animationsEnabled = false + case .ended, .cancelled: + cameraAnimationsManager.animationsEnabled = true + default: + break + } + } +} diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTapToZoomInGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTapToZoomInGestureHandler.swift index 9a962a38e41..8ab1381f7be 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTapToZoomInGestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTapToZoomInGestureHandler.swift @@ -4,22 +4,24 @@ import UIKit /// to double tap gestures with 1 touch internal final class DoubleTapToZoomInGestureHandler: GestureHandler { + private let mapboxMap: MapboxMapProtocol + + private let cameraAnimationsManager: CameraAnimationsManagerProtocol + internal init(gestureRecognizer: UITapGestureRecognizer, mapboxMap: MapboxMapProtocol, cameraAnimationsManager: CameraAnimationsManagerProtocol) { gestureRecognizer.numberOfTapsRequired = 2 gestureRecognizer.numberOfTouchesRequired = 1 - super.init( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + self.mapboxMap = mapboxMap + self.cameraAnimationsManager = cameraAnimationsManager + super.init(gestureRecognizer: gestureRecognizer) gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:))) } @objc private func handleGesture(_ gestureRecognizer: UITapGestureRecognizer) { switch gestureRecognizer.state { case .recognized: - cameraAnimationsManager.cancelAnimations() delegate?.gestureBegan(for: .doubleTapToZoomIn) delegate?.gestureEnded(for: .doubleTapToZoomIn, willAnimate: true) cameraAnimationsManager.ease( diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTouchToZoomOutGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTouchToZoomOutGestureHandler.swift index 69fa51b6722..eab72574527 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTouchToZoomOutGestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTouchToZoomOutGestureHandler.swift @@ -4,22 +4,25 @@ import UIKit /// to single tap gestures with 2 touches internal final class DoubleTouchToZoomOutGestureHandler: GestureHandler { + private let mapboxMap: MapboxMapProtocol + + private let cameraAnimationsManager: CameraAnimationsManagerProtocol + internal init(gestureRecognizer: UITapGestureRecognizer, mapboxMap: MapboxMapProtocol, cameraAnimationsManager: CameraAnimationsManagerProtocol) { gestureRecognizer.numberOfTapsRequired = 1 gestureRecognizer.numberOfTouchesRequired = 2 + self.mapboxMap = mapboxMap + self.cameraAnimationsManager = cameraAnimationsManager super.init( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + gestureRecognizer: gestureRecognizer) gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:))) } @objc private func handleGesture(_ gestureRecognizer: UITapGestureRecognizer) { switch gestureRecognizer.state { case .recognized: - cameraAnimationsManager.cancelAnimations() delegate?.gestureBegan(for: .doubleTouchToZoomOut) delegate?.gestureEnded(for: .doubleTouchToZoomOut, willAnimate: true) cameraAnimationsManager.ease( diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/GestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/GestureHandler.swift index 13728b947af..541e50776a7 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/GestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/GestureHandler.swift @@ -11,18 +11,10 @@ internal protocol GestureHandlerDelegate: AnyObject { internal class GestureHandler: NSObject { internal let gestureRecognizer: UIGestureRecognizer - internal let mapboxMap: MapboxMapProtocol - - internal let cameraAnimationsManager: CameraAnimationsManagerProtocol - internal weak var delegate: GestureHandlerDelegate? - init(gestureRecognizer: UIGestureRecognizer, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) { + init(gestureRecognizer: UIGestureRecognizer) { self.gestureRecognizer = gestureRecognizer - self.mapboxMap = mapboxMap - self.cameraAnimationsManager = cameraAnimationsManager } deinit { diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift index d9d793236e1..d5967d9d642 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift @@ -25,6 +25,10 @@ internal final class PanGestureHandler: GestureHandler, PanGestureHandlerProtoco /// The date when the most recent gesture changed event was handled private var lastChangedDate: Date? + private let mapboxMap: MapboxMapProtocol + + private let cameraAnimationsManager: CameraAnimationsManagerProtocol + /// Provides access to the current date in a way that can be mocked /// for unit testing private let dateProvider: DateProvider @@ -34,11 +38,10 @@ internal final class PanGestureHandler: GestureHandler, PanGestureHandlerProtoco cameraAnimationsManager: CameraAnimationsManagerProtocol, dateProvider: DateProvider) { gestureRecognizer.maximumNumberOfTouches = 1 + self.mapboxMap = mapboxMap + self.cameraAnimationsManager = cameraAnimationsManager self.dateProvider = dateProvider - super.init( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + super.init(gestureRecognizer: gestureRecognizer) gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:))) } @@ -53,7 +56,6 @@ internal final class PanGestureHandler: GestureHandler, PanGestureHandlerProtoco case .began: initialTouchLocation = touchLocation initialCameraState = mapboxMap.cameraState - cameraAnimationsManager.cancelAnimations() delegate?.gestureBegan(for: .pan) case .changed: guard let initialTouchLocation = initialTouchLocation, @@ -61,7 +63,6 @@ internal final class PanGestureHandler: GestureHandler, PanGestureHandlerProtoco return } lastChangedDate = dateProvider.now - cameraAnimationsManager.cancelAnimations() handleChange( withTouchLocation: touchLocation, initialTouchLocation: initialTouchLocation, diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/PinchGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/PinchGestureHandler.swift index b62d18e7e22..be1ab43951d 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/PinchGestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/PinchGestureHandler.swift @@ -18,14 +18,13 @@ internal final class PinchGestureHandler: GestureHandler { /// The camera bearing when the gesture began or unpaused private var initialBearing: CLLocationDirection? + private let mapboxMap: MapboxMapProtocol + /// Initialize the handler which creates the panGestureRecognizer and adds to the view internal init(gestureRecognizer: UIPinchGestureRecognizer, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) { - super.init( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + mapboxMap: MapboxMapProtocol) { + self.mapboxMap = mapboxMap + super.init(gestureRecognizer: gestureRecognizer) gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:))) } @@ -42,11 +41,8 @@ internal final class PinchGestureHandler: GestureHandler { initialCenter = mapboxMap.cameraState.center initialZoom = mapboxMap.cameraState.zoom initialBearing = mapboxMap.cameraState.bearing - cameraAnimationsManager.cancelAnimations() delegate?.gestureBegan(for: .pinch) case .changed: - cameraAnimationsManager.cancelAnimations() - // UIPinchGestureRecognizer sends a .changed event when the number // of touches decreases from 2 to 1. If this happens, we pause our // gesture handling. diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/PitchGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/PitchGestureHandler.swift index f5e795d4b14..d79a3a0bfc3 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/PitchGestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/PitchGestureHandler.swift @@ -5,15 +5,14 @@ import UIKit internal final class PitchGestureHandler: GestureHandler, UIGestureRecognizerDelegate { private var initialPitch: CGFloat? + private let mapboxMap: MapboxMapProtocol + internal init(gestureRecognizer: UIPanGestureRecognizer, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) { + mapboxMap: MapboxMapProtocol) { gestureRecognizer.minimumNumberOfTouches = 2 gestureRecognizer.maximumNumberOfTouches = 2 - super.init( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + self.mapboxMap = mapboxMap + super.init(gestureRecognizer: gestureRecognizer) gestureRecognizer.delegate = self gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:))) } @@ -38,7 +37,6 @@ internal final class PitchGestureHandler: GestureHandler, UIGestureRecognizerDel switch gestureRecognizer.state { case .began: initialPitch = mapboxMap.cameraState.pitch - cameraAnimationsManager.cancelAnimations() delegate?.gestureBegan(for: .pitch) case .changed: guard let view = gestureRecognizer.view, diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/QuickZoomGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/QuickZoomGestureHandler.swift index 613baf070fc..1518032506d 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/QuickZoomGestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/QuickZoomGestureHandler.swift @@ -4,16 +4,14 @@ import UIKit internal final class QuickZoomGestureHandler: GestureHandler { private var initialLocation: CGPoint? private var initialZoom: CGFloat? + private let mapboxMap: MapboxMapProtocol internal init(gestureRecognizer: UILongPressGestureRecognizer, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) { + mapboxMap: MapboxMapProtocol) { gestureRecognizer.numberOfTapsRequired = 1 gestureRecognizer.minimumPressDuration = 0 - super.init( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + self.mapboxMap = mapboxMap + super.init(gestureRecognizer: gestureRecognizer) gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:))) } @@ -24,7 +22,6 @@ internal final class QuickZoomGestureHandler: GestureHandler { let location = gestureRecognizer.location(in: view) switch gestureRecognizer.state { case .began: - cameraAnimationsManager.cancelAnimations() delegate?.gestureBegan(for: .quickZoom) initialLocation = location initialZoom = mapboxMap.cameraState.zoom diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/SingleTapGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/SingleTapGestureHandler.swift index fda7f9c9ae8..076b0c53e32 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/SingleTapGestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/SingleTapGestureHandler.swift @@ -3,22 +3,16 @@ import UIKit /// `SingleTapGestureHandler` manages a gesture recognizer looking for single tap touch events internal final class SingleTapGestureHandler: GestureHandler { - internal init(gestureRecognizer: UITapGestureRecognizer, - mapboxMap: MapboxMapProtocol, - cameraAnimationsManager: CameraAnimationsManagerProtocol) { + internal init(gestureRecognizer: UITapGestureRecognizer) { gestureRecognizer.numberOfTapsRequired = 1 gestureRecognizer.numberOfTouchesRequired = 1 - super.init( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + super.init(gestureRecognizer: gestureRecognizer) gestureRecognizer.addTarget(self, action: #selector(handleGesture(_:))) } @objc private func handleGesture(_ gestureRecognizer: UITapGestureRecognizer) { switch gestureRecognizer.state { case .recognized: - cameraAnimationsManager.cancelAnimations() delegate?.gestureBegan(for: .singleTap) default: break diff --git a/Sources/MapboxMaps/Gestures/GestureManager.swift b/Sources/MapboxMaps/Gestures/GestureManager.swift index b9273fc107b..01e9e50ea3c 100644 --- a/Sources/MapboxMaps/Gestures/GestureManager.swift +++ b/Sources/MapboxMaps/Gestures/GestureManager.swift @@ -89,6 +89,7 @@ public final class GestureManager: GestureHandlerDelegate { private let doubleTouchToZoomOutGestureHandler: GestureHandler private let quickZoomGestureHandler: GestureHandler private let singleTapGestureHandler: GestureHandler + private let animationLockoutGestureHandler: GestureHandler internal init(panGestureHandler: PanGestureHandlerProtocol, pinchGestureHandler: GestureHandler, @@ -96,7 +97,8 @@ public final class GestureManager: GestureHandlerDelegate { doubleTapToZoomInGestureHandler: GestureHandler, doubleTouchToZoomOutGestureHandler: GestureHandler, quickZoomGestureHandler: GestureHandler, - singleTapGestureHandler: GestureHandler) { + singleTapGestureHandler: GestureHandler, + animationLockoutGestureHandler: GestureHandler) { self.panGestureHandler = panGestureHandler self.pinchGestureHandler = pinchGestureHandler self.pitchGestureHandler = pitchGestureHandler @@ -104,6 +106,7 @@ public final class GestureManager: GestureHandlerDelegate { self.doubleTouchToZoomOutGestureHandler = doubleTouchToZoomOutGestureHandler self.quickZoomGestureHandler = quickZoomGestureHandler self.singleTapGestureHandler = singleTapGestureHandler + self.animationLockoutGestureHandler = animationLockoutGestureHandler panGestureHandler.delegate = self pinchGestureHandler.delegate = self diff --git a/Sources/MapboxMaps/Gestures/GestureRecognizers/AnyTouchGestureRecognizer.swift b/Sources/MapboxMaps/Gestures/GestureRecognizers/AnyTouchGestureRecognizer.swift new file mode 100644 index 00000000000..44ac350899b --- /dev/null +++ b/Sources/MapboxMaps/Gestures/GestureRecognizers/AnyTouchGestureRecognizer.swift @@ -0,0 +1,42 @@ +import UIKit.UIGestureRecognizerSubclass + +internal final class AnyTouchGestureRecognizer: UIGestureRecognizer { + + private var touches: Set = [] { + didSet { + if oldValue.isEmpty, !touches.isEmpty { + state = .began + } else if !oldValue.isEmpty, touches.isEmpty { + state = .ended + } + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + self.touches.formUnion(touches) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + self.touches.subtract(touches) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + self.touches.subtract(touches) + } + + override func reset() { + super.reset() + touches = [] + } + + override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } +} diff --git a/Sources/MapboxMaps/Location/LocationStyleProtocol.swift b/Sources/MapboxMaps/Location/LocationStyleProtocol.swift index b47ce5cf4ae..34cb1fbba9b 100644 --- a/Sources/MapboxMaps/Location/LocationStyleProtocol.swift +++ b/Sources/MapboxMaps/Location/LocationStyleProtocol.swift @@ -15,3 +15,5 @@ internal protocol LocationStyleProtocol: AnyObject { stretchY: [ImageStretches], content: ImageContent?) throws } + +extension Style: LocationStyleProtocol { } diff --git a/Sources/MapboxMaps/Ornaments/Compass/MapboxCompassOrnamentView.swift b/Sources/MapboxMaps/Ornaments/Compass/MapboxCompassOrnamentView.swift index 24920679f6c..ae022de71c2 100644 --- a/Sources/MapboxMaps/Ornaments/Compass/MapboxCompassOrnamentView.swift +++ b/Sources/MapboxMaps/Ornaments/Compass/MapboxCompassOrnamentView.swift @@ -69,7 +69,7 @@ internal class MapboxCompassOrnamentView: UIButton { fatalError("init(coder:) has not been implemented") } - @objc internal func didTap() { + @objc private func didTap() { tapAction?() } diff --git a/Sources/MapboxMaps/Ornaments/OrnamentSupportableView.swift b/Sources/MapboxMaps/Ornaments/OrnamentSupportableView.swift deleted file mode 100644 index 176b219263a..00000000000 --- a/Sources/MapboxMaps/Ornaments/OrnamentSupportableView.swift +++ /dev/null @@ -1,18 +0,0 @@ -import UIKit - -/// The `OrnamentSupportableView` protocol supports communication -/// from the MapboxMapsOrnaments module to the `MapView`. -internal protocol OrnamentSupportableView: UIView { - // View has been tapped - func tapped() - - // Compass ornament has been tapped - func compassTapped() - - func subscribeCameraChangeHandler(_ handler: @escaping (CameraState) -> Void) -} - -// Provides default implementation of OrnamentSupportableView methods. -internal extension OrnamentSupportableView { - func compassTapped() {} -} diff --git a/Sources/MapboxMaps/Ornaments/OrnamentsManager.swift b/Sources/MapboxMaps/Ornaments/OrnamentsManager.swift index f4149e9bcb9..5daee07e5bc 100644 --- a/Sources/MapboxMaps/Ornaments/OrnamentsManager.swift +++ b/Sources/MapboxMaps/Ornaments/OrnamentsManager.swift @@ -37,8 +37,10 @@ public class OrnamentsManager: NSObject { private var constraints = [NSLayoutConstraint]() - internal init(view: OrnamentSupportableView, - options: OrnamentOptions, + internal init(options: OrnamentOptions, + view: UIView, + mapboxMap: MapboxMapProtocol, + cameraAnimationsManager: CameraAnimationsManagerProtocol, infoButtonOrnamentDelegate: InfoButtonOrnamentDelegate) { self.options = options @@ -48,20 +50,27 @@ public class OrnamentsManager: NSObject { view.addSubview(logoView) // Scalebar View - scalebarView = MapboxScaleBarOrnamentView() + let scalebarView = MapboxScaleBarOrnamentView() // Check whether the scale bar is position on the right side of the map view. let scaleBarPosition = options.scaleBar.position scalebarView.isOnRight = scaleBarPosition == .bottomRight || scaleBarPosition == .topRight scalebarView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scalebarView) + self.scalebarView = scalebarView // Compass View - compassView = MapboxCompassOrnamentView(visibility: options.compass.visibility) + let compassView = MapboxCompassOrnamentView(visibility: options.compass.visibility) compassView.translatesAutoresizingMaskIntoConstraints = false - compassView.tapAction = { [weak view] in - view?.compassTapped() + compassView.tapAction = { + cameraAnimationsManager.cancelAnimations() + cameraAnimationsManager.ease( + to: CameraOptions(bearing: 0), + duration: 0.3, + curve: .easeOut, + completion: nil) } view.addSubview(compassView) + self.compassView = compassView // Info Button attributionButton = InfoButtonOrnament() @@ -75,7 +84,16 @@ public class OrnamentsManager: NSObject { updateOrnaments() // Subscribe to updates for scalebar and compass - view.subscribeCameraChangeHandler { [scalebarView, compassView] (cameraState) in + // MapboxMap should not be allowed to own a strong ref to compassView + // since compassView owns a tapAction that captures a strong ref to + // cameraAnimationsManager which has a strong ref to mapboxMap. + mapboxMap.onEvery(.cameraChanged) { [weak mapboxMap, weak scalebarView, weak compassView] _ in + guard let mapboxMap = mapboxMap, + let scalebarView = scalebarView, + let compassView = compassView else { + return + } + let cameraState = mapboxMap.cameraState // Update the scale bar scalebarView.metersPerPoint = Projection.metersPerPoint( @@ -84,7 +102,6 @@ public class OrnamentsManager: NSObject { // Update the compass compassView.currentBearing = Double(cameraState.bearing) - } } diff --git a/Tests/MapboxMapsTests/Foundation/Camera/BasicCameraAnimatorTests.swift b/Tests/MapboxMapsTests/Foundation/Camera/BasicCameraAnimatorTests.swift index 8c96edaae4f..594ef0592a6 100644 --- a/Tests/MapboxMapsTests/Foundation/Camera/BasicCameraAnimatorTests.swift +++ b/Tests/MapboxMapsTests/Foundation/Camera/BasicCameraAnimatorTests.swift @@ -57,6 +57,16 @@ final class BasicCameraAnimatorTests: XCTestCase { XCTAssertTrue(propertyAnimator.finishAnimationStub.invocations.isEmpty) } + func testIsReversed() { + animator.isReversed = true + + XCTAssertEqual(propertyAnimator.setIsReversedStub.parameters, [true]) + + animator.isReversed = false + + XCTAssertEqual(propertyAnimator.setIsReversedStub.parameters, [true, false]) + } + func testStartAndStopAnimation() { animator.addAnimations { (transition) in transition.zoom.toValue = cameraOptionsTestValue.zoom! @@ -66,6 +76,7 @@ final class BasicCameraAnimatorTests: XCTestCase { XCTAssertEqual(propertyAnimator.startAnimationStub.invocations.count, 1) XCTAssertEqual(propertyAnimator.addAnimationsStub.invocations.count, 1) + XCTAssertEqual(propertyAnimator.addCompletionStub.invocations.count, 1) XCTAssertNotNil(animator?.transition) XCTAssertEqual(animator?.transition?.toCameraOptions.zoom, 10) @@ -74,4 +85,40 @@ final class BasicCameraAnimatorTests: XCTestCase { XCTAssertEqual(propertyAnimator.finishAnimationStub.invocations.count, 1) XCTAssertEqual(propertyAnimator.finishAnimationStub.invocations.first?.parameters, .current) } + + func testAnimatorCompletionUpdatesCameraIfAnimationCompletedAtEnd() throws { + animator.addAnimations { (transition) in + transition.zoom.toValue = cameraOptionsTestValue.zoom! + } + animator.startAnimation() + let completion = try XCTUnwrap(propertyAnimator.addCompletionStub.parameters.first) + + completion(.end) + + XCTAssertEqual(mapboxMap.setCameraStub.invocations.count, 1) + } + + func testAnimatorCompletionUpdatesCameraIfAnimationCompletedAtStart() throws { + animator.addAnimations { (transition) in + transition.zoom.toValue = cameraOptionsTestValue.zoom! + } + animator.startAnimation() + let completion = try XCTUnwrap(propertyAnimator.addCompletionStub.parameters.first) + + completion(.start) + + XCTAssertEqual(mapboxMap.setCameraStub.invocations.count, 1) + } + + func testAnimatorCompletionDoesNotUpdateCameraIfAnimationCompletedAtCurrent() throws { + animator.addAnimations { (transition) in + transition.zoom.toValue = cameraOptionsTestValue.zoom! + } + animator.startAnimation() + let completion = try XCTUnwrap(propertyAnimator.addCompletionStub.parameters.first) + + completion(.current) + + XCTAssertEqual(mapboxMap.setCameraStub.invocations.count, 0) + } } diff --git a/Tests/MapboxMapsTests/Foundation/Camera/CameraAnimationsManagerTests.swift b/Tests/MapboxMapsTests/Foundation/Camera/CameraAnimationsManagerTests.swift new file mode 100644 index 00000000000..04fe89338ac --- /dev/null +++ b/Tests/MapboxMapsTests/Foundation/Camera/CameraAnimationsManagerTests.swift @@ -0,0 +1,59 @@ +import XCTest +@testable import MapboxMaps + +final class CameraAnimationsManagerTests: XCTestCase { + + var window: UIWindow! + var view: UIView! + var mapboxMap: MockMapboxMap! + var cameraAnimationsManager: CameraAnimationsManager! + + override func setUp() { + super.setUp() + window = UIWindow() + view = UIView() + window.addSubview(view) + window.makeKeyAndVisible() + mapboxMap = MockMapboxMap() + cameraAnimationsManager = CameraAnimationsManager( + cameraViewContainerView: view, + mapboxMap: mapboxMap) + } + + override func tearDown() { + cameraAnimationsManager = nil + mapboxMap = nil + view = nil + window.resignKey() + window = nil + super.tearDown() + } + + func testUpdateWithAnimationsEnabled() { + cameraAnimationsManager.animationsEnabled = true + let animator = cameraAnimationsManager.makeAnimator(duration: 1, curve: .linear) { (transition) in + transition.bearing.toValue = 180 + } + animator.startAnimation() + // flush animations to populate the underlying camera view's + // presentation layer so that update() will run + // also requires view to be in a window. + CATransaction.flush() + + cameraAnimationsManager.update() + + XCTAssertEqual(animator.state, .active) + XCTAssertEqual(mapboxMap.setCameraStub.invocations.count, 1) + } + + func testUpdateWithAnimationsDisabled() { + cameraAnimationsManager.animationsEnabled = false + let animator = cameraAnimationsManager.makeAnimator(duration: 1, curve: .linear) { _ in } + animator.startAnimation() + + cameraAnimationsManager.update() + + XCTAssertEqual(animator.state, .inactive) + XCTAssertEqual(mapboxMap.setCameraStub.invocations.count, 0) + } +} diff --git a/Tests/MapboxMapsTests/Foundation/Camera/MockMapboxMap.swift b/Tests/MapboxMapsTests/Foundation/Camera/MockMapboxMap.swift index deb1a2bf0ba..52103f6f3d4 100644 --- a/Tests/MapboxMapsTests/Foundation/Camera/MockMapboxMap.swift +++ b/Tests/MapboxMapsTests/Foundation/Camera/MockMapboxMap.swift @@ -2,6 +2,8 @@ final class MockMapboxMap: MapboxMapProtocol { + var size: CGSize = .zero + var cameraBounds = CameraBounds( bounds: CoordinateBounds( southwest: CLLocationCoordinate2D( @@ -53,4 +55,14 @@ final class MockMapboxMap: MapboxMapProtocol { func dragEnd() { dragEndStub.call() } + + struct OnEveryParams { + var eventType: MapEvents.EventKind + var handler: (Event) -> Void + } + let onEveryStub = Stub(defaultReturnValue: MockCancelable()) + @discardableResult + func onEvery(_ eventType: MapEvents.EventKind, handler: @escaping (Event) -> Void) -> Cancelable { + onEveryStub.call(with: OnEveryParams(eventType: eventType, handler: handler)) + } } diff --git a/Tests/MapboxMapsTests/Foundation/Camera/MockPropertyAnimator.swift b/Tests/MapboxMapsTests/Foundation/Camera/MockPropertyAnimator.swift index d4b2d1dc82d..1bb6c6256ed 100644 --- a/Tests/MapboxMapsTests/Foundation/Camera/MockPropertyAnimator.swift +++ b/Tests/MapboxMapsTests/Foundation/Camera/MockPropertyAnimator.swift @@ -2,6 +2,16 @@ import UIKit final class MockPropertyAnimator: UIViewPropertyAnimator { + let setIsReversedStub = Stub() + override var isReversed: Bool { + get { + fatalError("unimplemented") + } + set { + setIsReversedStub.call(with: newValue) + } + } + let stateStub = Stub(defaultReturnValue: .inactive) override var state: UIViewAnimatingState { stateStub.call() @@ -27,9 +37,9 @@ final class MockPropertyAnimator: UIViewPropertyAnimator { addAnimationsStub.call(with: animation) } - let addCompletionStub = Stub() + let addCompletionStub = Stub<(UIViewAnimatingPosition) -> Void, Void>() override func addCompletion(_ completion: @escaping (UIViewAnimatingPosition) -> Void) { - addCompletionStub.call() + addCompletionStub.call(with: completion) } struct ContinueAnimationParameters { diff --git a/Tests/MapboxMapsTests/Foundation/Camera/Mocks/MockCameraAnimationsManager.swift b/Tests/MapboxMapsTests/Foundation/Camera/Mocks/MockCameraAnimationsManager.swift index c7b2a464c34..b8cfdbc8a63 100644 --- a/Tests/MapboxMapsTests/Foundation/Camera/Mocks/MockCameraAnimationsManager.swift +++ b/Tests/MapboxMapsTests/Foundation/Camera/Mocks/MockCameraAnimationsManager.swift @@ -27,6 +27,8 @@ final class MockCameraAnimationsManager: CameraAnimationsManagerProtocol { cancelAnimationsStub.call() } + var animationsEnabled: Bool = true + struct DecelerateParameters { var location: CGPoint var velocity: CGPoint diff --git a/Tests/MapboxMapsTests/Foundation/MapboxMapTests.swift b/Tests/MapboxMapsTests/Foundation/MapboxMapTests.swift index 352a333f829..85d17f06764 100644 --- a/Tests/MapboxMapsTests/Foundation/MapboxMapTests.swift +++ b/Tests/MapboxMapsTests/Foundation/MapboxMapTests.swift @@ -144,8 +144,6 @@ final class MapboxMapTests: XCTestCase { let map = MapboxMap(mapClient: MockMapClient(), mapInitOptions: MapInitOptions()) // Compilation check only - _ = map as MapTransformDelegate - _ = map as CameraManagerProtocol _ = map as MapFeatureQueryable _ = map as ObservableProtocol _ = map as MapEventsObservable diff --git a/Tests/MapboxMapsTests/Foundation/Mocks/MockCancelable.swift b/Tests/MapboxMapsTests/Foundation/Mocks/MockCancelable.swift new file mode 100644 index 00000000000..96a83857d52 --- /dev/null +++ b/Tests/MapboxMapsTests/Foundation/Mocks/MockCancelable.swift @@ -0,0 +1,8 @@ +import MapboxMaps + +final class MockCancelable: Cancelable { + let cancelStub = Stub() + func cancel() { + cancelStub.call() + } +} diff --git a/Tests/MapboxMapsTests/Foundation/Mocks/MockMapViewDependencyProvider.swift b/Tests/MapboxMapsTests/Foundation/Mocks/MockMapViewDependencyProvider.swift index 5e24b75ef80..7b1eb8911fd 100644 --- a/Tests/MapboxMapsTests/Foundation/Mocks/MockMapViewDependencyProvider.swift +++ b/Tests/MapboxMapsTests/Foundation/Mocks/MockMapViewDependencyProvider.swift @@ -35,21 +35,17 @@ final class MockMapViewDependencyProvider: MapViewDependencyProviderProtocol { cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureManager { return GestureManager( panGestureHandler: MockPanGestureHandler( - gestureRecognizer: UIGestureRecognizer(), - mapboxMap: MockMapboxMap(), - cameraAnimationsManager: MockCameraAnimationsManager()), + gestureRecognizer: UIGestureRecognizer()), pinchGestureHandler: makeGestureHandler(), pitchGestureHandler: makeGestureHandler(), doubleTapToZoomInGestureHandler: makeGestureHandler(), doubleTouchToZoomOutGestureHandler: makeGestureHandler(), quickZoomGestureHandler: makeGestureHandler(), - singleTapGestureHandler: makeGestureHandler()) + singleTapGestureHandler: makeGestureHandler(), + animationLockoutGestureHandler: makeGestureHandler()) } func makeGestureHandler() -> GestureHandler { - return GestureHandler( - gestureRecognizer: UIGestureRecognizer(), - mapboxMap: MockMapboxMap(), - cameraAnimationsManager: MockCameraAnimationsManager()) + return GestureHandler(gestureRecognizer: UIGestureRecognizer()) } } diff --git a/Tests/MapboxMapsTests/Foundation/Projection/ProjectionTests.swift b/Tests/MapboxMapsTests/Foundation/Projection/ProjectionTests.swift index 505404ba3e3..a8535447aa8 100644 --- a/Tests/MapboxMapsTests/Foundation/Projection/ProjectionTests.swift +++ b/Tests/MapboxMapsTests/Foundation/Projection/ProjectionTests.swift @@ -1,18 +1,11 @@ import XCTest @testable import MapboxMaps -class ProjectionTests: XCTestCase { - - var projectorType: MapProjectionDelegate.Type! - - override func setUpWithError() throws { - try super.setUpWithError() - projectorType = Projection.self - } +final class ProjectionTests: XCTestCase { func testMetersPerPoint() { - let metersPerPointN = projectorType.metersPerPoint(for: 40.0, zoom: 16) - let metersPerPointS = projectorType.metersPerPoint(for: -40.0, zoom: 16) + let metersPerPointN = Projection.metersPerPoint(for: 40.0, zoom: 16) + let metersPerPointS = Projection.metersPerPoint(for: -40.0, zoom: 16) XCTAssertEqual(metersPerPointN, metersPerPointS) XCTAssertEqual(metersPerPointN, 0.91388626079034951, accuracy: 0.000001) @@ -28,10 +21,10 @@ class ProjectionTests: XCTestCase { ] let projectedMeters: [ProjectedMeters] = coords.map { - let meters = projectorType.projectedMeters(for: $0) + let meters = Projection.projectedMeters(for: $0) // Test round trip - let coordinate = projectorType.coordinate(for: meters) + let coordinate = Projection.coordinate(for: meters) XCTAssertEqual(coordinate.latitude, $0.latitude, accuracy: 0.000001) XCTAssertEqual(coordinate.longitude, $0.longitude, accuracy: 0.000001) return meters @@ -60,12 +53,12 @@ class ProjectionTests: XCTestCase { let zoomScale = pow(2, zoom) for coord in coords { - let mercator = projectorType.project(coord.0, zoomScale: zoomScale) + let mercator = Projection.project(coord.0, zoomScale: zoomScale) XCTAssertEqual(mercator.x, coord.1.x, accuracy: 0.0000001) XCTAssertEqual(mercator.y, coord.1.y, accuracy: 0.0000001) // Test round trip - let coordinate = projectorType.unproject(mercator, zoomScale: zoomScale) + let coordinate = Projection.unproject(mercator, zoomScale: zoomScale) XCTAssertEqual(coordinate.latitude, coord.0.latitude, accuracy: 0.0000001) XCTAssertEqual(coordinate.longitude, coord.0.longitude, accuracy: 0.0000001) } @@ -78,23 +71,23 @@ class ProjectionTests: XCTestCase { let zoom: CGFloat = 0 let zoomScale = pow(2, zoom) - let northMercator = projectorType.project(northPole, zoomScale: zoomScale) - let northPole2 = projectorType.unproject(northMercator, zoomScale: zoomScale) + let northMercator = Projection.project(northPole, zoomScale: zoomScale) + let northPole2 = Projection.unproject(northMercator, zoomScale: zoomScale) XCTAssertNotEqual(northPole2.latitude, northPole.latitude) XCTAssertEqual(northPole2.latitude, 85.051, accuracy: 0.001) - let southMercator = projectorType.project(southPole, zoomScale: zoomScale) - let southPole2 = projectorType.unproject(southMercator, zoomScale: zoomScale) + let southMercator = Projection.project(southPole, zoomScale: zoomScale) + let southPole2 = Projection.unproject(southMercator, zoomScale: zoomScale) XCTAssertNotEqual(southPole2.latitude, southPole.latitude) XCTAssertEqual(southPole2.latitude, -85.051, accuracy: 0.001) // Check clamping let northMax = CLLocationCoordinate2D(latitude: Projection.latitudeMax, longitude: 0) - let northMaxMercator = projectorType.project(northMax, zoomScale: zoomScale) + let northMaxMercator = Projection.project(northMax, zoomScale: zoomScale) XCTAssertEqual(northMercator.y, northMaxMercator.y, accuracy: 0.000001) let southMin = CLLocationCoordinate2D(latitude: Projection.latitudeMin, longitude: 0) - let southMinMercator = projectorType.project(southMin, zoomScale: zoomScale) + let southMinMercator = Projection.project(southMin, zoomScale: zoomScale) XCTAssertEqual(southMercator.y, southMinMercator.y, accuracy: 0.000001) } } diff --git a/Tests/MapboxMapsTests/Gestures/GestureHandlers/AnimationLockoutGestureHandlerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureHandlers/AnimationLockoutGestureHandlerTests.swift new file mode 100644 index 00000000000..13b0d025c31 --- /dev/null +++ b/Tests/MapboxMapsTests/Gestures/GestureHandlers/AnimationLockoutGestureHandlerTests.swift @@ -0,0 +1,56 @@ +import XCTest +@testable import MapboxMaps + +final class AnimationLockoutGestureHandlerTests: XCTestCase { + + var gestureRecognizer: MockGestureRecognizer! + var cameraAnimationsManager: MockCameraAnimationsManager! + var gestureHandler: AnimationLockoutGestureHandler! + + override func setUp() { + super.setUp() + gestureRecognizer = MockGestureRecognizer() + cameraAnimationsManager = MockCameraAnimationsManager() + gestureHandler = AnimationLockoutGestureHandler( + gestureRecognizer: gestureRecognizer, + cameraAnimationsManager: cameraAnimationsManager) + } + + override func tearDown() { + gestureHandler = nil + cameraAnimationsManager = nil + gestureRecognizer = nil + super.tearDown() + } + + func testInitialization() { + XCTAssertFalse(gestureRecognizer.cancelsTouchesInView) + } + + func testGestureBegan() { + cameraAnimationsManager.animationsEnabled = true + + gestureRecognizer.getStateStub.defaultReturnValue = .began + gestureRecognizer.sendActions() + + XCTAssertFalse(cameraAnimationsManager.animationsEnabled) + } + + func testGestureEnded() { + cameraAnimationsManager.animationsEnabled = false + + gestureRecognizer.getStateStub.defaultReturnValue = .ended + gestureRecognizer.sendActions() + + XCTAssertTrue(cameraAnimationsManager.animationsEnabled) + } + + func testGestureCancelled() { + cameraAnimationsManager.animationsEnabled = false + + gestureRecognizer.getStateStub.defaultReturnValue = .cancelled + gestureRecognizer.sendActions() + + XCTAssertTrue(cameraAnimationsManager.animationsEnabled) + } +} diff --git a/Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift index 4899df6e6cf..dbaa8f2cb29 100644 --- a/Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift +++ b/Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift @@ -51,7 +51,6 @@ final class PanGestureHandlerTests: XCTestCase { gestureRecognizer.sendActions() - XCTAssertEqual(cameraAnimationsManager.cancelAnimationsStub.invocations.count, 1) XCTAssertEqual(delegate.gestureBeganStub.parameters, [.pan]) } @@ -68,12 +67,10 @@ final class PanGestureHandlerTests: XCTestCase { initialTouchLocation, changedTouchLocation] panGestureHandler.panMode = panMode gestureRecognizer.sendActions() - cameraAnimationsManager.cancelAnimationsStub.reset() gestureRecognizer.getStateStub.defaultReturnValue = .changed gestureRecognizer.sendActions() - XCTAssertEqual(cameraAnimationsManager.cancelAnimationsStub.invocations.count, 1, line: line) XCTAssertEqual(mapboxMap.dragStartStub.parameters, [initialTouchLocation], line: line) XCTAssertEqual(mapboxMap.dragCameraOptionsStub.parameters, [ .init(from: initialTouchLocation, to: clampedTouchLocation)], line: line) @@ -157,7 +154,6 @@ final class PanGestureHandlerTests: XCTestCase { gestureRecognizer.sendActions() gestureRecognizer.getStateStub.defaultReturnValue = .changed gestureRecognizer.sendActions() - cameraAnimationsManager.cancelAnimationsStub.reset() mapboxMap.dragStartStub.reset() mapboxMap.dragCameraOptionsStub.reset() mapboxMap.dragEndStub.reset() @@ -253,14 +249,14 @@ final class PanGestureHandlerTests: XCTestCase { gestureRecognizer.sendActions() // changed 1 gestureRecognizer.sendActions() // ended 1 - // began 2 - triggers decelerate animation completion as a side effect of cancelling animations - let decelerateAnimationCompletion = try XCTUnwrap(cameraAnimationsManager.decelerateStub.parameters.first?.completion) - cameraAnimationsManager.cancelAnimationsStub.sideEffectQueue.append { _ in - decelerateAnimationCompletion() - } + // began 2 gestureRecognizer.sendActions() mapboxMap.setCameraStub.reset() + // cancel deceleration *after* the second gesture begins + let decelerateAnimationCompletion = try XCTUnwrap(cameraAnimationsManager.decelerateStub.parameters.first?.completion) + decelerateAnimationCompletion() + // changed 2 should still result in camera updates. a previous // implementation had a bug here where the cancellation of the animation // cleared the initial state for the subsequent gesture diff --git a/Tests/MapboxMapsTests/Gestures/GestureHandlers/PinchGestureHandlerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureHandlers/PinchGestureHandlerTests.swift index b5511d69cdc..923a931a1b0 100644 --- a/Tests/MapboxMapsTests/Gestures/GestureHandlers/PinchGestureHandlerTests.swift +++ b/Tests/MapboxMapsTests/Gestures/GestureHandlers/PinchGestureHandlerTests.swift @@ -5,7 +5,6 @@ final class PinchGestureHandlerTests: XCTestCase { var view: UIView! var gestureRecognizer: MockPinchGestureRecognizer! var mapboxMap: MockMapboxMap! - var cameraAnimationsManager: MockCameraAnimationsManager! var pinchGestureHandler: PinchGestureHandler! // swiftlint:disable:next weak_delegate var delegate: MockGestureHandlerDelegate! @@ -16,11 +15,9 @@ final class PinchGestureHandlerTests: XCTestCase { gestureRecognizer = MockPinchGestureRecognizer() view.addGestureRecognizer(gestureRecognizer) mapboxMap = MockMapboxMap() - cameraAnimationsManager = MockCameraAnimationsManager() pinchGestureHandler = PinchGestureHandler( gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + mapboxMap: mapboxMap) delegate = MockGestureHandlerDelegate() pinchGestureHandler.delegate = delegate } @@ -28,7 +25,6 @@ final class PinchGestureHandlerTests: XCTestCase { override func tearDown() { pinchGestureHandler = nil delegate = nil - cameraAnimationsManager = nil mapboxMap = nil gestureRecognizer = nil view = nil @@ -44,7 +40,6 @@ final class PinchGestureHandlerTests: XCTestCase { gestureRecognizer.sendActions() - XCTAssertEqual(cameraAnimationsManager.cancelAnimationsStub.invocations.count, 1) XCTAssertEqual(delegate.gestureBeganStub.parameters, [.pinch]) } @@ -67,9 +62,6 @@ final class PinchGestureHandlerTests: XCTestCase { CGPoint(x: 1, y: 1)] gestureRecognizer.getNumberOfTouchesStub.defaultReturnValue = 2 gestureRecognizer.sendActions() - // reset cancelAnimationsStub so we can verify - // that it's called when state is .changed - cameraAnimationsManager.cancelAnimationsStub.reset() gestureRecognizer.getStateStub.defaultReturnValue = .changed gestureRecognizer.locationStub.defaultReturnValue = changedPinchMidpoint // the new touch angle is 90° - that's 45° increase from the initial. @@ -82,8 +74,6 @@ final class PinchGestureHandlerTests: XCTestCase { gestureRecognizer.sendActions() - XCTAssertEqual(cameraAnimationsManager.cancelAnimationsStub.invocations.count, 1, - "Cancel animations was not called before commencing gesture processing") XCTAssertEqual(mapboxMap.setCameraStub.invocations.count, 3) guard mapboxMap.setCameraStub.invocations.count == 3 else { return diff --git a/Tests/MapboxMapsTests/Gestures/GestureHandlers/PitchGestureHandlerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureHandlers/PitchGestureHandlerTests.swift index c419010d963..fe866809220 100644 --- a/Tests/MapboxMapsTests/Gestures/GestureHandlers/PitchGestureHandlerTests.swift +++ b/Tests/MapboxMapsTests/Gestures/GestureHandlers/PitchGestureHandlerTests.swift @@ -5,7 +5,6 @@ final class PitchGestureHandlerTests: XCTestCase { var view: UIView! var gestureRecognizer: MockPanGestureRecognizer! var mapboxMap: MockMapboxMap! - var cameraAnimationsManager: MockCameraAnimationsManager! var pitchGestureHandler: PitchGestureHandler! // swiftlint:disable:next weak_delegate var delegate: MockGestureHandlerDelegate! @@ -16,11 +15,9 @@ final class PitchGestureHandlerTests: XCTestCase { gestureRecognizer = MockPanGestureRecognizer() view.addGestureRecognizer(gestureRecognizer) mapboxMap = MockMapboxMap() - cameraAnimationsManager = MockCameraAnimationsManager() pitchGestureHandler = PitchGestureHandler( gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + mapboxMap: mapboxMap) delegate = MockGestureHandlerDelegate() pitchGestureHandler.delegate = delegate gestureRecognizer.getNumberOfTouchesStub.defaultReturnValue = 2 @@ -29,7 +26,6 @@ final class PitchGestureHandlerTests: XCTestCase { override func tearDown() { delegate = nil pitchGestureHandler = nil - cameraAnimationsManager = nil mapboxMap = nil gestureRecognizer = nil view = nil @@ -78,7 +74,6 @@ final class PitchGestureHandlerTests: XCTestCase { gestureRecognizer.sendActions() XCTAssertEqual(delegate.gestureBeganStub.parameters, [.pitch]) - XCTAssertEqual(cameraAnimationsManager.cancelAnimationsStub.invocations.count, 1) XCTAssertEqual(gestureRecognizer.locationOfTouchStub.invocations.count, 0) } diff --git a/Tests/MapboxMapsTests/Gestures/GestureHandlers/QuickZoomGestureHandlerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureHandlers/QuickZoomGestureHandlerTests.swift index fca1ffb6102..69f7a2f4738 100644 --- a/Tests/MapboxMapsTests/Gestures/GestureHandlers/QuickZoomGestureHandlerTests.swift +++ b/Tests/MapboxMapsTests/Gestures/GestureHandlers/QuickZoomGestureHandlerTests.swift @@ -5,7 +5,6 @@ final class QuickZoomGestureHandlerTest: XCTestCase { var view: UIView! var gestureRecognizer: MockLongPressGestureRecognizer! var mapboxMap: MockMapboxMap! - var cameraAnimationsManager: MockCameraAnimationsManager! var quickZoomHandler: QuickZoomGestureHandler! // swiftlint:disable weak_delegate var delegate: MockGestureHandlerDelegate! @@ -16,11 +15,9 @@ final class QuickZoomGestureHandlerTest: XCTestCase { gestureRecognizer = MockLongPressGestureRecognizer() view.addGestureRecognizer(gestureRecognizer) mapboxMap = MockMapboxMap() - cameraAnimationsManager = MockCameraAnimationsManager() quickZoomHandler = QuickZoomGestureHandler( gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + mapboxMap: mapboxMap) delegate = MockGestureHandlerDelegate() quickZoomHandler.delegate = delegate } @@ -28,7 +25,6 @@ final class QuickZoomGestureHandlerTest: XCTestCase { override func tearDown() { delegate = nil quickZoomHandler = nil - cameraAnimationsManager = nil mapboxMap = nil gestureRecognizer = nil view = nil @@ -47,7 +43,6 @@ final class QuickZoomGestureHandlerTest: XCTestCase { gestureRecognizer.sendActions() XCTAssertEqual(delegate.gestureBeganStub.parameters, [.quickZoom]) - XCTAssertEqual(cameraAnimationsManager.cancelAnimationsStub.invocations.count, 1) } func testGestureChanged() { diff --git a/Tests/MapboxMapsTests/Gestures/GestureHandlers/SingleTapGestureHandlerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureHandlers/SingleTapGestureHandlerTests.swift index 5dcc4fda083..0b94f76f9f4 100644 --- a/Tests/MapboxMapsTests/Gestures/GestureHandlers/SingleTapGestureHandlerTests.swift +++ b/Tests/MapboxMapsTests/Gestures/GestureHandlers/SingleTapGestureHandlerTests.swift @@ -4,8 +4,6 @@ import XCTest final class SingleTapGestureHandlerTests: XCTestCase { var gestureRecognizer: MockTapGestureRecognizer! - var cameraAnimationsManager: MockCameraAnimationsManager! - var mapboxMap: MockMapboxMap! var gestureHandler: SingleTapGestureHandler! // swiftlint:disable:next weak_delegate var delegate: MockGestureHandlerDelegate! @@ -13,12 +11,7 @@ final class SingleTapGestureHandlerTests: XCTestCase { override func setUp() { super.setUp() gestureRecognizer = MockTapGestureRecognizer() - cameraAnimationsManager = MockCameraAnimationsManager() - mapboxMap = MockMapboxMap() - gestureHandler = SingleTapGestureHandler( - gestureRecognizer: gestureRecognizer, - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + gestureHandler = SingleTapGestureHandler(gestureRecognizer: gestureRecognizer) delegate = MockGestureHandlerDelegate() gestureHandler.delegate = delegate } @@ -26,8 +19,6 @@ final class SingleTapGestureHandlerTests: XCTestCase { override func tearDown() { delegate = nil gestureHandler = nil - mapboxMap = nil - cameraAnimationsManager = nil gestureRecognizer = nil super.tearDown() } @@ -43,6 +34,5 @@ final class SingleTapGestureHandlerTests: XCTestCase { gestureRecognizer.sendActions() XCTAssertEqual(delegate.gestureBeganStub.parameters, [.singleTap]) - XCTAssertEqual(cameraAnimationsManager.cancelAnimationsStub.invocations.count, 1) } } diff --git a/Tests/MapboxMapsTests/Gestures/GestureManagerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureManagerTests.swift index e9e158d2f0f..167501a65d3 100644 --- a/Tests/MapboxMapsTests/Gestures/GestureManagerTests.swift +++ b/Tests/MapboxMapsTests/Gestures/GestureManagerTests.swift @@ -12,6 +12,7 @@ final class GestureManagerTests: XCTestCase { var doubleTouchToZoomOutGestureHandler: GestureHandler! var quickZoomGestureHandler: GestureHandler! var singleTapGestureHandler: GestureHandler! + var animationLockoutGestureHandler: GestureHandler! var gestureManager: GestureManager! // swiftlint:disable:next weak_delegate var delegate: MockGestureManagerDelegate! @@ -21,15 +22,14 @@ final class GestureManagerTests: XCTestCase { mapboxMap = MockMapboxMap() cameraAnimationsManager = MockCameraAnimationsManager() panGestureHandler = MockPanGestureHandler( - gestureRecognizer: MockGestureRecognizer(), - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + gestureRecognizer: MockGestureRecognizer()) pinchGestureHandler = makeGestureHandler() pitchGestureHandler = makeGestureHandler() doubleTapToZoomInGestureHandler = makeGestureHandler() doubleTouchToZoomOutGestureHandler = makeGestureHandler() quickZoomGestureHandler = makeGestureHandler() singleTapGestureHandler = makeGestureHandler() + animationLockoutGestureHandler = makeGestureHandler() gestureManager = GestureManager( panGestureHandler: panGestureHandler, pinchGestureHandler: pinchGestureHandler, @@ -37,20 +37,22 @@ final class GestureManagerTests: XCTestCase { doubleTapToZoomInGestureHandler: doubleTapToZoomInGestureHandler, doubleTouchToZoomOutGestureHandler: doubleTouchToZoomOutGestureHandler, quickZoomGestureHandler: quickZoomGestureHandler, - singleTapGestureHandler: singleTapGestureHandler) - delegate = MockGestureManagerDelegate() + singleTapGestureHandler: singleTapGestureHandler, + animationLockoutGestureHandler: animationLockoutGestureHandler) + delegate = MockGestureManagerDelegate() gestureManager.delegate = delegate } override func tearDown() { delegate = nil gestureManager = nil + animationLockoutGestureHandler = nil + singleTapGestureHandler = nil quickZoomGestureHandler = nil doubleTouchToZoomOutGestureHandler = nil doubleTapToZoomInGestureHandler = nil pitchGestureHandler = nil pinchGestureHandler = nil - singleTapGestureHandler = nil panGestureHandler = nil cameraAnimationsManager = nil mapboxMap = nil @@ -58,10 +60,7 @@ final class GestureManagerTests: XCTestCase { } func makeGestureHandler() -> GestureHandler { - return GestureHandler( - gestureRecognizer: MockGestureRecognizer(), - mapboxMap: mapboxMap, - cameraAnimationsManager: cameraAnimationsManager) + return GestureHandler(gestureRecognizer: MockGestureRecognizer()) } func testPanGestureRecognizer() { diff --git a/Tests/MapboxMapsTests/Gestures/GestureRecognizers/AnyTouchGestureRecognizerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureRecognizers/AnyTouchGestureRecognizerTests.swift new file mode 100644 index 00000000000..28eab846751 --- /dev/null +++ b/Tests/MapboxMapsTests/Gestures/GestureRecognizers/AnyTouchGestureRecognizerTests.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import MapboxMaps + +final class AnyTouchGestureRecognizerTests: XCTestCase { + + var gestureRecognizer: AnyTouchGestureRecognizer! + + override func setUp() { + super.setUp() + gestureRecognizer = AnyTouchGestureRecognizer() + } + + override func tearDown() { + gestureRecognizer = nil + super.tearDown() + } + + func testCanBePreventedBy() { + XCTAssertFalse(gestureRecognizer.canBePrevented(by: MockGestureRecognizer())) + } + + func testCanPrevent() { + XCTAssertFalse(gestureRecognizer.canPrevent(MockGestureRecognizer())) + } + + func testTouchHandling() { + let touches = [UITouch(), UITouch(), UITouch()] + let event = UIEvent() + + gestureRecognizer.touchesBegan([touches[0]], with: event) + + XCTAssertEqual(gestureRecognizer.state, .began) + + gestureRecognizer.touchesBegan([touches[1]], with: event) + + XCTAssertEqual(gestureRecognizer.state, .began) + + gestureRecognizer.touchesCancelled([touches[0]], with: event) + + XCTAssertEqual(gestureRecognizer.state, .began) + + gestureRecognizer.touchesEnded([touches[1]], with: event) + + XCTAssertEqual(gestureRecognizer.state, .ended) + } +} diff --git a/Tests/MapboxMapsTests/Gestures/Mocks/MockGestureRecognizer.swift b/Tests/MapboxMapsTests/Gestures/Mocks/MockGestureRecognizer.swift index f875ba16a49..ffe4bdb9199 100644 --- a/Tests/MapboxMapsTests/Gestures/Mocks/MockGestureRecognizer.swift +++ b/Tests/MapboxMapsTests/Gestures/Mocks/MockGestureRecognizer.swift @@ -1,8 +1,34 @@ import UIKit final class MockGestureRecognizer: UIGestureRecognizer { + let getStateStub = Stub(defaultReturnValue: .possible) + override var state: UIGestureRecognizer.State { + get { + getStateStub.call() + } + // swiftlint:disable:next unused_setter_value + set { + fatalError("unimplemented") + } + } + let requireToFailStub = Stub() override func require(toFail otherGestureRecognizer: UIGestureRecognizer) { requireToFailStub.call(with: otherGestureRecognizer) } + + struct AddTargetParams { + var target: Any + var action: Selector + } + let addTargetStub = Stub() + override func addTarget(_ target: Any, action: Selector) { + addTargetStub.call(with: AddTargetParams(target: target, action: action)) + } + + func sendActions() { + for param in addTargetStub.parameters { + (param.target as? NSObject)?.perform(param.action, with: self) + } + } } diff --git a/Tests/MapboxMapsTests/MigrationGuide/MigrationGuideIntegrationTests.swift b/Tests/MapboxMapsTests/MigrationGuide/MigrationGuideIntegrationTests.swift index dd82e9dc9bc..4f5f358e575 100644 --- a/Tests/MapboxMapsTests/MigrationGuide/MigrationGuideIntegrationTests.swift +++ b/Tests/MapboxMapsTests/MigrationGuide/MigrationGuideIntegrationTests.swift @@ -186,7 +186,7 @@ class MigrationGuideIntegrationTests: IntegrationTestCase { // Configure map to show a scale bar mapView.ornaments.options.scaleBar.visibility = .visible - try mapView.mapboxMap.setCameraBounds(for: cameraBoundsOptions) + try mapView.mapboxMap.setCameraBounds(with: cameraBoundsOptions) //<-- } @@ -353,22 +353,17 @@ class MigrationGuideIntegrationTests: IntegrationTestCase { } } - func testMapCameraOptions() { + func testMapCameraOptions() throws { let mapView = MapView(frame: .zero) //--> let sw = CLLocationCoordinate2DMake(-12, -46) let ne = CLLocationCoordinate2DMake(2, 43) let restrictedBounds = CoordinateBounds(southwest: sw, northeast: ne) - mapView.camera.options = CameraBoundsOptions(bounds: restrictedBounds, - maxZoom: 15.0, - minZoom: 8.0) + try mapView.mapboxMap.setCameraBounds(with: CameraBoundsOptions(bounds: restrictedBounds, + maxZoom: 15.0, + minZoom: 8.0)) //<-- - - // Can also set directly, though this will trigger 3 didSets - mapView.camera.options.bounds = restrictedBounds - mapView.camera.options.minZoom = 8.0 - mapView.camera.options.maxZoom = 15.0 } func testGeoJSONSource() { diff --git a/Tests/MapboxMapsTests/Ornaments/Compass/CompassMapViewIntegrationTests.swift b/Tests/MapboxMapsTests/Ornaments/Compass/CompassMapViewIntegrationTests.swift index 9bd9c70ac6b..2d9cc89e021 100644 --- a/Tests/MapboxMapsTests/Ornaments/Compass/CompassMapViewIntegrationTests.swift +++ b/Tests/MapboxMapsTests/Ornaments/Compass/CompassMapViewIntegrationTests.swift @@ -1,68 +1,6 @@ import XCTest @testable import MapboxMaps -class CompassMapViewIntegrationTests: MapViewIntegrationTestCase { +final class CompassMapViewIntegrationTests: MapViewIntegrationTestCase { - func testUpdateMapBearing() throws { - let mapView = try XCTUnwrap(self.mapView, "Map view could not be found") - - let initialSubviews = mapView.subviews.filter { $0 is MapboxCompassOrnamentView } - - let compass = try XCTUnwrap(initialSubviews.first as? MapboxCompassOrnamentView, "The MapView should include a compass view as a subview") - - XCTAssertEqual(mapView.mapboxMap.cameraState.bearing, 0, "The map's initial bearing should be equal to 0") - XCTAssertTrue(compass.containerView.isHidden, "The compass should be hidden initially") - XCTAssertEqual(mapView.mapboxMap.cameraState.bearing, compass.currentBearing, "The map's initial bearing should be equal to the compass' bearing") - - mapView.mapboxMap.setCamera(to: CameraOptions(bearing: 30)) - XCTAssertFalse(compass.containerView.isHidden, "The compass should hidden when the bearing is 30.") - XCTAssertEqual(mapView.mapboxMap.cameraState.bearing, compass.currentBearing, "The map's bearing should be equal to the compass' current bearing") - XCTAssertEqual(mapView.mapboxMap.cameraState.bearing, 30, accuracy: 0.2, "The map's bearing should be equal to 30 with an accuracy of 0.2.") - - mapView.mapboxMap.setCamera(to: CameraOptions(bearing: 0)) - XCTAssertTrue(compass.containerView.isHidden) - XCTAssertEqual(mapView.mapboxMap.cameraState.bearing, 0) - } - - func testCompassTappedResetsToNorth() throws { - let mapView = try XCTUnwrap(self.mapView, "Map view could not be found") - - let initialSubviews = mapView.subviews.filter { $0 is MapboxCompassOrnamentView } - - let compass = try XCTUnwrap(initialSubviews.first as? MapboxCompassOrnamentView, "The MapView should include a compass view as a subview") - - mapView.mapboxMap.setCamera(to: CameraOptions(bearing: 30)) - - mapView.compassTapped() - - let mapExpectation = XCTestExpectation(description: "The bearing for the map should be 0 after a tap gesture") - let compassExpectation = XCTestExpectation(description: "The bearing for the compass should be 0 after a tap gesture.") - - mapView.mapboxMap.onEvery(.cameraChanged) { _ in - if mapView.mapboxMap.cameraState.bearing == 0 { - mapExpectation.fulfill() - } - - if compass.currentBearing.rounded() == 0 { - compassExpectation.fulfill() - } - } - - wait(for: [mapExpectation, compassExpectation], timeout: 5) - } - - func testCompassTappedCancelsAnimations() throws { - let mapView = try XCTUnwrap(self.mapView, "Map view could not be found") - - let animator = try XCTUnwrap( - mapView.camera.ease( - to: CameraOptions(center: CLLocationCoordinate2D(latitude: 1, longitude: 1)), - duration: 5) as? BasicCameraAnimator) - - XCTAssertEqual(animator.state, .active) - - mapView.compassTapped() - - XCTAssertEqual(animator.state, .inactive) - } } diff --git a/Tests/MapboxMapsTests/Ornaments/Mocks/MockInfoButtonOrnamentDelegate.swift b/Tests/MapboxMapsTests/Ornaments/Mocks/MockInfoButtonOrnamentDelegate.swift new file mode 100644 index 00000000000..02e4278e2ce --- /dev/null +++ b/Tests/MapboxMapsTests/Ornaments/Mocks/MockInfoButtonOrnamentDelegate.swift @@ -0,0 +1,6 @@ +@testable import MapboxMaps + +final class MockInfoButtonOrnamentDelegate: InfoButtonOrnamentDelegate { + func didTap(_ infoButtonOrnament: InfoButtonOrnament) { + } +} diff --git a/Tests/MapboxMapsTests/Ornaments/OrnamentManagerTests.swift b/Tests/MapboxMapsTests/Ornaments/OrnamentManagerTests.swift index ae612cfaa96..773e53c370d 100644 --- a/Tests/MapboxMapsTests/Ornaments/OrnamentManagerTests.swift +++ b/Tests/MapboxMapsTests/Ornaments/OrnamentManagerTests.swift @@ -1,37 +1,48 @@ import XCTest @testable import MapboxMaps -//swiftlint:disable explicit_acl explicit_top_level_acl -class OrnamentManagerTests: XCTestCase, AttributionDataSource { - - var ornamentSupportableView: OrnamentSupportableViewMock! +final class OrnamentManagerTests: XCTestCase { var options: OrnamentOptions! + var view: UIView! + var mapboxMap: MockMapboxMap! + var cameraAnimationsManager: MockCameraAnimationsManager! + // swiftlint:disable:next weak_delegate + var infoButtonOrnamentDelegate: MockInfoButtonOrnamentDelegate! var ornamentsManager: OrnamentsManager! - var attributionDialogManager: AttributionDialogManager! override func setUp() { - ornamentSupportableView = OrnamentSupportableViewMock(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - + super.setUp() options = OrnamentOptions() - attributionDialogManager = AttributionDialogManager(dataSource: self, delegate: nil) - ornamentsManager = OrnamentsManager(view: ornamentSupportableView, options: options, infoButtonOrnamentDelegate: attributionDialogManager) + view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + mapboxMap = MockMapboxMap() + cameraAnimationsManager = MockCameraAnimationsManager() + infoButtonOrnamentDelegate = MockInfoButtonOrnamentDelegate() + ornamentsManager = OrnamentsManager( + options: options, + view: view, + mapboxMap: mapboxMap, + cameraAnimationsManager: cameraAnimationsManager, + infoButtonOrnamentDelegate: infoButtonOrnamentDelegate) } override func tearDown() { - ornamentSupportableView = nil + ornamentsManager = nil + infoButtonOrnamentDelegate = nil + cameraAnimationsManager = nil + mapboxMap = nil + view = nil + options = nil + super.tearDown() } func testInitializer() { - XCTAssertEqual(ornamentSupportableView.subviews.count, 4) + XCTAssertEqual(view.subviews.count, 4) XCTAssertEqual(ornamentsManager.options.attributionButton.margins, options.attributionButton.margins) } - func testHidingOrnament() { - let initialSubviews = ornamentSupportableView.subviews.filter { $0.isKind(of: MapboxCompassOrnamentView.self) } - guard let isInitialCompassHidden = initialSubviews.first?.isHidden else { - XCTFail("Failed to access the compass' isHidden property.") - return - } + func testHidingOrnament() throws { + let compass = try XCTUnwrap(view.subviews.compactMap { $0 as? MapboxCompassOrnamentView }.first) + let initialCompassIsHidden = compass.isHidden XCTAssertEqual(options.compass.visibility, .adaptive) options.compass.visibility = .hidden @@ -40,19 +51,14 @@ class OrnamentManagerTests: XCTestCase, AttributionDataSource { XCTAssertEqual(options.compass.visibility, .hidden) - let updatedSubviews = ornamentSupportableView.subviews.filter { $0.isKind(of: MapboxCompassOrnamentView.self) } - guard let isUpdatedCompassHidden = updatedSubviews.first?.isHidden else { - XCTFail("Failed to access the updated compass' isHidden property.") - return - } + let updatedCompass = try XCTUnwrap(view.subviews.compactMap { $0 as? MapboxCompassOrnamentView }.first) - XCTAssertNotEqual(isInitialCompassHidden, isUpdatedCompassHidden) + XCTAssertNotEqual(initialCompassIsHidden, updatedCompass.isHidden) } func testScaleBarOnRight() throws { - let initialSubviews = ornamentSupportableView.subviews.filter { $0 is MapboxScaleBarOrnamentView } + let scaleBar = try XCTUnwrap(view.subviews.compactMap { $0 as? MapboxScaleBarOrnamentView }.first) - let scaleBar = try XCTUnwrap(initialSubviews.first as? MapboxScaleBarOrnamentView, "The ornament supportable map view should include a scale bar") XCTAssertFalse(scaleBar.isOnRight, "The default scale bar should be on the left initially.") ornamentsManager.options.scaleBar.position = .topRight @@ -65,7 +71,46 @@ class OrnamentManagerTests: XCTestCase, AttributionDataSource { XCTAssertTrue(scaleBar.isOnRight, "The scale bar should be on the right after the position has been updated to bottomRight.") } - func attributions() -> [Attribution] { - return [ Attribution(title: "This is a test", url: URL(string: "https://example.com/this-is-a-test")!)] + func testCompassTappedResetsToNorth() throws { + let compass = try XCTUnwrap(view.subviews.compactMap { $0 as? MapboxCompassOrnamentView }.first) + + // `sendActions(for:)` doesn't work if you're not running in a host app, + // so we use this workaround + for target in compass.allTargets { + for action in compass.actions(forTarget: target, forControlEvent: .touchUpInside) ?? [] { + (target as NSObject).perform(Selector(action)) + } + } + + XCTAssertEqual(cameraAnimationsManager.cancelAnimationsStub.invocations.count, 1) + XCTAssertEqual(cameraAnimationsManager.easeToStub.invocations.count, 1) + XCTAssertEqual(cameraAnimationsManager.easeToStub.parameters.first?.camera, CameraOptions(bearing: 0)) + XCTAssertEqual(cameraAnimationsManager.easeToStub.parameters.first?.duration, 0.3) + XCTAssertEqual(cameraAnimationsManager.easeToStub.parameters.first?.curve, .easeOut) + XCTAssertNil(cameraAnimationsManager.easeToStub.parameters.first?.completion) + } + + func testUpdateMapBearing() throws { + let compass = try XCTUnwrap(view.subviews.compactMap { $0 as? MapboxCompassOrnamentView }.first) + + XCTAssertEqual(mapboxMap.onEveryStub.invocations.count, 1) + XCTAssertEqual(mapboxMap.onEveryStub.parameters.first?.eventType, .cameraChanged) + let onEveryCameraChangeHandler = try XCTUnwrap(mapboxMap.onEveryStub.parameters.first?.handler) + + XCTAssertEqual(mapboxMap.cameraState.bearing, 0) + XCTAssertTrue(compass.containerView.isHidden, "The compass should be hidden initially") + XCTAssertEqual(mapboxMap.cameraState.bearing, compass.currentBearing) + + mapboxMap.cameraState.bearing += .random(in: (.leastNonzeroMagnitude)..<360) + onEveryCameraChangeHandler(Event(type: "", data: "")) + + XCTAssertFalse(compass.containerView.isHidden, "The compass should not be hidden when the bearing is non-zero.") + XCTAssertEqual(mapboxMap.cameraState.bearing, compass.currentBearing) + + mapboxMap.cameraState.bearing = 0 + onEveryCameraChangeHandler(Event(type: "", data: "")) + + XCTAssertTrue(compass.containerView.isHidden) + XCTAssertEqual(mapboxMap.cameraState.bearing, compass.currentBearing) } } diff --git a/Tests/MapboxMapsTests/Ornaments/OrnamentSupportableViewMock.swift b/Tests/MapboxMapsTests/Ornaments/OrnamentSupportableViewMock.swift deleted file mode 100644 index 39993787e02..00000000000 --- a/Tests/MapboxMapsTests/Ornaments/OrnamentSupportableViewMock.swift +++ /dev/null @@ -1,17 +0,0 @@ -import UIKit -@testable import MapboxMaps - -//swiftlint:disable explicit_acl explicit_top_level_acl -// Mock class that flags true when `OrnamentSupportableView` protocol methods have been called on it -class OrnamentSupportableViewMock: UIView, OrnamentSupportableView { - - func subscribeCameraChangeHandler(_ handler: @escaping (CameraState) -> Void) { - - } - - var tapCalled: Bool = false - - func tapped() { - tapCalled = true - } -}