diff --git a/ios/readme.md b/ios/readme.md index a0b93f3b3..7aaad01af 100644 --- a/ios/readme.md +++ b/ios/readme.md @@ -4,7 +4,7 @@

- The lightweight and modern Map SDK for Android (6.0+) and iOS (14+) + The lightweight and modern Map SDK for Android (8.0+, OpenGL ES 3.2) and iOS (14+)

openmobilemaps.io @@ -20,6 +20,81 @@

iOS

+## Table of Contents +- [Quick Start](#quick-start) +- [System Requirements](#system-requirements) +- [Installation](#installation) +- [How to use](#how-to-use) + - [MapView](#mapview) + - [Vector Tiles](#vector-tiles) + - [Camera and Gesture Handling](#camera-and-gesture-handling) + - [Performance Considerations](#performance-considerations) + - [Overlays](#overlays) + - [Customisation](#customisation) +- [Troubleshooting](#troubleshooting) +- [How to build](#how-to-build) +- [Testing and Debugging](#testing-and-debugging) +- [License](#license) +- [Third-Party Software](#third-party-software) + +## Quick Start + +Get up and running with Open Mobile Maps in just a few steps: + +1. **Add the package dependency** to your iOS project (iOS 14.0+ required) +2. **Import MapCore** in your Swift file +3. **Create a MapView** with a raster layer +4. **Display your map** + +### SwiftUI (iOS 17.0+) +```swift +import MapCore +import SwiftUI + +struct ContentView: View { + @State private var camera = MapView.Camera( + latitude: 46.962592372639634, + longitude: 8.378232525377973, + zoom: 1000000 + ) + + var body: some View { + MapView( + camera: $camera, + layers: [ + TiledRasterLayer("osm", webMercatorUrlFormat: "https://tile.openstreetmap.org/{z}/{x}/{y}.png") + ] + ) + } +} +``` + +### UIKit +```swift +import MapCore + +class MapViewController: UIViewController { + lazy var mapView = MCMapView() + + override func loadView() { + view = mapView + } + + override func viewDidLoad() { + super.viewDidLoad() + + mapView.add(layer: TiledRasterLayer("osm", webMercatorUrlFormat: "https://tile.openstreetmap.org/{z}/{x}/{y}.png")) + mapView.camera.move(toCenterPositionZoom: MCCoord(lat: 46.962592372639634, lon: 8.378232525377973), zoom: 1000000, animated: true) + } +} +``` + +## System Requirements + +- **iOS 14.0+** (SwiftUI MapView requires iOS 17.0+) +- **Xcode 14.0+** +- **Swift 5.7+** + ## Installation Open Mobile Maps is available through [Swift Package Manager](https://swift.org/package-manager/). @@ -168,6 +243,10 @@ struct ContentView: View { private func setupWMTSLayer() { guard let resource = MCWmtsCapabilitiesResource.create(xml), let wmtsLayer = resource.createLayer("identifier", tileLoader: MCTextureLoader()) else { + print("Failed to create WMTS layer - falling back to raster layer") + layers = [ + TiledRasterLayer("fallback", webMercatorUrlFormat: "https://tile.openstreetmap.org/{z}/{x}/{y}.png") + ] return } layers = [wmtsLayer] @@ -178,13 +257,19 @@ struct ContentView: View { ##### UIKit ```swift -let resource = MCWmtsCapabilitiesResource.create(xml)! +guard let resource = MCWmtsCapabilitiesResource.create(xml) else { + print("Failed to parse WMTS capabilities") + return +} ``` The created resource object is then capable of creating a layer object with a given identifier. ```swift -let layer = resource.createLayer("identifier", tileLoader: loader) -mapView.add(layer: layer?.asLayerInterface()) +guard let layer = resource.createLayer("identifier", tileLoader: loader) else { + print("Failed to create WMTS layer with identifier") + return +} +mapView.add(layer: layer.asLayerInterface()) ``` This feature is still being improved to support a wider range of WMTS capabilities. @@ -233,7 +318,221 @@ mapView.add(layer: try! VectorLayer("base-map", styleURL: "https://www.sample.or Additional features and differences will be documented soon. +### Camera and Gesture Handling + +Open Mobile Maps provides comprehensive camera control and gesture handling for both SwiftUI and UIKit. + +#### Camera Control + +The camera system allows you to programmatically control the map's view, including position, zoom, and in 3D mode, the viewing angle. + +##### SwiftUI Camera Binding + +In SwiftUI, the camera is bound to your view state, allowing for reactive updates: + +```swift +struct ContentView: View { + @State private var camera = MapView.Camera( + latitude: 46.962592372639634, + longitude: 8.378232525377973, + zoom: 1000000 + ) + + var body: some View { + VStack { + // Camera position updates automatically as user interacts + Text("Lat: \(camera.center.value?.lat ?? 0, specifier: "%.6f")") + Text("Lon: \(camera.center.value?.lon ?? 0, specifier: "%.6f")") + Text("Zoom: \(camera.zoom.value ?? 0, specifier: "%.0f")") + + MapView(camera: $camera, layers: layers) + + // Programmatic camera control + HStack { + Button("Zoom In") { + if let currentZoom = camera.zoom.value { + camera = MapView.Camera( + center: camera.center.value, + zoom: currentZoom * 0.5, // Zoom in + animated: true + ) + } + } + Button("Reset") { + camera = MapView.Camera( + latitude: 46.962592372639634, + longitude: 8.378232525377973, + zoom: 1000000, + animated: true + ) + } + } + } + } +} +``` + +##### UIKit Camera Control + +For UIKit, use the `MCMapView.camera` property: + +```swift +class MapViewController: UIViewController { + lazy var mapView = MCMapView() + + override func viewDidLoad() { + super.viewDidLoad() + + // Move camera with animation + mapView.camera.move( + toCenterPositionZoom: MCCoord(lat: 46.962592372639634, lon: 8.378232525377973), + zoom: 1000000, + animated: true + ) + + // Set camera bounds (restrict panning area) + let bounds = MCRectCoord( + topLeft: MCCoord(lat: 47.8, lon: 5.9), + bottomRight: MCCoord(lat: 45.8, lon: 10.5) + ) + mapView.camera.setBounds(bounds, paddingLeft: 50, paddingRight: 50, paddingTop: 100, paddingBottom: 50) + + // Set zoom limits + mapView.camera.setMinZoom(100000) // Min zoom level + mapView.camera.setMaxZoom(1000) // Max zoom level + } + + // Listen to camera changes + private func setupCameraListener() { + mapView.callbackHandler = MapCallbackHandler { [weak self] in + let currentPosition = self?.mapView.camera.getCenterPosition() + print("Camera moved to: \(currentPosition?.lat ?? 0), \(currentPosition?.lon ?? 0)") + } + } +} +``` + +#### Custom Gesture Handling + +You can customize gesture behavior or add custom touch handling: + +##### Custom Touch Handler (UIKit) + +```swift +class CustomTouchHandler: MCMapViewTouchHandler { + override func onTap(_ posScreen: MCVec2F, posMap: MCCoord, confirmed: Bool) { + if confirmed { + print("Tapped at map coordinate: \(posMap.lat), \(posMap.lon)") + // Handle tap event + } + } + + override func onLongPress(_ posScreen: MCVec2F, posMap: MCCoord) { + print("Long press at: \(posMap.lat), \(posMap.lon)") + // Handle long press event + } + + override func onPan(_ posScreen: MCVec2F, posMap: MCCoord, translation: MCVec2F, state: MCGestureState) { + // Handle custom pan behavior + if state == .BEGAN { + print("Pan started") + } + } +} + +// In your view controller: +mapView.touchHandler = CustomTouchHandler() +``` + +##### Disabling Gestures + +```swift +// Disable specific gestures +mapView.setGestureEnabled(.PAN, enabled: false) +mapView.setGestureEnabled(.ZOOM, enabled: false) +mapView.setGestureEnabled(.ROTATION, enabled: false) +``` + + + +``` + +### Performance Considerations + +To ensure optimal performance with Open Mobile Maps: +#### Layer Management +- **Limit concurrent layers**: Too many layers can impact rendering performance +- **Remove unused layers**: Call `mapView.remove(layer:)` when layers are no longer needed +- **Use appropriate tile sizes**: Standard 256x256 or 512x512 tiles work best + +#### Memory Management +- **Dispose of resources**: Large textures and layers should be properly cleaned up +- **Monitor memory usage**: Use Xcode's memory debugger to track texture and layer memory usage +- **Cache management**: The SDK automatically manages tile caching, but you can customize cache size + +```swift +// Configure cache size (in bytes) +MCTileLoader.setMaxCacheSize(100 * 1024 * 1024) // 100 MB cache + +// Clear cache when needed +MCTileLoader.clearCache() + +// Check cache status +let cacheSize = MCTileLoader.getCurrentCacheSize() +print("Current cache size: \(cacheSize) bytes") +``` + +#### Offline Support +- **Tile pre-caching**: Download tiles in advance for offline use +- **Local tile sources**: Serve tiles from app bundle or documents directory +- **Cache management**: Monitor storage usage and implement cache eviction policies + +```swift +// Example: Pre-cache tiles for a specific area +func precacheTilesForRegion(_ bounds: MCRectCoord, maxZoom: Int) { + let tileLoader = MCTileLoader() + + // Calculate tile coordinates for the bounds and zoom levels + for zoom in 0...maxZoom { + let tiles = calculateTilesForBounds(bounds, zoom: zoom) + for tileCoord in tiles { + let request = MCTileLoaderRequest(x: tileCoord.x, y: tileCoord.y, zoom: tileCoord.zoom) + tileLoader.load(request) + } + } +} +``` + +#### Rendering Optimization +- **Minimize overdraw**: Avoid overlapping opaque layers +- **Use appropriate zoom levels**: Don't load unnecessarily high-resolution tiles +- **Batch layer updates**: Group multiple layer changes together when possible + +```swift +// Good: Batch layer changes +mapView.beginLayerTransaction() +mapView.add(layer: layer1) +mapView.add(layer: layer2) +mapView.remove(layer: oldLayer) +mapView.commitLayerTransaction() +``` + +#### Thread Safety +- **Main thread UI updates**: Always update MapView properties on the main thread +- **Background processing**: Heavy processing should be done on background queues + +```swift +DispatchQueue.global(qos: .userInitiated).async { + // Heavy processing + let processedData = self.processMapData() + + DispatchQueue.main.async { + // Update UI on main thread + self.mapView.add(layer: processedData) + } +} +``` ### Overlays @@ -336,8 +635,14 @@ struct MapWithIconView: UIViewRepresentable { // Add icon layer let iconLayer = MCIconLayerInterface.create() - let image = UIImage(named: imageName) - let texture = try! TextureHolder(image!.cgImage!) + + guard let image = UIImage(named: imageName), + let cgImage = image.cgImage else { + print("Failed to load image: \(imageName)") + return mapView + } + + let texture = try! TextureHolder(cgImage) let icon = MCIconFactory.createIcon( "icon", coordinate: coordinate, @@ -383,8 +688,14 @@ struct ContentView: View { ```swift let iconLayer = MCIconLayerInterface.create() -let image = UIImage(named: "image") -let texture = try! TextureHolder(image!.cgImage!) + +guard let image = UIImage(named: "image"), + let cgImage = image.cgImage else { + print("Failed to load image") + return +} + +let texture = try! TextureHolder(cgImage) let icon = MCIconFactory.createIcon("icon", coordinate: coordinate, texture: texture, @@ -573,7 +884,16 @@ class TiledLayerConfig: MCTiled2dMapLayerConfig { #### Change map projection -To render the map using a different coordinate system, initialize the map view with a Map Config. The library provides a factory for the [EPSG3857](https://epsg.io/4326) Coordinate system and others, which we can use to initialize the map view. Layers can have a different projection than the map view itself. +Open Mobile Maps supports different coordinate systems and projections. The library provides built-in support for common coordinate systems and allows for custom implementations. + +##### Supported Coordinate Systems + +- **EPSG:3857** (Web Mercator) - Default for most web mapping services +- **EPSG:4326** (WGS84) - Standard latitude/longitude coordinates +- **EPSG:2056** (LV95/LV03+) - Swiss coordinate system +- **Custom coordinate systems** can be implemented by extending the coordinate system interfaces + +To render the map using a different coordinate system, initialize the map view with a Map Config. The library provides a factory for coordinate systems which we can use to initialize the map view. Layers can have a different projection than the map view itself. ##### SwiftUI @@ -611,6 +931,187 @@ struct ContentView: View { MCMapView(mapConfig: .init(mapCoordinateSystem: MCCoordinateSystemFactory.getEpsg2056System())) ``` +#### Advanced Customization Examples + +##### Custom Map Configuration + +```swift +// Create a custom map configuration with specific settings +let mapConfig = MCMapConfig( + mapCoordinateSystem: MCCoordinateSystemFactory.getEpsg3857System(), + camera3dConfig: MCCamera3dConfigFactory.getBasicConfig() +) + +// Configure rendering settings +mapConfig.renderingBackend = .METAL // Use Metal rendering on iOS +mapConfig.multisampling = true // Enable anti-aliasing +``` + +##### Custom Tile Loading + +```swift +// Implement custom tile loader for specialized data sources +class CustomTileLoader: MCTileLoaderInterface { + func load(_ request: MCTileLoaderRequest) { + // Custom tile loading logic + // Could load from local storage, custom server, etc. + DispatchQueue.global().async { + // Load tile data + let tileData = self.loadCustomTileData(request) + DispatchQueue.main.async { + request.onLoaded(tileData) + } + } + } + + func cancel(_ request: MCTileLoaderRequest) { + // Cancel loading if needed + } +} + +// Use custom loader +let customLayer = TiledRasterLayer("custom", config: customConfig, tileLoader: CustomTileLoader()) +``` + +##### Custom Rendering Callbacks + +```swift +class MapCallbackHandler: MCMapViewCallbackHandler { + func onMapReady() { + print("Map is ready for interaction") + } + + func onCameraChanged() { + print("Camera position changed") + } + + func onRenderingError(_ error: String) { + print("Rendering error: \(error)") + } +} + +mapView.callbackHandler = MapCallbackHandler() +``` + +##### Styling and Theming + +```swift +// Customize map appearance +extension MCMapView { + func applyDarkTheme() { + // Apply dark styling to layers + backgroundColor = .black + + // Update existing raster layers with dark tiles + layers.compactMap { $0 as? TiledRasterLayer }.forEach { layer in + // Switch to dark tile variant if available + layer.updateUrlFormat("https://dark-tiles.example.com/{z}/{x}/{y}.png") + } + } + + func applyCustomStyling() { + // Custom visual settings + layer.cornerRadius = 10 + layer.borderWidth = 2 + layer.borderColor = UIColor.systemBlue.cgColor + } +} +``` + +##### Accessibility Support + +```swift +// Make map accessible +mapView.isAccessibilityElement = true +mapView.accessibilityLabel = "Interactive map" +mapView.accessibilityHint = "Double tap to interact, drag to pan" + +// Custom accessibility for map features +class AccessibleMapView: MCMapView { + override func accessibilityElementDidBecomeFocused() { + // Announce current map region + let center = camera.getCenterPosition() + UIAccessibility.post(notification: .announcement, + argument: "Map centered at \(center.lat), \(center.lon)") + } +} +``` + +## Troubleshooting + +### Common Issues and Solutions + +#### Build Issues + +**"Module 'MapCore' not found"** +- Ensure you've added the package dependency correctly +- Try cleaning your build folder (โ‡งโŒ˜K) +- Reset package caches: File โ†’ Package Dependencies โ†’ Reset Package Caches + +**"Minimum deployment target"** +- Verify your project deployment target is iOS 14.0 or later +- For SwiftUI MapView, iOS 17.0+ is required + +#### Runtime Issues + +**"Failed to create vector layer"** +```swift +do { + let layer = try VectorLayer("layer-id", styleURL: styleURL) + mapView.add(layer: layer) +} catch VectorLayerError.invalidStyleURL { + print("Style URL is not valid or accessible") +} catch VectorLayerError.networkError { + print("Network error loading style") +} catch { + print("Other error: \(error)") +} +``` + +**Memory warnings or crashes** +- Reduce the number of simultaneous layers +- Lower tile cache size: `MCTileLoader.setMaxCacheSize(50 * 1024 * 1024)` +- Remove unused layers promptly + +**Poor rendering performance** +- Ensure you're not blocking the main thread +- Check for excessive layer overdraw +- Verify map view size constraints are properly set + +#### Layer Issues + +**Tiles not loading** +- Verify the tile URL pattern is correct +- Check network connectivity +- Ensure tile server supports CORS (for web requests) +- Test the tile URL in a browser: `https://example.com/1/0/0.png` + +**Vector layer styling issues** +- Validate your style JSON against the Mapbox style specification +- Check console for style parsing errors +- Ensure sprite and glyph URLs are accessible + +### Debugging Tips + +#### Enable Debug Logging +```swift +// Enable detailed logging (debug builds only) +#if DEBUG +MCLogger.setLogLevel(.DEBUG) +#endif +``` + +#### Performance Debugging +```swift +// Monitor rendering performance +mapView.isRenderingDebugEnabled = true // Shows frame time overlay +``` + +#### Network Debugging +- Use Charles Proxy or similar tools to monitor network requests +- Check tile loading patterns and response times +- Verify HTTP status codes for tile requests + ## How to build If you'd like to build Open Mobile Maps yourself, make sure you have all submodules initialized and updated. To do this, use @@ -634,6 +1135,136 @@ The [Package.swift](../Package.swift) file can be opened in Xcode and build dire ## License This project is licensed under the terms of the MPL 2 license. See the [LICENSE](../LICENSE) file. +## Testing and Debugging + +### Unit Testing Map Components + +Open Mobile Maps components can be tested using standard XCTest frameworks: + +```swift +import XCTest +import MapCore + +class MapTests: XCTestCase { + + func testMapViewInitialization() { + let mapView = MCMapView() + XCTAssertNotNil(mapView) + XCTAssertNotNil(mapView.camera) + } + + func testLayerCreation() { + let layer = TiledRasterLayer("test", webMercatorUrlFormat: "https://example.com/{z}/{x}/{y}.png") + XCTAssertEqual(layer.layerName, "test") + } + + func testVectorLayerCreation() { + do { + let layer = try VectorLayer("vector-test", styleURL: "https://example.com/style.json") + XCTAssertEqual(layer.layerName, "vector-test") + } catch { + XCTFail("Vector layer creation failed: \(error)") + } + } +} +``` + +### Integration Testing + +For testing map interactions and rendering: + +```swift +class MapIntegrationTests: XCTestCase { + var mapView: MCMapView! + + override func setUp() { + super.setUp() + mapView = MCMapView() + // Add to a test window for proper lifecycle + let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) + window.addSubview(mapView) + mapView.frame = window.bounds + } + + func testLayerAddition() { + let layer = TiledRasterLayer("test", webMercatorUrlFormat: "https://tile.openstreetmap.org/{z}/{x}/{y}.png") + + let expectation = self.expectation(description: "Layer added") + + mapView.add(layer: layer) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + // Verify layer was added + XCTAssertTrue(self.mapView.layers.contains { $0.layerName == "test" }) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1.0) + } +} +``` + +### SwiftUI Testing + +For SwiftUI MapView testing: + +```swift +import SwiftUI +import ViewInspector + +@available(iOS 17.0, *) +class SwiftUIMapTests: XCTestCase { + + func testMapViewCreation() throws { + let camera = MapView.Camera(latitude: 0, longitude: 0, zoom: 1000) + let mapView = MapView(camera: .constant(camera), layers: []) + + XCTAssertNoThrow(try mapView.inspect()) + } +} +``` + +### Performance Testing + +Monitor performance characteristics: + +```swift +class MapPerformanceTests: XCTestCase { + + func testLayerLoadingPerformance() { + let mapView = MCMapView() + + measure { + for i in 0..<10 { + let layer = TiledRasterLayer("layer\(i)", webMercatorUrlFormat: "https://example.com/{z}/{x}/{y}.png") + mapView.add(layer: layer) + } + } + } +} +``` + +### Debugging Tools + +#### Xcode Integration +- Use Xcode's **Memory Graph Debugger** to detect memory leaks +- **View Debugger** can help inspect map view hierarchy +- **Network Link Conditioner** to test poor network conditions + +#### Custom Debugging +```swift +extension MCMapView { + func debugInfo() -> String { + return """ + Camera Position: \(camera.getCenterPosition()) + Zoom Level: \(camera.getZoom()) + Layer Count: \(layers.count) + Active Layers: \(layers.map { $0.layerName }.joined(separator: ", ")) + """ + } +} +``` + ## Third-Party Software This project depends on: