如何在swiftUI中添加移回用户位置按钮?

时间:2019-10-15 15:47:09

标签: swift mapkit swiftui

我对Swift和SwiftUI还是很陌生,我想在mapview的顶部添加一个用户跟踪按钮,以便在点击时用户的当前位置可以回到屏幕中央。我已经有了mapview和按钮,但是无法正常工作。

这是ContentView.swift文件,我被加在****位置:

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()
          }
        }
      }
    }

这是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()

        }
    }
}

3 个答案:

答案 0 :(得分:0)

我遇到了同样的问题,几个小时后,我设法按要求工作了

  • 启动时,它会显示用户的位置(如果他授权了该位置),但是会等待他点击按钮来激活关注。
  • 如果他授权并点击按钮,它就会跟随他。
  • 如果由于某种原因该应用无法访问他的位置(拒绝访问,限制…),它将告诉他在“设置”中进行更改并将其重定向到该位置。
  • 如果他在运行应用程序时更改了授权,它将自动更改。
  • 如果地图跟随他,则按钮消失。
  • 如果他拖动地图(停止跟踪),该按钮将再次出现。
  • (该按钮支持黑暗模式)

我真的希望它能对您有所帮助,我想有一天会把它放在GitHub上。如果可以,我将在此处添加链接。

首先,我不想每次都重新创建MKMapView,所以我将其放在名为MapViewContainer

的类中
  

尽管??‍♂️

,但我认为这不是一个好习惯      

如果您不想使用它,只需将@EnvironmentObject private var mapViewContainer: MapViewContainer中的let mapView = MKMapView(frame: .zero)替换为MKMapViewRepresentable(并将mapViewContainer.mapView更改为mapView

import MapKit

class MapViewContainer: ObservableObject {

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

}

然后,我可以创建我的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)
            }
        }

    }

}

最后,将其放入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())
    }

}

答案 1 :(得分:0)

对于其他在实现方面遇到困难的人,@ Remi b。回答是否是一个非常可行的选择,我花了很多时间试图将其实施到我的项目中,但最终还是采取了另一种方式。这样,位置按钮就可以正常工作并循环显示位置跟踪类型,并且按钮图像类似于Maps App中的图像。这就是我的目的:

在添加基本的MKMapView之后,我为UIViewRepresentable创建了一个MKUserTrackingButton,如下所示: 注意:@EnvironmentObject var viewModel: ViewModel包含我的mapView

struct LocationButton: UIViewRepresentable {
    @EnvironmentObject var viewModel: ViewModel

    func makeUIView(context: Context) -> MKUserTrackingButton {
        return MKUserTrackingButton(mapView: viewModel.mapView)
    }

    func updateUIView(_ uiView: MKUserTrackingButton, context: Context) { }
}

然后在我的SwiftUI ContentView中或要将跟踪按钮添加到的任何位置:

struct MapButtonsView: View {
    @EnvironmentObject var viewModel: ViewModel
    
    var body: some View {
        ZStack {
            VStack {
                Spacer()
                Spacer()
                HStack {
                    Spacer()
                    VStack(spacing: 12) {
                        
                        Spacer()
                        
                        // User tracking button
                        LocationButton()
                            .frame(width: 20, height: 20)
                            .background(Color.white)
                            .padding()
                            .cornerRadius(8)
                        
                    }
                    .padding()
                }
            }
        }
    }
}

答案 2 :(得分:0)

这是我返回用户位置的方法。我使用 Notification 来通知 Mapview。我在我的 github

中上传了演示项目

首先定义一个通知名称

extension Notification.Name {
  static let goToCurrentLocation = Notification.Name("goToCurrentLocation")
}

其次,在 MapView 协调器中监听通知:


import SwiftUI
import MapKit
import Combine

struct MapView: UIViewRepresentable {
    private let mapView = MKMapView()

    func makeUIView(context: Context) -> MKMapView {
        mapView.isRotateEnabled = false
        mapView.showsUserLocation = true
        mapView.delegate = context.coordinator
        let categories: [MKPointOfInterestCategory] = [.restaurant, .atm, .hotel]
        let filter = MKPointOfInterestFilter(including: categories)
        mapView.pointOfInterestFilter = filter
        return mapView
    }
    
    func updateUIView(_ mapView: MKMapView, context: Context) {
    }
    func makeCoordinator() -> Coordinator {
        .init(self)
    }
    
    class Coordinator: NSObject, MKMapViewDelegate {
        private var control: MapView
        private var lastUserLocation: CLLocationCoordinate2D?
        private var subscriptions: Set<AnyCancellable> = []

        init(_ control: MapView) {
            self.control = control
            super.init()
            
            NotificationCenter.default.publisher(for: .goToCurrentLocation)
                .receive(on: DispatchQueue.main)
                .sink { [weak self] output in
                    guard let lastUserLocation = self?.lastUserLocation else { return }
                    control.mapView.setCenter(lastUserLocation, animated: true)
                }
                .store(in: &subscriptions)
        }
        
        func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
            lastUserLocation = userLocation.coordinate
        }
    }
}

最后,当用户点击按钮时发送通知:

var body: some View {
        return ZStack {
            MapView()
                .ignoresSafeArea(.all)
                .onAppear() {
                    viewModel.startLocationServices()
                    goToUserLocation()
                }
            
            VStack {
                Spacer()
                
                Button(action: {
                    goToUserLocation()
                }, label: {
                    Image(systemName: "location")
                        .font(.title2)
                        .padding(10)
                        .background(Color.primary)
                        .clipShape(Circle())
                })
                
            }
            .frame(maxWidth: .infinity, alignment: .trailing)
            .padding()
        }


    private func goToUserLocation() {
        NotificationCenter.default.post(name: .goToCurrentLocation, object: nil)
    }