How to add a move back to user location button in swiftUI?

耗尽温柔 提交于 2020-01-14 03:27:28

问题


I am quite new to Swift and SwiftUI, and I want to add a user tracking button on top of the mapview, so user's current location can be back in the center of the screen when tapped. I have already have the mapview and the button, but failed to make it work.

here is the ContentView.swift file, and I am stuck at the place with ****:

import SwiftUI
import MapKit

struct ContentView: View {
    var body: some View {
      ZStack {
        MapView(locationManager: $locationManager)
        .edgesIgnoringSafeArea(.bottom)

        HStack {
          Spacer()
          VStack {
            Spacer()
            Button(action: {
                ******
            }) {
              Image(systemName: "location")
                .imageScale(.small)
                .accessibility(label: Text("Locate Me"))
                .padding()
            }
            .background(Color.white)
            .cornerRadius(10)
            .padding()
          }
        }
      }
    }

And here is the MapView.swift:

import SwiftUI
import MapKit
import CoreLocation
import ECMapNavigationAble


struct MapView: UIViewRepresentable, ECMapNavigationAble{

    var locationManager = CLLocationManager()

    func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
        MKMapView()
    }

    func updateUIView(_ view: MKMapView, context: UIViewRepresentableContext<MapView>){
        view.showsUserLocation = true
        view.isPitchEnabled = false

        self.locationManager.requestAlwaysAuthorization()
        self.locationManager.requestWhenInUseAuthorization()

        if CLLocationManager.locationServicesEnabled() {
            self.locationManager.desiredAccuracy = kCLLocationAccuracyBest

        }

        if let userLocation = locationManager.location?.coordinate {
            let userLocationEC = ECLocation(coordinate : userLocation, type: .wgs84)
            let viewRegion = MKCoordinateRegion(center: userLocationEC.gcj02Coordinate, latitudinalMeters: 200, longitudinalMeters: 200)
            view.userTrackingMode = .follow
            view.setRegion(viewRegion, animated: true)
        }

        DispatchQueue.main.async{
            self.locationManager.startUpdatingLocation()

        }
    }
}

回答1:


I had the same problem, and after a few hours, I managed to get something working as required

  • At launch, it shows the user location if he authorised it, but waits for him to tap the button to activate the follow.
  • If he authorised and taps the button, it follows him.
  • If for some reason the app cannot access his location (denied access, restrictions…), it will tell him to change it in Settings and redirect him there.
  • If he changes the authorization while running the app, it will change automatically.
  • If the map is following him, the button disappears.
  • If he drags the map (stop following), the button appears again.
  • (The button supports Dark Mode)

I really hope it'll help you, I think I'll put it on GitHub some day. I'll add the link here if I do.

First, I didn't want to recreate the MKMapView every time, so I put it in a class I called MapViewContainer

I don't think it's a good practice, though 🤷🏻‍♂️

If you don't want to use it, just replace @EnvironmentObject private var mapViewContainer: MapViewContainer by let mapView = MKMapView(frame: .zero) in MKMapViewRepresentable (and change the mapViewContainer.mapView for mapView)

import MapKit

class MapViewContainer: ObservableObject {

    @Published public private(set) var mapView = MKMapView(frame: .zero)

}

Then, I could create my MapViewRepresentable

import SwiftUI
import MapKit

// MARK: - MKMapViewRepresentable

struct MKMapViewRepresentable: UIViewRepresentable {

    var userTrackingMode: Binding<MKUserTrackingMode>

    @EnvironmentObject private var mapViewContainer: MapViewContainer

    func makeUIView(context: UIViewRepresentableContext<MKMapViewRepresentable>) -> MKMapView {
        mapViewContainer.mapView.delegate = context.coordinator

        context.coordinator.followUserIfPossible()

        return mapViewContainer.mapView
    }

    func updateUIView(_ mapView: MKMapView, context: UIViewRepresentableContext<MKMapViewRepresentable>) {
        if mapView.userTrackingMode != userTrackingMode.wrappedValue {
            mapView.setUserTrackingMode(userTrackingMode.wrappedValue, animated: true)
        }
    }

    func makeCoordinator() -> MapViewCoordinator {
        let coordinator = MapViewCoordinator(self)
        return coordinator
    }

    // MARK: - Coordinator

    class MapViewCoordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {

        var control: MKMapViewRepresentable

        let locationManager = CLLocationManager()

        init(_ control: MKMapViewRepresentable) {
            self.control = control

            super.init()

            setupLocationManager()
        }

        func setupLocationManager() {
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            locationManager.pausesLocationUpdatesAutomatically = true
        }

        func followUserIfPossible() {
            switch CLLocationManager.authorizationStatus() {
            case .authorizedAlways, .authorizedWhenInUse:
                control.userTrackingMode.wrappedValue = .follow
            default:
                break
            }
        }

