Skip to content
Draft
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
163 changes: 163 additions & 0 deletions ios/maps/ExampleAnchorUsage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* Copyright (c) 2021 Ubique Innovation AG <https://www.ubique.ch>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

import UIKit
import MapCore

/// Example usage of MCMapAnchor - demonstrates the API as specified in the issue
class ExampleAnchorUsage: UIViewController {

private var mapView: MCMapView!

override func viewDidLoad() {
super.viewDidLoad()
setupMapView()
demonstrateAnchorAPI()
}

private func setupMapView() {
// Create map view with basic configuration
mapView = MCMapView()
mapView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mapView)

NSLayoutConstraint.activate([
mapView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}

private func demonstrateAnchorAPI() {
// Exact example from the issue description:

// Create an anchor for a specific coordinate
let zurichCoord = MCCoord(lat: 47.3769, lon: 8.5417)
let anchor = mapView.createAnchor(for: zurichCoord)

// Position any UIView relative to the map coordinate using AutoLayout
let pinView = createPinView(color: .red)
NSLayoutConstraint.activate([
pinView.centerXAnchor.constraint(equalTo: anchor.centerXAnchor),
pinView.centerYAnchor.constraint(equalTo: anchor.centerYAnchor)
])

// The pin automatically stays positioned at the coordinate as users interact with the map

// Additional examples showing different anchor types and dynamic updates:
demonstrateMultipleAnchors()
demonstrateDynamicUpdates()
demonstrateAnchorManagement()
}

private func demonstrateMultipleAnchors() {
// Create multiple anchors with different positioning
let parisCoord = MCCoord(lat: 48.8566, lon: 2.3522)
let parisAnchor = mapView.createAnchor(for: parisCoord)

let parisPin = createPinView(color: .blue)
NSLayoutConstraint.activate([
parisPin.leadingAnchor.constraint(equalTo: parisAnchor.trailingAnchor, constant: 10),
parisPin.topAnchor.constraint(equalTo: parisAnchor.bottomAnchor, constant: -5)
])

// London with different anchor positioning
let londonCoord = MCCoord(lat: 51.5074, lon: -0.1278)
let londonAnchor = mapView.createAnchor(for: londonCoord)

let londonLabel = createLabel(text: "London")
NSLayoutConstraint.activate([
londonLabel.bottomAnchor.constraint(equalTo: londonAnchor.topAnchor, constant: -5),
londonLabel.centerXAnchor.constraint(equalTo: londonAnchor.centerXAnchor)
])
}

private func demonstrateDynamicUpdates() {
// Create an anchor that will move dynamically
let movingCoord = MCCoord(lat: 46.9481, lon: 7.4474) // Bern
let movingAnchor = mapView.createAnchor(for: movingCoord)

let movingPin = createPinView(color: .green)
NSLayoutConstraint.activate([
movingPin.centerXAnchor.constraint(equalTo: movingAnchor.centerXAnchor),
movingPin.centerYAnchor.constraint(equalTo: movingAnchor.centerYAnchor)
])

// After 2 seconds, move the anchor to a new location
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// Update the coordinate - this should automatically move the pin
movingAnchor.coordinate = MCCoord(lat: 46.5197, lon: 6.6323) // Geneva
print("Moved anchor from Bern to Geneva")
}
}

private func demonstrateAnchorManagement() {
// Show anchor lifecycle management
print("Total anchors before: \(mapView.activeAnchors.count)")

// Create a temporary anchor
let tempCoord = MCCoord(lat: 47.0502, lon: 8.3093)
let tempAnchor = mapView.createAnchor(for: tempCoord)
print("Total anchors after creating temp: \(mapView.activeAnchors.count)")

// Remove it after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.mapView.removeAnchor(tempAnchor)
print("Total anchors after removing temp: \(self.mapView.activeAnchors.count)")
}

// After 5 seconds, remove all anchors
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
print("Removing all anchors...")
self.mapView.removeAllAnchors()
print("Total anchors after removing all: \(self.mapView.activeAnchors.count)")
}
}

// MARK: - Helper Methods

