User Location and Location Privacy Example iOS (#2389)
This commit is contained in:
@@ -35,7 +35,7 @@ The code samples in the documentation should ideally be compiled on CI so they d
|
||||
Fence your example code with
|
||||
|
||||
```swift
|
||||
// #-example-code(LineTapMapView)
|
||||
// #-example-code(LineTapMap)
|
||||
...
|
||||
// #-end-example-code
|
||||
```
|
||||
@@ -43,7 +43,7 @@ Fence your example code with
|
||||
Prefix your documentation code block with
|
||||
|
||||
````md
|
||||
<!-- include-example(LineTapMapView) -->
|
||||
<!-- include-example(LineTapMap) -->
|
||||
|
||||
```swift
|
||||
...
|
||||
|
||||
@@ -30,10 +30,10 @@ import MapLibre
|
||||
|
||||
To use MapLibre with SwiftUI we need to create a wrapper for the UIKit view that MapLibre provides (using UIViewRepresentable. The simplest way to implement this protocol is as follows:
|
||||
|
||||
<!-- include-example(SimpleMapView) -->
|
||||
<!-- include-example(SimpleMap) -->
|
||||
|
||||
```swift
|
||||
struct SimpleMapView: UIViewRepresentable {
|
||||
struct SimpleMap: UIViewRepresentable {
|
||||
func makeUIView(context _: Context) -> MLNMapView {
|
||||
let mapView = MLNMapView()
|
||||
return mapView
|
||||
|
||||
@@ -6,10 +6,10 @@ Demonstrating adding ``MLNPolyline`` annotations and responding to user input.
|
||||
|
||||
This example draws a line from the tapped location to the center of the map. Handling the tap is done by the `Coordinator` class. It converts the location on the view to a geographic coordinate. It removes existing annotations before adding the new line.
|
||||
|
||||
<!-- include-example(LineTapMapView) -->
|
||||
<!-- include-example(LineTapMap) -->
|
||||
|
||||
```swift
|
||||
struct LineTapMapView: UIViewRepresentable {
|
||||
struct LineTapMap: UIViewRepresentable {
|
||||
func makeUIView(context: Context) -> MLNMapView {
|
||||
let mapView = MLNMapView()
|
||||
|
||||
@@ -32,9 +32,9 @@ struct LineTapMapView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
class Coordinator: NSObject {
|
||||
var parent: LineTapMapView
|
||||
var parent: LineTapMap
|
||||
|
||||
init(_ parent: LineTapMapView) {
|
||||
init(_ parent: LineTapMap) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
|
||||
132
platform/ios/MapLibre.docc/LocationPrivacyExample.md
Normal file
132
platform/ios/MapLibre.docc/LocationPrivacyExample.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# User Location & Location Privacy
|
||||
|
||||
Requesting precise location with ``MLNLocationManager``.
|
||||
|
||||
## Overview
|
||||
|
||||
This example shows how to request a precise location with ``MLNLocationManager``.
|
||||
|
||||
First of all you need to prepare your `Info.plist`. You need to provide a description why your app needs to access location:
|
||||
|
||||
```plist
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Dummy Location When In Use Description</string>
|
||||
```
|
||||
|
||||
As well as a description why your app needs precise location.
|
||||
|
||||
```plist
|
||||
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
|
||||
<dict>
|
||||
<key>MLNAccuracyAuthorizationDescription</key>
|
||||
<string>Dummy Precise Location Description</string>
|
||||
</dict>
|
||||
```
|
||||
|
||||
The `Coordinator` defined below is also the ``MLNMapViewDelegate``. When the location manager authorization changes it will call the relevant method. If precise location has not been granted, a button is shown at the bottom of the map.
|
||||
|
||||

|
||||
|
||||
When the button is pressed a pop-up with the description we set in `Info.plist` will be shown:
|
||||
|
||||

|
||||
|
||||
<!-- include-example(LocationPrivacyExample) -->
|
||||
|
||||
```swift
|
||||
enum LocationAccuracyState {
|
||||
case unknown
|
||||
case reducedAccuracy
|
||||
case fullAccuracy
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, MLNMapViewDelegate {
|
||||
@Binding var mapView: MLNMapView
|
||||
@Binding var locationAccuracy: LocationAccuracyState
|
||||
var pannedToUserLocation = false
|
||||
|
||||
init(mapView: Binding<MLNMapView>, locationAccuracy: Binding<LocationAccuracyState>) {
|
||||
_mapView = mapView
|
||||
_locationAccuracy = locationAccuracy
|
||||
}
|
||||
|
||||
func mapView(_: MLNMapView, didChangeLocationManagerAuthorization manager: MLNLocationManager) {
|
||||
guard let accuracySetting = manager.accuracyAuthorization else {
|
||||
return
|
||||
}
|
||||
|
||||
switch accuracySetting() {
|
||||
case .fullAccuracy:
|
||||
locationAccuracy = .fullAccuracy
|
||||
case .reducedAccuracy:
|
||||
locationAccuracy = .reducedAccuracy
|
||||
@unknown default:
|
||||
locationAccuracy = .unknown
|
||||
}
|
||||
}
|
||||
|
||||
// when a location is available for the first time, we fly to it
|
||||
func mapView(_ mapView: MLNMapView, didUpdate _: MLNUserLocation?) {
|
||||
if pannedToUserLocation {
|
||||
return
|
||||
}
|
||||
guard let userLocation = mapView.userLocation else {
|
||||
print("User location is currently not available.")
|
||||
return
|
||||
}
|
||||
mapView.fly(to: MLNMapCamera(lookingAtCenter: userLocation.coordinate, altitude: 100_000, pitch: 0, heading: 0))
|
||||
}
|
||||
}
|
||||
|
||||
struct LocationPrivacyExample: UIViewRepresentable {
|
||||
@Binding var mapView: MLNMapView
|
||||
@Binding var locationAccuracy: LocationAccuracyState
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(mapView: $mapView, locationAccuracy: $locationAccuracy)
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> MLNMapView {
|
||||
let mapView = MLNMapView()
|
||||
mapView.showsUserLocation = true
|
||||
mapView.delegate = context.coordinator
|
||||
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_: MLNMapView, context _: Context) {}
|
||||
}
|
||||
|
||||
struct LocationPrivacyExampleView: View {
|
||||
@State private var mapView = MLNMapView()
|
||||
@State var locationAccuracy: LocationAccuracyState = .unknown
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
LocationPrivacyExample(mapView: $mapView, locationAccuracy: $locationAccuracy)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
if locationAccuracy == LocationAccuracyState.reducedAccuracy {
|
||||
Button("Request Precise Location") {
|
||||
handleButtonPress(mapView: mapView)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleButtonPress(mapView: MLNMapView) {
|
||||
print("Requesting precice location")
|
||||
switch locationAccuracy {
|
||||
case .reducedAccuracy:
|
||||
let purposeKey = "MLNAccuracyAuthorizationDescription"
|
||||
mapView.locationManager.requestTemporaryFullAccuracyAuthorization!(withPurposeKey: purposeKey)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -15,6 +15,7 @@ Powerful, free and open-source mapping toolkit with full control over data sourc
|
||||
|
||||
- <doc:GettingStarted>
|
||||
- <doc:LineOnUserTap>
|
||||
- <doc:LocationPrivacyExample>
|
||||
|
||||
## Topics
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ ios_application(
|
||||
"ipad",
|
||||
],
|
||||
infoplists = [":Info.plist"],
|
||||
minimum_os_version = "15.0",
|
||||
minimum_os_version = "16.0",
|
||||
provisioning_profile = "xcode_profile",
|
||||
visibility = ["@rules_xcodeproj//xcodeproj:generated"],
|
||||
deps = [":Sources"],
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
|
||||
<dict>
|
||||
<key>MLNAccuracyAuthorizationDescription</key>
|
||||
<string>Dummy Precise Location Description</string>
|
||||
</dict>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string></string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Dummy Location When In Use Description</string>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UILaunchScreen</key>
|
||||
@@ -36,4 +45,4 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -2,8 +2,8 @@ import MapLibre
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// #-example-code(LineTapMapView)
|
||||
struct LineTapMapView: UIViewRepresentable {
|
||||
// #-example-code(LineTapMap)
|
||||
struct LineTapMap: UIViewRepresentable {
|
||||
func makeUIView(context: Context) -> MLNMapView {
|
||||
let mapView = MLNMapView()
|
||||
|
||||
@@ -26,9 +26,9 @@ struct LineTapMapView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
class Coordinator: NSObject {
|
||||
var parent: LineTapMapView
|
||||
var parent: LineTapMap
|
||||
|
||||
init(_ parent: LineTapMapView) {
|
||||
init(_ parent: LineTapMap) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
|
||||
102
platform/ios/app-swift/Sources/LocationPrivacyExample.swift
Normal file
102
platform/ios/app-swift/Sources/LocationPrivacyExample.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
import MapLibre
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// #-example-code(LocationPrivacyExample)
|
||||
enum LocationAccuracyState {
|
||||
case unknown
|
||||
case reducedAccuracy
|
||||
case fullAccuracy
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, MLNMapViewDelegate {
|
||||
@Binding var mapView: MLNMapView
|
||||
@Binding var locationAccuracy: LocationAccuracyState
|
||||
var pannedToUserLocation = false
|
||||
|
||||
init(mapView: Binding<MLNMapView>, locationAccuracy: Binding<LocationAccuracyState>) {
|
||||
_mapView = mapView
|
||||
_locationAccuracy = locationAccuracy
|
||||
}
|
||||
|
||||
func mapView(_: MLNMapView, didChangeLocationManagerAuthorization manager: MLNLocationManager) {
|
||||
guard let accuracySetting = manager.accuracyAuthorization else {
|
||||
return
|
||||
}
|
||||
|
||||
switch accuracySetting() {
|
||||
case .fullAccuracy:
|
||||
locationAccuracy = .fullAccuracy
|
||||
case .reducedAccuracy:
|
||||
locationAccuracy = .reducedAccuracy
|
||||
@unknown default:
|
||||
locationAccuracy = .unknown
|
||||
}
|
||||
}
|
||||
|
||||
// when a location is available for the first time, we fly to it
|
||||
func mapView(_ mapView: MLNMapView, didUpdate _: MLNUserLocation?) {
|
||||
if pannedToUserLocation {
|
||||
return
|
||||
}
|
||||
guard let userLocation = mapView.userLocation else {
|
||||
print("User location is currently not available.")
|
||||
return
|
||||
}
|
||||
mapView.fly(to: MLNMapCamera(lookingAtCenter: userLocation.coordinate, altitude: 100_000, pitch: 0, heading: 0))
|
||||
}
|
||||
}
|
||||
|
||||
struct LocationPrivacyExample: UIViewRepresentable {
|
||||
@Binding var mapView: MLNMapView
|
||||
@Binding var locationAccuracy: LocationAccuracyState
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(mapView: $mapView, locationAccuracy: $locationAccuracy)
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> MLNMapView {
|
||||
let mapView = MLNMapView()
|
||||
mapView.showsUserLocation = true
|
||||
mapView.delegate = context.coordinator
|
||||
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_: MLNMapView, context _: Context) {}
|
||||
}
|
||||
|
||||
struct LocationPrivacyExampleView: View {
|
||||
@State private var mapView = MLNMapView()
|
||||
@State var locationAccuracy: LocationAccuracyState = .unknown
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
LocationPrivacyExample(mapView: $mapView, locationAccuracy: $locationAccuracy)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
if locationAccuracy == LocationAccuracyState.reducedAccuracy {
|
||||
Button("Request Precise Location") {
|
||||
handleButtonPress(mapView: mapView)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleButtonPress(mapView: MLNMapView) {
|
||||
print("Requesting precice location")
|
||||
switch locationAccuracy {
|
||||
case .reducedAccuracy:
|
||||
let purposeKey = "MLNAccuracyAuthorizationDescription"
|
||||
mapView.locationManager.requestTemporaryFullAccuracyAuthorization!(withPurposeKey: purposeKey)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #-end-example-code
|
||||
@@ -6,7 +6,7 @@ import UIKit
|
||||
struct MapLibreApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
MapLibreNavigationView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
platform/ios/app-swift/Sources/MapLibreNavigationView.swift
Normal file
20
platform/ios/app-swift/Sources/MapLibreNavigationView.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import MapLibre
|
||||
import SwiftUI
|
||||
|
||||
struct MapLibreNavigationView: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
NavigationLink("SimpleMap") {
|
||||
SimpleMap().edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
NavigationLink("LineTapMap") {
|
||||
LineTapMap().edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
NavigationLink("LocationPrivacyExample") {
|
||||
LocationPrivacyExampleView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import MapLibre
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// #-example-code(SimpleMapView)
|
||||
struct SimpleMapView: UIViewRepresentable {
|
||||
// #-example-code(SimpleMap)
|
||||
struct SimpleMap: UIViewRepresentable {
|
||||
func makeUIView(context _: Context) -> MLNMapView {
|
||||
let mapView = MLNMapView()
|
||||
return mapView
|
||||
@@ -13,9 +13,3 @@ struct SimpleMapView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
// #-end-example-code
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
SimpleMapView().edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,23 @@
|
||||
*/
|
||||
|
||||
import fs from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
/** documentation files that will be updated */
|
||||
const documentationPaths = [
|
||||
"platform/ios/MapLibre.docc/GettingStarted.md",
|
||||
"platform/ios/MapLibre.docc/LineOnUserTap.md",
|
||||
];
|
||||
/**
|
||||
* @param {string} baseDir
|
||||
* @param {(path: string) => boolean} predicate
|
||||
* @returns paths
|
||||
*/
|
||||
async function getFilePaths(baseDir, predicate) {
|
||||
const entries = (await fs.readdir(baseDir, { withFileTypes: true }))
|
||||
return entries.map((entry) => join(entry.path, entry.name)).filter(predicate);
|
||||
}
|
||||
|
||||
/** source files that contain examples */
|
||||
const examplePaths = [
|
||||
"platform/ios/app-swift/Sources/SimpleMapView.swift",
|
||||
"platform/ios/app-swift/Sources/LineTapMapView.swift"
|
||||
];
|
||||
// documentation files that will be updated and source files that contain examples
|
||||
const [documentationPaths, examplePaths] = await Promise.all([
|
||||
getFilePaths("platform/ios/MapLibre.docc", path => path.endsWith(".md")),
|
||||
getFilePaths("platform/ios/app-swift/Sources", path => path.endsWith(".swift"))
|
||||
]);
|
||||
|
||||
/**
|
||||
* Parses a source file and looks for examples fenced in
|
||||
@@ -67,7 +72,8 @@ async function loadExamples() {
|
||||
for (const examplePath of examplePaths) {
|
||||
const newExamples = await parseExampleCode(examplePath);
|
||||
if (Object.keys(newExamples).length === 0) {
|
||||
throw new Error(`File '${examplePath}' does not contain any examples`);
|
||||
// no examples found
|
||||
continue;
|
||||
}
|
||||
examples = {...newExamples, ...examples};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user