Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support annotation view lookup by id. #1512

Merged
merged 4 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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