didSelect 上的 MapKit/MKMapViewDelegate:如果启用了用户位置,则底部工作表不会打开

Posted

技术标签:

【中文标题】didSelect 上的 MapKit/MKMapViewDelegate:如果启用了用户位置,则底部工作表不会打开【英文标题】:MapKit/MKMapViewDelegate on didSelect: bottom sheet doesn't open if user location is enabled 【发布时间】:2020-07-27 15:56:51 【问题描述】:

我正在尝试在 ios 上重建 Apple Maps 的注释选择行为。如果我点击地图图钉,它应该会打开一个底部表格,其中包含有关该地点的更多信息。

虽然已经对所有基本组件进行了编码,但如果在地图上启用了用户位置,我的底部工作表不会打开。

User Location disabled: bottom sheet opens correctly

User Location enabled: bottom sheet doesn’t open, but WHY?

如果启用了用户位置,为什么我的底部工作表打不开?我真的很感激一些意见。谢谢!

如何复制它:

    根据您要测试的内容更改MapMainView.swift 中的变量showCurrentLocation。 请不要忘记添加 Info.plist 条目 Privacy - Location When In Use Usage DescriptionPrivacy - Location Always Usage Description 以访问您设备的本地位置。

ContentView.swift

import SwiftUI

struct ContentView: View 
    var body: some View 
        MapMainView()
    

MapMainView.swift

import Foundation
import SwiftUI
import MapKit

struct MapMainView: View 
    let showCurrentLocation = false
    let locationFetcher = LocationFetcher()
    @State var selectedPin: MapPin? = nil
    @State var isBottomSheetOpen: Bool = false
    @State var examplePins = [MapPin]()

    var body: some View 
        GeometryReader  geometry in
            ZStack() 
                VStack() 
                    Spacer()
                    BottomSheetView(isOpen: self.$isBottomSheetOpen, maxHeight: geometry.size.height * 0.3) 
                        Text(String(self.selectedPin?.title ?? "no title")).foregroundColor(Color.black)
                    
                
                .edgesIgnoringSafeArea(.all)
                .zIndex(1)
            
                MapView(locationFetcher: self.locationFetcher, showCurrentLocation: self.showCurrentLocation, displayedPins: self.$examplePins, selectedPin: self.$selectedPin, isBottomSheetOpen: self.$isBottomSheetOpen)
                    .edgesIgnoringSafeArea(.all)
                    .onAppear
                        var currentLat: Double
                        var currentLng: Double
                    
                        if self.showCurrentLocation 
                            currentLat = self.locationFetcher.getCurrentCoordinates()?.latitude ?? 46.9457590197085
                            currentLng = self.locationFetcher.getCurrentCoordinates()?.longitude ?? 8.007923669708498
                         else 
                            currentLat = 46.9457590197085
                            currentLng = 8.007923669708498
                        
                    
                        self.examplePins.append(MapPin(coordinate: CLLocationCoordinate2D(latitude: currentLat - 0.004, longitude: currentLng - 0.002), title: "First Pin"))
                        self.examplePins.append(MapPin(coordinate: CLLocationCoordinate2D(latitude: currentLat + 0.002, longitude: currentLng + 0.002), title: "Second Pin"))
                        self.examplePins.append(MapPin(coordinate: CLLocationCoordinate2D(latitude: currentLat - 0.002, longitude: currentLng + 0.004), title: "Third Pin"))
                
            
        
    


class MapPin: NSObject, MKAnnotation 
    let coordinate: CLLocationCoordinate2D
    let title: String?

    init(coordinate: CLLocationCoordinate2D, title: String? = nil) 
        self.coordinate = coordinate
        self.title = title
    

MapView.swift

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable 
    let locationFetcher: LocationFetcher
    let showCurrentLocation: Bool
    @Binding var displayedPins: [MapPin]
    @Binding var selectedPin: MapPin?
    @Binding var isBottomSheetOpen: Bool

    func makeCoordinator() -> Coordinator 
        Coordinator(self)
    

    func makeUIView(context: Context) -> MKMapView 
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
    
        if showCurrentLocation 
            mapView.showsUserLocation = true
            self.locationFetcher.attemptLocationAccess()
            centerLocation(mapView: mapView, locationCoordinate: locationFetcher.getCurrentCoordinates())
         else 
            centerLocation(mapView: mapView, locationCoordinate: CLLocationCoordinate2D(latitude: 46.9457590197085, longitude: 8.007923669708498))
        
    
        return mapView
    

    func updateUIView(_ mapView: MKMapView, context: Context) 
        if self.displayedPins.count != mapView.annotations.count 
            mapView.removeAnnotations(mapView.annotations)
            mapView.addAnnotations(self.displayedPins)
        
    

    func centerLocation(mapView: MKMapView, locationCoordinate: CLLocationCoordinate2D?) 
        if locationCoordinate != nil 
            let kilometerRadius = 1.5;
            let scalingFactor = abs((cos(2 * Double.pi * locationCoordinate!.latitude / 360.0)));
            let span = MKCoordinateSpan(latitudeDelta: kilometerRadius/111, longitudeDelta: kilometerRadius/(scalingFactor * 111))
            let region = MKCoordinateRegion(center: locationCoordinate!, span: span)
            mapView.setRegion(region, animated: true)
        
    


