Skip to content

Commit

Permalink
User Location and Location Privacy Example iOS (maplibre#2389)
Browse files Browse the repository at this point in the history
  • Loading branch information
louwers authored May 13, 2024
1 parent b61c87b commit 29ccd3f
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 34 deletions.
4 changes: 2 additions & 2 deletions platform/ios/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ 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
```

Prefix your documentation code block with

````md
<!-- include-example(LineTapMapView) -->
<!-- include-example(LineTapMap) -->

```swift
...
Expand Down
4 changes: 2 additions & 2 deletions platform/ios/MapLibre.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions platform/ios/MapLibre.docc/LineOnUserTap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
}

Expand Down
132 changes: 132 additions & 0 deletions platform/ios/MapLibre.docc/LocationPrivacyExample.md
Original file line number Diff line number Diff line change
@@ -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.

![](ImpreciseLocation.png)

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

![](PreciseLocationRequestPopup.png)

<!-- 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
}
}
}
```
1 change: 1 addition & 0 deletions platform/ios/MapLibre.docc/MapLibre.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Powerful, free and open-source mapping toolkit with full control over data sourc

- <doc:GettingStarted>
- <doc:LineOnUserTap>
- <doc:LocationPrivacyExample>

## Topics

Expand Down
2 changes: 1 addition & 1 deletion platform/ios/app-swift/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
11 changes: 10 additions & 1 deletion platform/ios/app-swift/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -36,4 +45,4 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
</plist>
8 changes: 4 additions & 4 deletions platform/ios/app-swift/Sources/LineTapMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
}

Expand Down
102 changes: 102 additions & 0 deletions platform/ios/app-swift/Sources/LocationPrivacyExample.swift
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion platform/ios/app-swift/Sources/MapLibreApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import UIKit
struct MapLibreApp: App {
var body: some Scene {
WindowGroup {
ContentView()
MapLibreNavigationView()
}
}
}
20 changes: 20 additions & 0 deletions platform/ios/app-swift/Sources/MapLibreNavigationView.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
}
Loading

0 comments on commit 29ccd3f

Please sign in to comment.