        private func present(_ alert: UIAlertController, animated: Bool = true, completion: (() -> Void)? = nil) {
            // UIApplication.shared.keyWindow has been deprecated in iOS 13,
            // so you need a little workaround to avoid the compiler warning
            // https://stackoverflow.com/a/58031897/10967642

            let keyWindow = UIApplication.shared.windows.first { $0.isKeyWindow }
            keyWindow?.rootViewController?.present(alert, animated: animated, completion: completion)
        }

        // MARK: MKMapViewDelegate

        func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
            #if DEBUG
            print("\(type(of: self)).\(#function): userTrackingMode=", terminator: "")
            switch mode {
            case .follow:            print(".follow")
            case .followWithHeading: print(".followWithHeading")
            case .none:              print(".none")
            @unknown default:        print("@unknown")
            }
            #endif

            if CLLocationManager.locationServicesEnabled() {
                switch mode {
                case .follow, .followWithHeading:
                    switch CLLocationManager.authorizationStatus() {
                    case .notDetermined:
                        locationManager.requestWhenInUseAuthorization()
                    case .restricted:
                        // Possibly due to active restrictions such as parental controls being in place
                        let alert = UIAlertController(title: "Location Permission Restricted", message: "The app cannot access your location. This is possibly due to active restrictions such as parental controls being in place. Please disable or remove them and enable location permissions in settings.", preferredStyle: .alert)
                        alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
                            // Redirect to Settings app
                            UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
                        })
                        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))

                        present(alert)

                        DispatchQueue.main.async {
                            self.control.userTrackingMode.wrappedValue = .none
                        }
                    case .denied:
                        let alert = UIAlertController(title: "Location Permission Denied", message: "Please enable location permissions in settings.", preferredStyle: .alert)
                        alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
                            // Redirect to Settings app
                            UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
                        })
                        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
                        present(alert)

                        DispatchQueue.main.async {
                            self.control.userTrackingMode.wrappedValue = .none
                        }
                    default:
                        DispatchQueue.main.async {
                            self.control.userTrackingMode.wrappedValue = mode
                        }
                    }
                default:
                    DispatchQueue.main.async {
                        self.control.userTrackingMode.wrappedValue = mode
                    }
                }
            } else {
                let alert = UIAlertController(title: "Location Services Disabled", message: "Please enable location services in settings.", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
                    // Redirect to Settings app
                    UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
                })
                alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
                present(alert)

                DispatchQueue.main.async {
                    self.control.userTrackingMode.wrappedValue = mode
                }
            }
        }

        // MARK: CLLocationManagerDelegate

        func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
            #if DEBUG
            print("\(type(of: self)).\(#function): status=", terminator: "")
            switch status {
            case .notDetermined:       print(".notDetermined")
            case .restricted:          print(".restricted")
            case .denied:              print(".denied")
            case .authorizedAlways:    print(".authorizedAlways")
            case .authorizedWhenInUse: print(".authorizedWhenInUse")
            @unknown default:          print("@unknown")
            }
            #endif

            switch status {
            case .authorizedAlways, .authorizedWhenInUse:
                locationManager.startUpdatingLocation()
                control.mapViewContainer.mapView.setUserTrackingMode(control.userTrackingMode.wrappedValue, animated: true)
            default:
                control.mapViewContainer.mapView.setUserTrackingMode(.none, animated: true)
            }
        }

    }

}

And finally, put it in a SwiftUI View

import SwiftUI
import CoreLocation.CLLocation
import MapKit.MKAnnotationView
import MapKit.MKUserLocation

struct MapView: View {

    @State private var userTrackingMode: MKUserTrackingMode = .none

    var body: some View {
        ZStack {
            MKMapViewRepresentable(userTrackingMode: $userTrackingMode)
                .environmentObject(MapViewContainer())
                .edgesIgnoringSafeArea(.all)
            VStack {
                if !(userTrackingMode == .follow || userTrackingMode == .followWithHeading) {
                    HStack {
                        Spacer()
                        Button(action: { self.followUser() }) {
                            Image(systemName: "location.fill")
                                .modifier(MapButton(backgroundColor: .primary))
                        }
                        .padding(.trailing)
                    }
                    .padding(.top)
                }
                Spacer()
            }
        }
    }

    private func followUser() {
        userTrackingMode = .follow
    }

}

fileprivate struct MapButton: ViewModifier {

    let backgroundColor: Color
    var fontColor: Color = Color(UIColor.systemBackground)

    func body(content: Content) -> some View {
        content
            .padding()
            .background(self.backgroundColor.opacity(0.9))
            .foregroundColor(self.fontColor)
            .font(.title)
            .clipShape(Circle())
    }

}


来源:https://stackoverflow.com/questions/58398437/how-to-add-a-move-back-to-user-location-button-in-swiftui

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!