diff --git a/Apps/Examples/Examples.xcodeproj/project.pbxproj b/Apps/Examples/Examples.xcodeproj/project.pbxproj index 1e1d794dd28e..44430ca27abd 100644 --- a/Apps/Examples/Examples.xcodeproj/project.pbxproj +++ b/Apps/Examples/Examples.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ 5A6D7B2A302A6555FE23FF80 /* blueprint_style.json in Resources */ = {isa = PBXBuildFile; fileRef = 989F5AB9D5D8AD39D21327A1 /* blueprint_style.json */; }; 5B2CE02503AF44EBC86FE884 /* MapboxMaps in Frameworks */ = {isa = PBXBuildFile; productRef = 0AF5F744C6369BF1FB233FB6 /* MapboxMaps */; }; 5E508E47388A646B4F74DD0B /* AnnotationsOrderTestExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3595A6E8FB1FD9F41DEB5C6F /* AnnotationsOrderTestExample.swift */; }; + 5F2AD73C8104089C9291574E /* GeofencingUserLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A816C6E4D7A0A532EEE84 /* GeofencingUserLocation.swift */; }; 5F537B052041931CB507E12B /* ViewportPlayground.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE863B179BF4F740C36D185E /* ViewportPlayground.swift */; }; 5F556CB71C442EC2A8C2E229 /* VisionOSMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B0E9086FE939F5D723136D /* VisionOSMain.swift */; platformFilters = (xros, ); }; 5FF3E34B523C39A404154BF7 /* OfflineRegionManagerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1229327C13654C370B5641FC /* OfflineRegionManagerExample.swift */; platformFilters = (ios, ); }; @@ -130,6 +131,7 @@ D94672F30272E31087AB5DDD /* NavigationSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC5980DD30479F30127BA71 /* NavigationSimulator.swift */; platformFilters = (ios, ); }; D98624793DA36578289F02FF /* MapScrollExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65535FB9F190778001AB847A /* MapScrollExample.swift */; }; DA69CB0BD9F0DDA0FD1387B0 /* DataJoinExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D0CD9C2D04EA5B12E7F84C /* DataJoinExample.swift */; platformFilters = (ios, ); }; + DCA54F7383085A8FD822F0BF /* GeofencingPlayground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7613C4E19DCD679A2620223C /* GeofencingPlayground.swift */; }; DFC64A62538E787D57B6514D /* DynamicViewAnnotationExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3333EF3E0F1C789809F385AF /* DynamicViewAnnotationExample.swift */; platformFilters = (ios, ); }; E121F023995CCF2F3A65BC2A /* LocateMeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DA0608D44DEF6C4A82777C /* LocateMeExample.swift */; }; E2617ACF1E2367C012A87CD1 /* ViewAnnotationsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59AACE9E33102AE90526569F /* ViewAnnotationsExample.swift */; }; @@ -241,6 +243,7 @@ 70922E748D003176C4A3C60A /* HeatmapLayerGlobeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeatmapLayerGlobeExample.swift; sourceTree = ""; }; 7274E152F7FBB7894447F822 /* AnimateGeoJSONLineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimateGeoJSONLineExample.swift; sourceTree = ""; }; 75D03F5A3A0E879717BFE421 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 7613C4E19DCD679A2620223C /* GeofencingPlayground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeofencingPlayground.swift; sourceTree = ""; }; 78811E5A3185D2D32495870A /* SimpleMapExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleMapExample.swift; sourceTree = ""; }; 7A77AEDBF679F223D4412FEE /* AttributionDialogueExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionDialogueExamples.swift; sourceTree = ""; }; 7DB76F486D80FED88678B04D /* LongTapAnimationExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongTapAnimationExample.swift; sourceTree = ""; }; @@ -300,6 +303,7 @@ D8730F8FB259A4F889609108 /* radar2.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = radar2.gif; sourceTree = ""; }; DAC8E7B565C817D872CFBCAD /* AnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationView.swift; sourceTree = ""; }; DC98E9169E8E7DFE8DC1CB27 /* BasicLocationPulsingExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicLocationPulsingExample.swift; sourceTree = ""; }; + DD3A816C6E4D7A0A532EEE84 /* GeofencingUserLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeofencingUserLocation.swift; sourceTree = ""; }; DD6F1212BB2453DBFECE12F2 /* StandardStyleLocationsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardStyleLocationsExample.swift; sourceTree = ""; }; DE6CEA9899CC0EC4F4381E19 /* CarPlayMapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayMapViewController.swift; sourceTree = ""; }; DE863B179BF4F740C36D185E /* ViewportPlayground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewportPlayground.swift; sourceTree = ""; }; @@ -391,6 +395,8 @@ 46CE3D9C2873C0767DD76D85 /* ClusteringExample.swift */, C61CC711054A032EE0446036 /* DynamicStylingExample.swift */, A6B06A1D70F479D8DC5C375A /* FeaturesQueryExample.swift */, + 7613C4E19DCD679A2620223C /* GeofencingPlayground.swift */, + DD3A816C6E4D7A0A532EEE84 /* GeofencingUserLocation.swift */, 62DA0608D44DEF6C4A82777C /* LocateMeExample.swift */, 45B39AE24486FED5ED30392D /* LocationOverrideExample.swift */, 6CD7ADCCB774239AA0090C46 /* RasterParticleExample.swift */, @@ -864,6 +870,8 @@ 854CE1A84AADF6FBB232CB5F /* FeaturesQueryExample.swift in Sources */, AE6E90DB7B6DA4580C2DAB59 /* FrameViewAnnotationsExample.swift in Sources */, 9717811318AAE88DE9214307 /* GeofencingExample.swift in Sources */, + DCA54F7383085A8FD822F0BF /* GeofencingPlayground.swift in Sources */, + 5F2AD73C8104089C9291574E /* GeofencingUserLocation.swift in Sources */, 0414AD72988F405F5BA1D843 /* GlobeFlyToExample.swift in Sources */, 49F6209402BF34C06C90107A /* HeatmapLayerGlobeExample.swift in Sources */, B9B1EE72E6203358F2785916 /* IconSizeChangeExample.swift in Sources */, diff --git a/Apps/Examples/Examples/AppDelegate.swift b/Apps/Examples/Examples/AppDelegate.swift index 0d0ce6875b92..1bfecbbf4508 100644 --- a/Apps/Examples/Examples/AppDelegate.swift +++ b/Apps/Examples/Examples/AppDelegate.swift @@ -115,28 +115,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let action = content.userInfo["action"]! as! String os_log(.info, "AppDelegate Got %s for feature %s", action, featureId) - if #available(iOS 13.0, *) { - guard let navigationController = (UIApplication.shared.connectedScenes.first?.delegate as? - SceneDelegate)?.windows.first?.rootViewController as? UINavigationController else { - os_log(.error, "AppDelegate Navigation controller not found") - return - } - var geofencingViewController: GeofencingExample - if navigationController.topViewController is GeofencingExample { - geofencingViewController = navigationController.topViewController as! GeofencingExample - } else { - geofencingViewController = Examples.geofencingExample.makeViewController() as! GeofencingExample - navigationController.pushViewController(geofencingViewController, animated: true) - } - - switch action { - case "Entered": geofencingViewController.updateEnterGeofenceId(id: featureId) - case "Exited": geofencingViewController.updateExitGeofenceId(id: featureId) - case "Dwelled": geofencingViewController.updateDwellGeofenceId(id: featureId) - default: os_log(.error, "AppDelegate Unknown action %s", action) - } - - } // tell the app that we have finished processing the user’s action / response completionHandler() } diff --git a/Apps/Examples/Examples/Info.plist b/Apps/Examples/Examples/Info.plist index 4719dade8f6c..7312845478be 100644 --- a/Apps/Examples/Examples/Info.plist +++ b/Apps/Examples/Examples/Info.plist @@ -29,9 +29,9 @@ MBXAccessToken MAPBOX_ACCESS_TOKEN NSLocationAlwaysAndWhenInUseUsageDescription - Get user location + Provide user location to be able to experience examples of background location capabilities. NSLocationWhenInUseUsageDescription - Get user location + Location is requred for proper functioning of certain examples. UIApplicationSceneManifest CPSupportsDashboardNavigationScene diff --git a/Apps/Examples/Examples/SwiftUI Examples/GeofencingPlayground.swift b/Apps/Examples/Examples/SwiftUI Examples/GeofencingPlayground.swift new file mode 100644 index 000000000000..8838ba971f9b --- /dev/null +++ b/Apps/Examples/Examples/SwiftUI Examples/GeofencingPlayground.swift @@ -0,0 +1,335 @@ +import Foundation +import SwiftUI +@_spi(Experimental) import MapboxMaps +@_spi(Experimental) import MapboxCommon + +/// This is an Example for Experimental API that is subject to change. +@available(iOS 14.0, *) +struct GeofencingPlayground: View { + @State private var showInfoSheet = false + @State private var isochrone: Turf.Feature? + @ObservedObject private var geofencing = Geofencing() + private var initialLocationProvider = InitialLocationProvider() + + var body: some View { + MapReader { proxy in + Map(initialViewport: .camera(center: .apple, zoom: 13)) { + Puck2D(bearing: .heading) + if let isochrone { + Isochrone(id: "isochrone", feature: isochrone) + } + } + .onMapTapGesture { context in + fetch(from: .isochrone(coordinate: context.coordinate, contourMinutes: 3)) { newFeature in + geofencing.replace(oldFeature: isochrone, with: newFeature) + isochrone = newFeature + } + } + .onMapLoaded { _ in geofencing.start() } + .ignoresSafeArea() + .safeOverlay(alignment: .trailing) { + InfoButton(action: { showInfoSheet = true }) + .padding(.all) + } + .safeOverlay(alignment: .bottom) { + LoggingView(hasUserConsent: geofencing.hasUserConsent, lastEvent: geofencing.lastEvent, isochrone: isochrone) + } + .sheet(isPresented: $showInfoSheet) { + InfoView() + .defaultDetents() + } + } + } +} + +@available(iOS 14.0, *) +private struct Isochrone: MapStyleContent { + var id: String + var feature: Turf.Feature + + var body: some MapStyleContent { + GeoJSONSource(id: "isochrone-source") + .data(.feature(feature)) + + FillLayer(id: "isochrone-layer", source: "isochrone-source") + .fillColor(.random) + .fillOpacity(0.5) + .fillColorTransition(StyleTransition(duration: 0.5, delay: 0.1)) + } +} + +@available(iOS 14.0, *) +private struct LoggingView: View { + var hasUserConsent: Bool + var lastEvent: GeofenceEvent? + var isochrone: Turf.Feature? + + var body: some View { + VStack(spacing: 8) { + VStack(alignment: .leading, spacing: 2) { + if isochrone == nil { + Text("Tap on map to add isochrone for the point.").font(.safeMonospaced) + } + if let geofencingEvent = lastEvent { + IndicativeLog(color: geofencingEvent.color, text: "Last geofencing event: \(geofencingEvent.type)") + } else { + IndicativeLog(color: .orange, text: "No geofencing events." ) + } + + IndicativeLog( + color: hasUserConsent ? .green : .red, + text: "Geofencing consent is given - \(hasUserConsent)." + ) + } + .floating() + + HStack { + OvalButton(title: "Enable Pushes", action: requestNotificationPermission) + OvalButton(title: "Enable Location", action: requestLocationAuthorization) + } + } + .padding(.bottom, 30) + } +} + +@available(iOS 14.0, *) +private struct InfoView: View { + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text("Dwell event emitted after 1 minute inside.") + .font(.safeMonospaced) + Spacer() + Text("Geofences are stored persistently.") + .font(.safeMonospaced) + Spacer() + Text("Deleting the isochrone programtically doesn't mean exiting from it.") + .font(.safeMonospaced) + Spacer() + Text("To test background functionality you may enable push notifications and hide the app.") + .font(.safeMonospaced) + Spacer() + Text("Easisest way to test location on simulator is to use Apple pre-defined routes.") + .font(.safeMonospaced) + Text("Simulator -> Features -> Location -> (Apple, Freeway Drive, City Bicycle Ride).") + .font(.safeMonospaced) + } + .padding(.vertical, 12) + .padding(.horizontal, 12) + } +} + +@available(iOS 14.0, *) +private final class InitialLocationProvider { + private var cancellables = Set() + + func start(locationManager: LocationManager?, _ onIntialLocation: @escaping (CLLocationCoordinate2D) -> Void) { + locationManager?.onLocationChange + .debounce(for: .seconds(0.5), scheduler: RunLoop.main) + .sink { [weak self] locations in + guard let location = locations.first else { return print("No locations received") } + onIntialLocation(location.coordinate) + self?.cancellables.removeAll() + } + .store(in: &cancellables) + } +} + +@available(iOS 14.0, *) +private final class Geofencing: ObservableObject { + @Published var lastEvent: GeofenceEvent? + @Published var hasUserConsent: Bool = GeofencingUtils.getUserConsent() + + func start() { + let geofencing = GeofencingFactory.getOrCreate() + geofencing.configure(options: GeofencingOptions()) { [weak self] result in + guard let self else { return } + /// Geofences are store in database on disk. + /// To make example isolated and synchronised with UI we try to delete existing feature from database. + geofencing.clearFeatures { result in + print("Clear features: \(result)") + geofencing.addObserver(observer: self) { result in print("Add observer: \(result)") } + } + } + } + + func replace(oldFeature: Turf.Feature?, with newFeature: Turf.Feature?) { + guard let newFeature else { return } + let geofencing = GeofencingFactory.getOrCreate() + + if let featureId = oldFeature?.identifier?.string { + geofencing.removeFeature(identifier: featureId) { result in + print("Remove feature with id(\(featureId): \(result)") + geofencing.addFeature(feature: newFeature) { result in print("Add feature \(result)") } + } + } else { + geofencing.addFeature(feature: newFeature) { result in print("Add feature \(result)") } + } + } + + func add(feature: Turf.Feature) { + let geofencing = GeofencingFactory.getOrCreate() + geofencing.addFeature(feature: feature) { result in print("Add feature: \(result)") } + } + + func remove(featureId: String) { + let geofencing = GeofencingFactory.getOrCreate() + geofencing.removeFeature(identifier: featureId) { result in print("Remove feature with id(\(featureId): \(result)") } + } + + func reset() { + GeofencingFactory.reset() + } +} + +private extension GeoJSONSourceData { + static func isochrone(_ featureCollection: FeatureCollection) -> GeoJSONSourceData { + .featureCollection(featureCollection) + } +} + +@available(iOS 14.0, *) +extension Geofencing: GeofencingObserver { + func onEntry(event: GeofencingEvent) { + DispatchQueue.main.async { self.lastEvent = GeofenceEvent(type: .entry, feature: event.feature) } + } + + func onDwell(event: GeofencingEvent) { + DispatchQueue.main.async { self.lastEvent = GeofenceEvent(type: .dwell, feature: event.feature) } + } + + func onExit(event: GeofencingEvent) { + DispatchQueue.main.async { self.lastEvent = GeofenceEvent(type: .exit, feature: event.feature) } + } + + func onUserConsentChanged(isConsentGiven: Bool) { + DispatchQueue.main.async { self.hasUserConsent = isConsentGiven } + } + + func onError(error: GeofencingError) {} +} + +@available(iOS 14.0, *) +private struct GeofenceEvent { + enum GeofenceEventType { + case entry + case dwell + case exit + } + + var type: GeofenceEventType + var feature: Turf.Feature + + var color: Color { + switch type { + case .dwell: + return .green + case .exit: + return .red + case .entry: + return .blue + } + } +} + +private extension URL { + static func isochrone( + coordinate: CLLocationCoordinate2D, + profile: IsochroneProfile = .driving, + contourMinutes: Int = 10, + createPolygon: Bool = true + ) -> URL { + guard let accessToken = Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String else { + fatalError("No access token provided to the Examples app.") + } + + return URL(string: "https://api.mapbox.com/isochrone/v1/mapbox/\(profile.rawValue)/\(coordinate.longitude)%2C\(coordinate.latitude)?contours_minutes=\(contourMinutes)&polygons=\(createPolygon)&denoise=1&access_token=\(accessToken)")! + + } +} + +enum IsochroneProfile: String { + case driving + case drivingTraffic = "driving-traffic" + case walking + case cycling +} + +private func fetch(from isochroneURL: URL, _ completion: @escaping (Turf.Feature?) -> Void) { + URLSession.shared.dataTask(with: URLRequest(url: isochroneURL)) { data, response, error in + var feature: Turf.Feature? + defer { DispatchQueue.main.async { completion(feature) } } + if let data { + let featureCollection = try? JSONDecoder().decode(FeatureCollection.self, from: data) + /// Assuming here that isochrone polygon returned as a single feature, which is not strictly guaranteed. + feature = featureCollection?.features.first?.enriched() + } + }.resume() +} + +private func requestNotificationPermission() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge]) { success, error in + print("Notification permission request finished. Success: \(success), error: \(String(describing: error))") + } +} + +private func requestLocationAuthorization() { + CLLocationManager().requestAlwaysAuthorization() + print("Location request finished.") +} + +private extension Turf.Feature { + func enriched() -> Turf.Feature { + var enrichedFeature = Feature(geometry: geometry) + enrichedFeature = enrichedFeature.properties([GeofencingPropertiesKeys.dwellTimeKey: 1]) + enrichedFeature.identifier = .string("isochrone") + return enrichedFeature + } +} + +@available(iOS 14.0, *) +struct InfoButton: View { + var action: () -> Void + + var body: some View { + Button(action: action) { + Image(systemName: "info.circle") + .font(.system(size: 24)) + .foregroundColor(.blue) + .background(Circle() + .fill(Color.white) + .frame(width: 40, height: 40)) + } + } +} + +@available(iOS 14.0, *) +struct OvalButton: View { + var title: String + var action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(Capsule().fill(Color.blue)) + } + } +} + +@available(iOS 14.0, *) +struct IndicativeLog: View { + var color: Color + var text: String + + var body: some View { + HStack { + Circle() + .fill(color) + .frame(width: 15, height: 15) + Text(text).font(.safeMonospaced) + } + } +} diff --git a/Apps/Examples/Examples/SwiftUI Examples/GeofencingUserLocation.swift b/Apps/Examples/Examples/SwiftUI Examples/GeofencingUserLocation.swift new file mode 100644 index 000000000000..b6300dc80729 --- /dev/null +++ b/Apps/Examples/Examples/SwiftUI Examples/GeofencingUserLocation.swift @@ -0,0 +1,157 @@ +import Foundation +import SwiftUI +@_spi(Experimental) import MapboxMaps +@_spi(Experimental) import MapboxCommon + +/// This is an Example for Experimental API that is subject to change. +@available(iOS 14.0, *) +struct GeofencingUserLocation: View { + @State private var initialLocation: CLLocationCoordinate2D? + @ObservedObject private var geofencing = Geofencing() + private let initialLocationProvider = InitialLocationProvider() + + var body: some View { + MapReader { proxy in + Map(initialViewport: .followPuck(zoom: 16)) { + Puck2D(bearing: .heading) + if let initialLocation = initialLocation { + GeofenceCircle(id: "circle", location: initialLocation, event: geofencing.lastEvent) + } + } + .onMapLoaded { _ in startGeofencing(proxy.location) } + } + .ignoresSafeArea() + } + + func startGeofencing(_ locationManager: LocationManager?) { + geofencing.start { + initialLocationProvider.start(locationManager: locationManager) { initialLocation in + var feature = Feature(geometry: .geofenceCircle(initialLocation)) + feature.identifier = .string("geofence-source-circle") + feature = feature.properties([GeofencingPropertiesKeys.dwellTimeKey: 1]) + geofencing.add(feature: feature, onSuccess: { _initialLocation.wrappedValue = initialLocation }) + } + } + } +} + +@available(iOS 14.0, *) +private struct GeofenceCircle: MapStyleContent { + var id: String + var location: CLLocationCoordinate2D + var event: GeofenceEvent? + + var body: some MapStyleContent { + GeoJSONSource(id: "geofence-source-\(id)") + .data(.geofenceCircle(location)) + + FillLayer(id: "geofence-layer-\(id)", source: "geofence-source-\(id)") + .fillColor(color(for: event)) + .fillOpacity(0.5) + } + + private func color(for event: GeofenceEvent?) -> UIColor { + switch event?.type { + case .none: + return .yellow + case .entry: + return .blue + case .exit: + return .red + case .dwell: + return .green + } + } +} + +@available(iOS 14.0, *) +private final class InitialLocationProvider { + private var cancellables = Set() + + func start(locationManager: LocationManager?, _ onIntialLocation: @escaping (CLLocationCoordinate2D) -> Void) { + locationManager?.onLocationChange + .debounce(for: .seconds(0.5), scheduler: RunLoop.main) + .sink { [weak self] locations in + guard let location = locations.first else { return print("No locations received") } + onIntialLocation(location.coordinate) + self?.cancellables.removeAll() + } + .store(in: &cancellables) + } +} + +@available(iOS 14.0, *) +private final class Geofencing: ObservableObject { + @Published var lastEvent: GeofenceEvent? + + func start(_ completion: @escaping () -> Void) { + let geofencing = GeofencingFactory.getOrCreate() + geofencing.configure(options: GeofencingOptions()) { [weak self] result in + guard let self else { return } + /// Geofences are store in database on disk. + /// To make example isolated and synchronised with UI we try to delete existing feature from database. + geofencing.clearFeatures { result in + print("Clear features: \(result)") + geofencing.addObserver(observer: self) { result in print("Add observer: \(result)") } + completion() + } + } + } + + func add(feature: Turf.Feature, onSuccess: @escaping () -> Void) { + let geofencing = GeofencingFactory.getOrCreate() + geofencing.addFeature(feature: feature) { result in + print("Add feature result: \(result)") + if case .success = result { onSuccess() } + } + } + + func remove(featureId: String, completion: @escaping () -> Void) { + let geofencing = GeofencingFactory.getOrCreate() + geofencing.removeFeature(identifier: featureId) { result in + print("Remove feature result: \(result)") + completion() + } + } +} + +@available(iOS 14.0, *) +extension Geofencing: GeofencingObserver { + func onEntry(event: GeofencingEvent) { + DispatchQueue.main.async { self.lastEvent = GeofenceEvent(type: .entry, feature: event.feature) } + } + + func onDwell(event: GeofencingEvent) { + DispatchQueue.main.async { self.lastEvent = GeofenceEvent(type: .dwell, feature: event.feature) } + } + + func onExit(event: GeofencingEvent) { + DispatchQueue.main.async { self.lastEvent = GeofenceEvent(type: .exit, feature: event.feature) } + } + + func onError(error: GeofencingError) {} + func onUserConsentChanged(isConsentGiven: Bool) {} +} + +private struct GeofenceEvent { + enum GeofenceEventType { + case entry + case dwell + case exit + } + + var type: GeofenceEventType + var feature: Turf.Feature +} + +private extension GeoJSONSourceData { + static func geofenceCircle(_ center: LocationCoordinate2D) -> GeoJSONSourceData { + .geometry(.geofenceCircle(center)) + } +} + +private extension Turf.Geometry { + static func geofenceCircle(_ center: LocationCoordinate2D) -> Turf.Geometry { + .polygon(Polygon(center: center, radius: 30, vertices: 64)) + } +} diff --git a/Apps/Examples/Examples/SwiftUI Examples/SwiftUIRoot.swift b/Apps/Examples/Examples/SwiftUI Examples/SwiftUIRoot.swift index 09768877aee4..32b7878339b5 100644 --- a/Apps/Examples/Examples/SwiftUI Examples/SwiftUIRoot.swift +++ b/Apps/Examples/Examples/SwiftUI Examples/SwiftUIRoot.swift @@ -34,6 +34,11 @@ struct SwiftUIRoot: View { ExampleLink("Clustering data", note: "Display GeoJSON data with clustering using custom layers and handle interactions with them.", destination: ClusteringExample()) } header: { Text("Use cases") } + Section { + ExampleLink("GeofencingUserLocation", note: "Set geofence on user initial location.", destination: GeofencingUserLocation()) + ExampleLink("GeofencingPlayground", note: "Showcase isochrone API together with geofences.", destination: GeofencingPlayground()) + } header: { Text("Use cases") } + Section { ExampleLink("Map settings", note: "Showcase of the most possible map configurations.", destination: MapSettingsExample()) ExampleLink("Viewport Playground", note: "Showcase of the possible viewport states.", destination: ViewportPlayground()) diff --git a/CHANGELOG.md b/CHANGELOG.md index 921fadd3ca88..d3a8b83e6e8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Mapbox welcomes participation and contributions from everyone. ## main +* Add two separete Geofence examples in SwiftUI - `GeofencingPlayground` and `GeofencingUserLocation` + ## 11.8.0-rc.1 - 23 October, 2024 * Fix the bug when MapView would ignore the new bounds size if there are more than a single resizing event in the animation.