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

问题描述 投票:0回答:4

我对 Swift 和 SwiftUI 很陌生,我想在地图视图的顶部添加一个用户跟踪按钮,这样当点击时用户的当前位置可以回到屏幕中央。我已经有了 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()

        }
    }
}
swift mapkit swiftui
4个回答
6
投票

我有同样的问题,几个小时后,我设法让一些东西按要求工作

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

我真的希望它能帮到你,我想有一天我会把它放在 GitHub 上。如果我这样做,我会在这里添加链接。

首先,我不想每次都重新创建 MKMapView,所以我把它放在一个我称为
MapViewContainer

的类中

虽然我不认为这是一个好习惯🤷🏻u200d♂️

如果你不想使用它,只需将

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

0
投票

对于其他在实施方面遇到困难的人,@Remi b。回答是否是一个非常可行的选择,我花了很多时间试图将其实施到我的项目中,但我最终采用了不同的方式。这允许位置按钮工作并循环仅类型的位置跟踪和按钮图像,就像在地图应用程序中一样。这就是我的选择:

添加我的基本

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

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



0
投票

这里是我如何添加一个按钮以使用 SwiftUI 和 MapKit 跳回用户的位置。

这段代码并不完美(解包变量可以更好地处理)但它会帮助像我这样的其他菜鸟。

关键是:

  1. 使用@StateObject 作为位置数据。这样 Map 就可以观察到 这个对象随着它的变化而更新地图。
  2. 在 MapViewModel
     上添加一个调用 
    updateMapToUsersLocation
  3. 函数的按钮
  4. 您需要
    @Published var region
    区域,以便当它更改时地图将更新。
  5. region.center = locationManager.location!.coordinate
    locationManager 有用户位置,将发布区域设置为等于该位置,地图正在观察该对象,因此它会更新。

主要内容查看

//
//  ContentView.swift
//  MapExperiment001
//
//  Created by @joshdance
//

import SwiftUI
import MapKit

struct MapView: View {
    
    @StateObject private var mapViewModel = MapViewModel()
    
    var body: some View {
        
        ZStack {
            
            Map(coordinateRegion: $mapViewModel.region, showsUserLocation: true)
                .ignoresSafeArea()
                .accentColor(Color(.systemPink))
                .onAppear {
                    mapViewModel.checkIfLocationManagerIsEnabled()
                }
            
            VStack {
                Spacer()
                
                HStack {
                    VStack(alignment: .leading) {
                        Text("User Lat: \(String(format: "%.5f", mapViewModel.userLat))")
                        Text("User Long: \(String(format: "%.5f", mapViewModel.userLong))")
                    }.padding()
                    
                    Spacer()
                    
                    Button(action: {
                        mapViewModel.updateMapToUsersLocation()
                    }) {
                        Image(systemName: "location.fill")
                            .foregroundColor(.blue)
                            .padding()
                    }.frame(width: 64, height: 64)
                        .background(Color.white)
                    .clipShape(Circle())
                    .padding()
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}

地图视图模型

//
//  MapViewModel.swift
//  MapExperiment001
//
//  Created by @joshdance
//

import MapKit

enum MapDetails {
    static let startingLocation = CLLocationCoordinate2D(latitude: 40.2338, longitude: -111.6585)
    static let defaultSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
}

final class MapViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
    
    @Published var userLat: Double = 1.0
    @Published var userLong: Double = 2.0
    
    @Published var region = MKCoordinateRegion(
        center: MapDetails.startingLocation,
        span: MapDetails.defaultSpan)
    
    var locationManager: CLLocationManager? //optinal because they can turn off location services.
    
    func checkIfLocationManagerIsEnabled() {
        if CLLocationManager.locationServicesEnabled() == true {
            locationManager = CLLocationManager()
            //when CLLocation manager is created it fires delegate 'locationManagerDidChangeAuthorization' event.
            locationManager!.delegate = self
            
        } else {
            //TODO show alert letting them know it is off and how to turn it on.
        }
    } // end check
    
    func updateMapToUsersLocation() {
        guard let locationManager = locationManager else { return }
        
        let coordinate = locationManager.location!.coordinate
        region.center = coordinate
        userLat = coordinate.latitude
        userLong = coordinate.longitude
    }
    
    //check app permission
    private func checkAppLocationPermission() {
        guard let locationManager = locationManager else { return }
    
        switch locationManager.authorizationStatus {
            
        case .notDetermined:
            //ask permission
            locationManager.requestWhenInUseAuthorization()
        case .restricted:
            //restricted due to parental controls etc
            print("Your location is restricted maybe due to parental controls")
        case .denied:
            print("You have denied this app location permission, you can turn it back on in Settings.")
        case .authorizedAlways:
            updateMapToUsersLocation()
        case .authorizedWhenInUse:
            updateMapToUsersLocation()
        @unknown default:
            break
        }
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        checkAppLocationPermission()
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.