Skip to content

Commit

Permalink
Support annotation view lookup by id. (#1512)
Browse files Browse the repository at this point in the history
- Allow adding annotation view with a custom id, that can be used later to look it up.
  • Loading branch information
maios committed Aug 9, 2022
1 parent 3813c38 commit d0b4ab6
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Mapbox welcomes participation and contributions from everyone.
* Fix NaN latitude crash rarely happening in `CameraAnimationsManager.fly(to:duration:completion)`. ([#1485](https://github.com/mapbox/mapbox-maps-ios/pull/1485))
* Fix `Style.updateLayer(withId:type:update)` so resetting a layer's properties should work. ([#1476](https://github.com/mapbox/mapbox-maps-ios/pull/1476))
* Add support for sonar-like pulsing animation around 2D puck. ([#1513](https://github.com/mapbox/mapbox-maps-ios/pull/1513))
* Support view annotation lookup by an identifier. ([#1512](https://github.com/mapbox/mapbox-maps-ios/pull/1512))

## 10.7.0 - July 28, 2022

Expand Down
48 changes: 42 additions & 6 deletions Sources/MapboxMaps/Annotations/ViewAnnotationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public final class ViewAnnotationManager {

private let containerView: UIView
private let mapboxMap: MapboxMapProtocol
private var currentViewId = 0
private var viewsById: [String: UIView] = [:]
private var idsByView: [UIView: String] = [:]
private var expectedHiddenByView: [UIView: Bool] = [:]
Expand Down Expand Up @@ -101,15 +100,46 @@ public final class ViewAnnotationManager {
/// supplied ``ViewAnnotationOptions/associatedFeatureId`` is already used by another annotation view
/// - ``MapError``: errors during insertion
public func add(_ view: UIView, options: ViewAnnotationOptions) throws {
guard idsByView[view] == nil else {
try add(view, id: nil, options: options)
}

/// Add a `UIView` instance which will be displayed as an annotation.
/// View dimensions will be taken as width / height from the bounds of the view
/// unless they are not specified explicitly with ``ViewAnnotationOptions/width`` and ``ViewAnnotationOptions/height``.
///
/// Annotation `options` must include Geometry where we want to bind our view annotation.
///
/// Width and height could be specified explicitly but better idea will be not specifying them
/// as they will be calculated automatically based on view layout.
///
/// > Important: The annotation view to be added should have `UIView.transform` property set to `.identity`.
/// Providing a transformed view can result in annotation views being misplaced, overlapped and other layout artifacts.
///
/// - Note: Use ``ViewAnnotationManager/update(_:options:)`` for changing the visibilty of the view, instead
/// of `UIView.isHidden` so that it is removed from the layout calculation.
///
/// - Parameters:
/// - view: `UIView` to be added to the map
/// - id: The unique string for the `view`.
/// - options: ``ViewAnnotationOptions`` to control the layout and visibility of the annotation
///
/// - Throws:
/// - ``ViewAnnotationManagerError/viewIsAlreadyAdded`` if the supplied view is already added as an annotation, or there is an existing annotation view with the same `id`.
/// - ``ViewAnnotationManagerError/geometryFieldMissing`` if options did not include geometry
/// - ``ViewAnnotationManagerError/associatedFeatureIdIsAlreadyInUse`` if the
/// supplied ``ViewAnnotationOptions/associatedFeatureId`` is already used by another annotation view
/// - ``MapError``: errors during insertion
public func add(_ view: UIView, id: String?, options: ViewAnnotationOptions) throws {
guard idsByView[view] == nil && id.flatMap(view(forId:)) == nil else {
throw ViewAnnotationManagerError.viewIsAlreadyAdded
}
guard options.geometry != nil else {
throw ViewAnnotationManagerError.geometryFieldMissing
}
if let associatedFeatureId = options.associatedFeatureId, viewsByFeatureIds[associatedFeatureId] != nil {
guard options.associatedFeatureId.flatMap(view(forFeatureId:)) == nil else {
throw ViewAnnotationManagerError.associatedFeatureIdIsAlreadyInUse
}

var creationOptions = options
if creationOptions.width == nil {
creationOptions.width = view.bounds.size.width
Expand All @@ -118,11 +148,9 @@ public final class ViewAnnotationManager {
creationOptions.height = view.bounds.size.height
}

let id = String(currentViewId)
currentViewId += 1

view.translatesAutoresizingMaskIntoConstraints = false

let id = id ?? UUID().uuidString
try mapboxMap.addViewAnnotation(withId: id, options: creationOptions)
viewsById[id] = view
idsByView[view] = id
Expand Down Expand Up @@ -201,6 +229,14 @@ public final class ViewAnnotationManager {
}
}

/// Find view annotation by the given `id`.
///
/// - Parameter id: The identifier of the view set in ``ViewAnnotationManager/add(_:id:options:)``.
/// - Returns: `UIView` if view was found, otherwise `nil`.
public func view(forId id: String) -> UIView? {
viewsById[id]
}

/// Find `UIView` by feature id if it was specified as part of ``ViewAnnotationOptions/associatedFeatureId``.
///
/// - Parameters:
Expand Down
46 changes: 32 additions & 14 deletions Tests/MapboxMapsTests/Annotations/ViewAnnotationManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,30 @@ final class ViewAnnotationManagerTests: XCTestCase {
let testView = UIView()
let geometry = Point(CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0))
let options = ViewAnnotationOptions(geometry: geometry, width: 0.0, height: 0.0)
try? manager.add(testView, options: options)

XCTAssertNoThrow(try manager.add(testView, id: "test-id", options: options))
XCTAssertEqual(mapboxMap.addViewAnnotationStub.invocations.count, 1)
XCTAssertEqual(mapboxMap.addViewAnnotationStub.invocations.last?.parameters, .init(id: "0", options: options))
XCTAssertEqual(mapboxMap.addViewAnnotationStub.invocations.last?.parameters, .init(id: "test-id", options: options))
XCTAssertEqual(testView.superview, container)
XCTAssertEqual(container.subviews.count, 1)

// Should fail if the view is already added
XCTAssertThrowsError(try manager.add(testView, options: ViewAnnotationOptions(geometry: geometry)))
XCTAssertNoThrow(try manager.add(UIView(), options: options))
XCTAssertNotNil(UUID(uuidString: mapboxMap.addViewAnnotationStub.invocations.last!.parameters.id), "Generated annotation view ID must be a valid UUID")
}

func testAddExistingView() {
let testView = UIView()
let options = ViewAnnotationOptions(geometry: Point(.init(latitude: 0.0, longitude: 0.0)))

// Adding views should increment keys
XCTAssertEqual(mapboxMap.addViewAnnotationStub.invocations.last?.parameters.id, "0")
try? manager.add(UIView(), options: ViewAnnotationOptions(geometry: geometry))
XCTAssertEqual(mapboxMap.addViewAnnotationStub.invocations.last?.parameters.id, "1")
XCTAssertNoThrow(try manager.add(testView, options: options))
XCTAssertThrowsError(try manager.add(testView, options: options))
}

func testAddViewWithExistingID() {
let options = ViewAnnotationOptions(geometry: Point(.init(latitude: 0.0, longitude: 0.0)))

XCTAssertNoThrow(try manager.add(UIView(), id: "test-id", options: options))
XCTAssertThrowsError(try manager.add(UIView(), id: "test-id", options: options))
}

func testAddViewReadSize() {
Expand Down Expand Up @@ -95,6 +106,13 @@ final class ViewAnnotationManagerTests: XCTestCase {
XCTAssertTrue(mapboxMap.removeViewAnnotationStub.invocations.isEmpty)
}

func testGetViewByID() {
let testView = addTestAnnotationView(id: "test-id")

XCTAssertEqual(manager.view(forId: "test-id"), testView)
XCTAssertNotEqual(manager.view(forId: "other-id"), testView)
}

func testAssociatedFeatureIdIsAlreadyInUse() {
let testView = UIView()
let geometry = Point(CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0))
Expand Down Expand Up @@ -256,12 +274,12 @@ final class ViewAnnotationManagerTests: XCTestCase {
}

func testPlacementPosition() {
let annotationView = addTestAnnotationView()
let annotationView = addTestAnnotationView(id: "test-id")
XCTAssertEqual(container.subviews.count, 1)
XCTAssertEqual(annotationView.frame, CGRect.zero)

manager.onViewAnnotationPositionsUpdate(forPositions: [ViewAnnotationPositionDescriptor(
identifier: "0",
identifier: "test-id",
width: 100,
height: 50,
leftTopCoordinate: CGPoint(x: 150.0, y: 200.0)
Expand All @@ -271,7 +289,7 @@ final class ViewAnnotationManagerTests: XCTestCase {
}

func testPlacementHideMissingAnnotations() {
let annotationViewA = addTestAnnotationView()
let annotationViewA = addTestAnnotationView(id: "test-id")
let annotationViewB = addTestAnnotationView()
let annotationViewC = addTestAnnotationView()

Expand All @@ -280,7 +298,7 @@ final class ViewAnnotationManagerTests: XCTestCase {
XCTAssertFalse(annotationViewC.isHidden)

manager.onViewAnnotationPositionsUpdate(forPositions: [ViewAnnotationPositionDescriptor(
identifier: "0",
identifier: "test-id",
width: 100,
height: 50,
leftTopCoordinate: CGPoint(x: 150.0, y: 200.0)
Expand Down Expand Up @@ -357,11 +375,11 @@ final class ViewAnnotationManagerTests: XCTestCase {

// MARK: - Helper functions

private func addTestAnnotationView(featureId: String? = nil) -> UIView {
private func addTestAnnotationView(id: String? = nil, featureId: String? = nil) -> UIView {
let geometry = Point(CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0))
let options = ViewAnnotationOptions(geometry: geometry, associatedFeatureId: featureId)
let view = UIView()
try! manager.add(view, options: options)
try! manager.add(view, id: id, options: options)
mapboxMap.optionsForViewAnnotationWithIdStub.defaultReturnValue = options
return view
}
Expand Down

0 comments on commit d0b4ab6

Please sign in to comment.