private func createPinView(color: UIColor) -> UIView {
let pin = UIView()
pin.backgroundColor = color
pin.layer.cornerRadius = 10
pin.layer.borderWidth = 2
pin.layer.borderColor = UIColor.white.cgColor
pin.translatesAutoresizingMaskIntoConstraints = false
mapView.addSubview(pin)

NSLayoutConstraint.activate([
pin.widthAnchor.constraint(equalToConstant: 20),
pin.heightAnchor.constraint(equalToConstant: 20)
])

return pin
}

private func createLabel(text: String) -> UILabel {
let label = UILabel()
label.text = text
label.backgroundColor = UIColor.black.withAlphaComponent(0.7)
label.textColor = .white
label.font = UIFont.systemFont(ofSize: 12, weight: .medium)
label.textAlignment = .center
label.layer.cornerRadius = 4
label.clipsToBounds = true
label.translatesAutoresizingMaskIntoConstraints = false
mapView.addSubview(label)

NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalToConstant: 24),
label.widthAnchor.constraint(greaterThanOrEqualToConstant: 60)
])

return label
}
}
100 changes: 100 additions & 0 deletions ios/maps/IMPLEMENTATION_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# MCMapAnchor Implementation Summary

This document summarizes the implementation of the MCMapAnchor feature as requested in issue #855.

## Files Created/Modified

### New Files:
1. **`ios/maps/MCMapAnchor.swift`** - Core anchor class implementation
2. **`ios/maps/ExampleAnchorUsage.swift`** - Complete usage examples
3. **`ios/maps/MCMapAnchor_README.md`** - Documentation and usage guide

### Modified Files:
1. **`ios/maps/MCMapView.swift`** - Added anchor management extensions

## Implementation Checklist vs Requirements

### ✅ MCMapAnchor Class Features
- [x] **Modifiable Coordinates**: `coordinate` property can be updated at any time
- [x] **AutoLayout Integration**: Provides all required NSLayoutAnchor properties:
- `centerXAnchor`
- `centerYAnchor`
- `leadingAnchor`
- `trailingAnchor`
- `topAnchor`
- `bottomAnchor`
- [x] **Automatic Updates**: Listens to camera changes via `MCMapCameraListenerInterface`
- [x] **Efficient Constraint Management**: Proper internal positioning constraints with cleanup

### ✅ MCMapView Extensions
- [x] **`createAnchor(for coordinate: MCCoord)`** - Creates new anchors
- [x] **`removeAnchor(_:)`** - Removes specific anchor
- [x] **`removeAllAnchors()`** - Removes all anchors
- [x] **`activeAnchors`** - Access to all current anchors
- [x] **Camera listener integration** - Updates all anchors on map interactions

### ✅ Implementation Details
- [x] **Hidden internal UIView** that participates in the layout system
- [x] **Coordinate conversion** using `camera.screenPosFromCoord()`
- [x] **Efficient updates** - Only batches updates when camera actually changes
- [x] **Memory Management** - Uses `CameraListenerWrapper` with weak references
- [x] **Proper cleanup** - Constraint cleanup on anchor removal and MapView deallocation

### ✅ Usage Example (Exact from Issue)
```swift
// Create an anchor for a specific coordinate
let zurichCoord = MCCoord(lat: 47.3769, lon: 8.5417)
let anchor = mapView.createAnchor(for: zurichCoord)

// Position any UIView relative to the map coordinate using AutoLayout
let pinView = UIView()
NSLayoutConstraint.activate([
pinView.centerXAnchor.constraint(equalTo: anchor.centerXAnchor),
pinView.centerYAnchor.constraint(equalTo: anchor.centerYAnchor)
])

// The pin automatically stays positioned at the coordinate as users interact with the map
```

## Technical Architecture

### Camera Listener Integration
- `CameraListenerWrapper` implements `MCMapCameraListenerInterface`
- Responds to `onVisibleBoundsChanged`, `onRotationChanged`, and `onCameraChange`
- Uses weak references to prevent retain cycles
- Only active when anchors exist (added/removed as needed)

### Memory Management
- Associated objects for anchor storage on MCMapView instances
- Weak references from anchors to map view
- Automatic cleanup in MCMapView.deinit()
- Proper constraint deactivation and view removal

### Performance Optimizations
- Camera listener only registered when anchors exist
- Batched updates for all anchors on camera changes
- Thread-safe coordinate updates with main queue layout calls
- Minimal overhead when no anchors are present

