diff --git a/Sources/InternalUtils/Logger.swift b/Sources/InternalUtils/Logger.swift new file mode 100644 index 0000000..48375d8 --- /dev/null +++ b/Sources/InternalUtils/Logger.swift @@ -0,0 +1,11 @@ +import os + +public extension Logger { + /// For more information see: + /// - https://developer.apple.com/documentation/os/viewing-log-messages + /// For even more details see: + /// - https://developer.apple.com/forums/thread/705868 + init(category: String) { + self.init(subsystem: "MapLibre-SwiftUI", category: category) + } +} diff --git a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift index a11513c..9d4f12e 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCameraUpdating.swift @@ -28,8 +28,12 @@ public protocol MLNMapViewCameraUpdating: AnyObject { animated: Bool, completionHandler: (() -> Void)? ) + + @MainActor var activityIdentifier: String { get } } extension MLNMapView: MLNMapViewCameraUpdating { - // No definition + public var activityIdentifier: String { + MapActivity.loggingValue(tag) + } } diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 02a8c5f..2a16d6b 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -1,14 +1,33 @@ import InternalUtils import MapLibre import MapLibreSwiftDSL +import os import SwiftUI +private extension Logger { + static let uiViewControllerRepresentable = Logger(category: "UIViewControllerRepresentable") +} + /// Identifies the activity this ``MapView`` is being used for. Useful for debugging purposes. -public enum MapActivity: Int { +public enum MapActivity: Int, CustomStringConvertible { /// Navigation in a standard window. Default. case standard = 0 /// Navigation in a CarPlay template. case carplay = 2025 + + public var description: String { + switch self { + case .standard: + "standard" + case .carplay: + "carplay" + } + } + + static func loggingValue(_ rawValue: Self.RawValue) -> String { + guard let activity = MapActivity(rawValue: rawValue) else { return String(rawValue) } + return activity.description + } } public struct MapView: UIViewControllerRepresentable { @@ -60,7 +79,9 @@ public struct MapView: UIViewControllerRepresentab } public func makeCoordinator() -> MapViewCoordinator { - MapViewCoordinator( + Logger.uiViewControllerRepresentable + .debug("\(#function, privacy: .public), activity: \(activity, privacy: .public)") + return MapViewCoordinator( parent: self, onGesture: { processGesture($0, $1) }, onViewProxyChanged: { onViewProxyChanged?($0) }, @@ -69,6 +90,8 @@ public struct MapView: UIViewControllerRepresentab } public func makeUIViewController(context: Context) -> T { + Logger.uiViewControllerRepresentable + .debug("\(#function, privacy: .public), activity: \(activity, privacy: .public)") // Create the map view let controller = makeViewController() controller.mapView.delegate = context.coordinator @@ -105,6 +128,8 @@ public struct MapView: UIViewControllerRepresentab } public func updateUIViewController(_ uiViewController: T, context: Context) { + Logger.uiViewControllerRepresentable + .debug("\(#function, privacy: .public), activity: \(activity, privacy: .public)") context.coordinator.parent = self applyModifiers(uiViewController, runUnsafe: true) diff --git a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift index f07a779..4b9bebd 100644 --- a/Sources/MapLibreSwiftUI/MapViewCoordinator.swift +++ b/Sources/MapLibreSwiftUI/MapViewCoordinator.swift @@ -1,6 +1,33 @@ import Foundation import MapLibre import MapLibreSwiftDSL +import os + +private extension Logger { + static let mlnMapViewDelegate = Logger(category: "MLNMapViewDelegate") + static let cameraUpdate = Logger(category: "cameraUpdate") +} + +extension MLNCameraChangeReason: @retroactive CustomStringConvertible { + private static let descriptions: [(Self, String)] = [ + (.programmatic, "programmatic"), + (.resetNorth, "resetNorth"), + (.gesturePan, "gesturePan"), + (.gesturePinch, "gesturePinch"), + (.gestureRotate, "gestureRotate"), + (.gestureZoomIn, "gestureZoomIn"), + (.gestureZoomOut, "gestureZoomOut"), + (.gestureOneFingerZoom, "gestureOneFingerZoom"), + (.gestureTilt, "gestureTilt"), + (.transitionCancelled, "transitionCancelled"), + ] + + public var description: String { + var names = Self.descriptions.filter { contains($0.0) }.map(\.1) + if names.isEmpty { names = ["none"] } + return names.joined(separator: ",") + } +} public class MapViewCoordinator: NSObject, @preconcurrency MLNMapViewDelegate { @@ -178,6 +205,11 @@ MLNMapViewDelegate { return } + Logger.cameraUpdate + .debug( + "camera: \(camera, privacy: .public) frame: \(NSCoder.string(for: mapView.frame), privacy: .public) animated: \(animated, privacy: .public) activity: \(mapView.activityIdentifier, privacy: .public)" + ) + snapshotCamera = camera // Cancel any existing camera update completion task. @@ -373,15 +405,25 @@ MLNMapViewDelegate { // MARK: - MLNMapViewDelegate - public func mapView(_: MLNMapView, didFinishLoading mglStyle: MLNStyle) { + @MainActor + public func mapView(_ mapView: MLNMapView, didFinishLoading mglStyle: MLNStyle) { + Logger.mlnMapViewDelegate + .debug( + "\(#function, privacy: .public) style: \(mglStyle, privacy: .public) activity: \(mapView.activityIdentifier, privacy: .public)" + ) addLayers(to: mglStyle) onStyleLoaded?(mglStyle) } /// The MapView's region has changed with a specific reason. + @MainActor public func mapView( - _ mapView: MLNMapView, regionDidChangeWith reason: MLNCameraChangeReason, animated _: Bool + _ mapView: MLNMapView, regionDidChangeWith reason: MLNCameraChangeReason, animated: Bool ) { + Logger.mlnMapViewDelegate + .debug( + "\(#function, privacy: .public) reason: \(reason, privacy: .public) animated: \(animated, privacy: .public) activity: \(mapView.activityIdentifier, privacy: .public)" + ) // TODO: We could put this in regionIsChangingWith if we calculate significant change/debounce. MainActor.assumeIsolated { // regionIsChangingWith is not called for the final update, so we need to call updateViewProxy @@ -412,6 +454,10 @@ MLNMapViewDelegate { @MainActor public func mapView(_ mapView: MLNMapView, regionIsChangingWith reason: MLNCameraChangeReason) { + Logger.mlnMapViewDelegate + .debug( + "\(#function, privacy: .public) reason: \(reason, privacy: .public) activity: \(mapView.activityIdentifier, privacy: .public)" + ) if proxyUpdateMode == .realtime { updateViewProxy(mapView: mapView, reason: reason) } diff --git a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift index ac0e779..50cee91 100644 --- a/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift +++ b/Sources/MapLibreSwiftUI/Models/MapCamera/MapViewCamera.swift @@ -5,7 +5,7 @@ import MapLibre /// The SwiftUI MapViewCamera. /// /// This manages the camera state within the MapView. -public struct MapViewCamera: Hashable, Equatable, Sendable { +public struct MapViewCamera: Hashable, Equatable, Sendable, CustomStringConvertible { public enum Defaults { public static let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) public static let zoom: Double = 10 @@ -139,4 +139,8 @@ public struct MapViewCamera: Hashable, Equatable, Sendable { MapViewCamera(state: .rect(boundingBox: box, edgePadding: edgePadding), lastReasonForChange: .programmatic) } + + public var description: String { + "State: \(state) last: \((lastReasonForChange != nil) ? "\(lastReasonForChange!)" : "nil")" + } } diff --git a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift index cdaf06d..258e018 100644 --- a/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift +++ b/Tests/MapLibreSwiftUITests/MapViewCoordinator/MapViewCoordinatorCameraTests.swift @@ -38,6 +38,10 @@ final class MapViewCoordinatorCameraTests: XCTestCase { ) .willReturn() + given(maplibreMapView) + .activityIdentifier + .willReturn("standard") + try await simulateCameraUpdateAndWait { self.coordinator.applyCameraChangeFromStateUpdate( self.maplibreMapView, camera: camera, animated: false @@ -95,6 +99,10 @@ final class MapViewCoordinatorCameraTests: XCTestCase { ) .willReturn() + given(maplibreMapView) + .activityIdentifier + .willReturn("standard") + try await simulateCameraUpdateAndWait { self.coordinator.applyCameraChangeFromStateUpdate( self.maplibreMapView, camera: newCamera, animated: false @@ -140,6 +148,10 @@ final class MapViewCoordinatorCameraTests: XCTestCase { .setZoomLevel(.any, animated: .any) .willReturn() + given(maplibreMapView) + .activityIdentifier + .willReturn("standard") + try await simulateCameraUpdateAndWait { self.coordinator.applyCameraChangeFromStateUpdate( self.maplibreMapView, camera: newCamera, animated: false @@ -185,6 +197,10 @@ final class MapViewCoordinatorCameraTests: XCTestCase { .setZoomLevel(.any, animated: .any) .willReturn() + given(maplibreMapView) + .activityIdentifier + .willReturn("standard") + try await simulateCameraUpdateAndWait { self.coordinator.applyCameraChangeFromStateUpdate( self.maplibreMapView, camera: newCamera, animated: false @@ -230,6 +246,10 @@ final class MapViewCoordinatorCameraTests: XCTestCase { .setZoomLevel(.any, animated: .any) .willReturn() + given(maplibreMapView) + .activityIdentifier + .willReturn("standard") + try await simulateCameraUpdateAndWait { self.coordinator.applyCameraChangeFromStateUpdate( self.maplibreMapView, camera: newCamera, animated: false diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testBoundingBox.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testBoundingBox.1.txt index 092ae5f..0ad2c1d 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testBoundingBox.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testBoundingBox.1.txt @@ -1,6 +1,4 @@ -▿ MapViewCamera - ▿ lastReasonForChange: Optional - - some: CameraChangeReason.programmatic +▿ State: CameraState.rect(northeast: CLLocationCoordinate2D(latitude: 24.6993808, longitude: 46.7709285), southwest: CLLocationCoordinate2D(latitude: 24.6056011, longitude: 46.67369842529297), edgePadding: UIEdgeInsets(top: 20.0, left: 20.0, bottom: 20.0, right: 20.0)) last: programmatic ▿ state: CameraState ▿ rect: (2 elements) ▿ boundingBox: MLNCoordinateBounds @@ -15,3 +13,5 @@ - left: 20.0 - right: 20.0 - top: 20.0 + ▿ lastReasonForChange: Optional + - some: CameraChangeReason.programmatic diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt index 318c577..4f27b21 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testCenterCamera.1.txt @@ -1,5 +1,4 @@ -▿ MapViewCamera - - lastReasonForChange: Optional.none +▿ State: CameraState.centered(onCoordinate: CLLocationCoordinate2D(latitude: 12.3, longitude: 23.4), zoom: 5.0, pitch: 12.0, pitchRange: free, direction: 23.0) last: nil ▿ state: CameraState ▿ centered: (5 elements) ▿ onCoordinate: CLLocationCoordinate2D @@ -9,3 +8,4 @@ - pitch: 12.0 - pitchRange: CameraPitchRange.free - direction: 23.0 + - lastReasonForChange: Optional.none diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt index f405f82..a04d0dd 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithCourse.1.txt @@ -1,6 +1,4 @@ -▿ MapViewCamera - ▿ lastReasonForChange: Optional - - some: CameraChangeReason.programmatic +▿ State: CameraState.trackingUserLocationWithCourse(zoom: (18.0, 0.0, MapLibreSwiftUI.CameraPitchRange.freeWithinRange(minimum: 12.0, maximum: 34.0))) last: programmatic ▿ state: CameraState ▿ trackingUserLocationWithCourse: (3 elements) - zoom: 18.0 @@ -9,3 +7,5 @@ ▿ freeWithinRange: (2 elements) - minimum: 12.0 - maximum: 34.0 + ▿ lastReasonForChange: Optional + - some: CameraChangeReason.programmatic diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt index cb5da4b..2eb212b 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackUserLocationWithHeading.1.txt @@ -1,8 +1,8 @@ -▿ MapViewCamera - ▿ lastReasonForChange: Optional - - some: CameraChangeReason.programmatic +▿ State: CameraState.trackingUserLocationWithHeading(zoom: (10.0, 0.0, MapLibreSwiftUI.CameraPitchRange.free)) last: programmatic ▿ state: CameraState ▿ trackingUserLocationWithHeading: (3 elements) - zoom: 10.0 - pitch: 0.0 - pitchRange: CameraPitchRange.free + ▿ lastReasonForChange: Optional + - some: CameraChangeReason.programmatic diff --git a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt index 4252dc9..e549cc9 100644 --- a/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt +++ b/Tests/MapLibreSwiftUITests/Models/MapCamera/__Snapshots__/MapViewCameraTests/testTrackingUserLocation.1.txt @@ -1,6 +1,4 @@ -▿ MapViewCamera - ▿ lastReasonForChange: Optional - - some: CameraChangeReason.programmatic +▿ State: CameraState.trackingUserLocation(zoom: (10.0, 0.0, MapLibreSwiftUI.CameraPitchRange.freeWithinRange(minimum: 12.0, maximum: 34.0), 0.0)) last: programmatic ▿ state: CameraState ▿ trackingUserLocation: (4 elements) - zoom: 10.0 @@ -10,3 +8,5 @@ - minimum: 12.0 - maximum: 34.0 - direction: 0.0 + ▿ lastReasonForChange: Optional + - some: CameraChangeReason.programmatic