class Coordinator: NSObject, MKMapViewDelegate 
    var parent: MapView

    init(_ parent: MapView) 
        self.parent = parent
    

    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) 
        guard let pin = view.annotation as? MapPin else 
            return
        
        mapView.setCenter(pin.coordinate, animated: true)
    
        DispatchQueue.main.async 
           self.parent.selectedPin = pin
           self.parent.isBottomSheetOpen = true
        
    


    func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) 
        guard (view.annotation as? MapPin) != nil else 
            return
        
    
        DispatchQueue.main.async 
           self.parent.selectedPin = nil
           self.parent.isBottomSheetOpen = false
        
    

BottomSheetView.swift

import Foundation
import SwiftUI

struct BottomSheetView<Content: View>: View 
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @Binding var isOpen: Bool

    let maxHeight: CGFloat
    let minHeight: CGFloat
    let content: Content

    @GestureState private var translation: CGFloat = 0

    private var offset: CGFloat 
        isOpen ? 0 : maxHeight - minHeight
    

    private var indicator: some View 
        RoundedRectangle(cornerRadius: Constants.RADIUS)
            .fill(Color.black)
            .frame(
                width: Constants.INDICATOR_WIDTH,
                height: Constants.INDICATOR_HEIGHT
        ).onTapGesture 
            self.isOpen.toggle()
        
    

    init(isOpen: Binding<Bool>, maxHeight: CGFloat, @ViewBuilder content: () -> Content) 
        self.minHeight = maxHeight * Constants.MIN_HEIGHT_RATIO
        self.maxHeight = maxHeight
        self.content = content()
        self._isOpen = isOpen
    

    var body: some View 
        GeometryReader  geometry in
            VStack(spacing: 0) 
                self.indicator.padding()
                self.content
            
            .frame(width: geometry.size.width, height: self.maxHeight, alignment: .top)
            .background(Color.white)
            .cornerRadius(Constants.RADIUS)
            .frame(height: geometry.size.height, alignment: .bottom)
            .offset(y: max(self.offset + self.translation, 0))
            .animation(.interactiveSpring())
            .gesture(
                DragGesture().updating(self.$translation)  value, state, _ in
                    state = value.translation.height
                .onEnded  value in
                    let snapDistance = self.maxHeight * Constants.SNAP_RATIO
                    guard abs(value.translation.height) > snapDistance else 
                        return
                    
                    self.isOpen = value.translation.height < 0
                
            )
           
     


enum Constants 
    static let RADIUS: CGFloat = 16
    static let INDICATOR_HEIGHT: CGFloat = 6
    static let INDICATOR_WIDTH: CGFloat = 60
    static let SNAP_RATIO: CGFloat = 0.25
    static let MIN_HEIGHT_RATIO: CGFloat = 0

LocationFetcher.swift

import CoreLocation
import SwiftUI

class LocationFetcher: NSObject, CLLocationManagerDelegate 
    let locationManager = CLLocationManager()

    override init() 
        super.init()
        locationManager.delegate = self
    

    func attemptLocationAccess() 
        guard CLLocationManager.locationServicesEnabled() else 
          return
        
        locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        if CLLocationManager.authorizationStatus() == .notDetermined 
          locationManager.requestWhenInUseAuthorization()
         else 
          locationManager.requestLocation()
        
        locationManager.startUpdatingLocation()
    

    func getCurrentCoordinates() -> CLLocationCoordinate2D? 
        return locationManager.location?.coordinate
    

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])    
    

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) 
        fatalError("error: \(error.localizedDescription)")
    

或者你可以在这里下载所有文件https://gofile.io/d/LSfli5

【问题讨论】:

【参考方案1】:

我自己发现了问题。不仅是我的 MapPins,而且蓝色的位置圈也算作 mapView 注释。因此,如果启用了定位服务,我的 updateUIView() 会删除并在每个 didSelect 上添加我的所有注释,因为 self.displayedPins.count != mapView.annotations.count 始终为 false,这会导致底部表格突然消失。

在过滤注释后它终于起作用了:

func updateUIView(_ mapView: MKMapView, context: Context) 
    let displayedMapPins = mapView.annotations.filter  annotation in
        return annotation is MapPin
    
    
    if self.mapPinsToDisplay.count != displayedMapPins.count 
        mapView.removeAnnotations(displayedMapPins)
        mapView.addAnnotations(self.mapPinsToDisplay)
    

【讨论】:

以上是关于didSelect 上的 MapKit/MKMapViewDelegate:如果启用了用户位置,则底部工作表不会打开的主要内容,如果未能解决你的问题,请参考以下文章

如何在表格视图的 didSelect 上显示弹出框

tableView didSelect 单元格不起作用

如何禁用为 UITableViewCell 的某些部分调用 DidSelect?

使用 didSelect 方法在 viewDidLoad 之后在单元格中显示图像

使用 SearchController 后的 DidSelect 导致快速崩溃

表点击和 didselect 方法冲突