## Testing and Validation

### Syntax Validation
- ✅ All Swift files pass `swiftc -parse` validation
- ✅ Proper import statements and dependencies
- ✅ Consistent code style matching existing codebase

### Example Usage
- ✅ Comprehensive example in `ExampleAnchorUsage.swift`
- ✅ Demonstrates all API methods and features
- ✅ Shows advanced usage patterns and edge cases
- ✅ Matches exact API from issue requirements

### Documentation
- ✅ Complete usage guide with examples
- ✅ API documentation in code comments
- ✅ Performance considerations documented
- ✅ Implementation details explained

## Result

The implementation fully satisfies all requirements from issue #855 and provides a robust, efficient, and easy-to-use anchors system for iOS MCMapView that enables positioning UIKit views relative to map coordinates using standard AutoLayout constraints.
113 changes: 113 additions & 0 deletions ios/maps/MCMapAnchor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2021 Ubique Innovation AG <https://www.ubique.ch>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

import UIKit
@_exported import MapCoreSharedModule

/// A layout anchor that maintains its position relative to a map coordinate using AutoLayout constraints.
/// The anchor automatically updates its screen position when the map camera changes.
open class MCMapAnchor: NSObject {

// MARK: - Public Properties

/// The map coordinate this anchor is tied to
public var coordinate: MCCoord {
didSet {
updatePosition()
}
}

/// AutoLayout anchors for positioning views relative to the map coordinate
public var centerXAnchor: NSLayoutXAxisAnchor { internalView.centerXAnchor }
public var centerYAnchor: NSLayoutYAxisAnchor { internalView.centerYAnchor }
public var leadingAnchor: NSLayoutXAxisAnchor { internalView.leadingAnchor }
public var trailingAnchor: NSLayoutXAxisAnchor { internalView.trailingAnchor }
public var topAnchor: NSLayoutYAxisAnchor { internalView.topAnchor }
public var bottomAnchor: NSLayoutYAxisAnchor { internalView.bottomAnchor }

// MARK: - Private Properties

/// Hidden internal view that participates in the layout system
private let internalView: UIView

/// Weak reference to the map view to prevent retain cycles
private weak var mapView: MCMapView?

/// Internal constraints for positioning the anchor view
private var centerXConstraint: NSLayoutConstraint?
private var centerYConstraint: NSLayoutConstraint?

// MARK: - Initialization

/// Creates a new map anchor tied to the specified coordinate
/// - Parameters:
/// - coordinate: The map coordinate this anchor should track
/// - mapView: The map view this anchor belongs to
internal init(coordinate: MCCoord, mapView: MCMapView) {
self.coordinate = coordinate
self.mapView = mapView
self.internalView = UIView()

super.init()

setupInternalView()
updatePosition()
}

// MARK: - Private Methods

private func setupInternalView() {
guard let mapView = mapView else { return }

// Configure the internal view
internalView.isHidden = true
internalView.isUserInteractionEnabled = false
internalView.translatesAutoresizingMaskIntoConstraints = false

// Add to map view
mapView.addSubview(internalView)

// Create initial constraints
centerXConstraint = internalView.centerXAnchor.constraint(equalTo: mapView.leadingAnchor)
centerYConstraint = internalView.centerYAnchor.constraint(equalTo: mapView.topAnchor)

centerXConstraint?.isActive = true
centerYConstraint?.isActive = true
}

/// Updates the screen position based on the current coordinate and camera state
internal func updatePosition() {
guard let mapView = mapView else { return }

// Convert coordinate to screen position using the camera
let screenPosition = mapView.camera.screenPosFromCoord(coordinate)

// Update constraints to position the internal view at the screen location
centerXConstraint?.constant = CGFloat(screenPosition.x)
centerYConstraint?.constant = CGFloat(screenPosition.y)

// Force layout update if needed
if Thread.isMainThread {
mapView.setNeedsLayout()
} else {
DispatchQueue.main.async {
mapView.setNeedsLayout()
}
}
}

/// Cleanup method called when the anchor is removed
internal func cleanup() {
centerXConstraint?.isActive = false
centerYConstraint?.isActive = false
internalView.removeFromSuperview()
mapView = nil
}
}
Loading