swiftui - 如何将 MKUserTrackingButton 链接到 SwiftUI map View

标签 swiftui

在我的项目中,我使用 SwiftUI:

Map(coordinateRegion: $region)

我添加了一个 MKUserTrackingButton:

struct UserTrackingButton: UIViewRepresentable {
func makeUIView(context: Context) -> MKUserTrackingButton {
   MKUserTrackingButton(mapView: context.environment.mkMapView)
}

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

}

但是它不起作用。那么如何将我的 SwiftUI map View 链接到此按钮?

最佳答案

在创建 MapView 的方法中,使用 MapView 作为引用创建 MKUserTrackingButton,然后将该按钮作为 subview 添加到 map View 中。所以像这样:

func makeUIView(context: Context) -> MKMapView {
  let mapView = MKMapView(frame: UIScreen.main.bounds)

  mapView.delegate = context.coordinator

  // set mapView properties as you need them

  let trackingButton = MKUserTrackingButton(mapView: mapView) 
  mapView.addSubview(trackingButton)

  // positioning the button on the right size with a bit
  // of autolayout constraints.  Adjust constants to 
  // your liking.
  trackingButton.translatesAutoresizingMaskIntoConstraints = false
  trackingButton.trailingAnchor.constraint(equalTo: mapView.layoutMarginsGuide.trailingAnchor, constant: -16).isActive = true
  trackingButton.topAnchor.constraint(equalTo: mapView.layoutMarginsGuide.topAnchor, constant: 42).isActive = true

  return mapView
}

我使用自动布局约束来定位按钮,具体取决于 map 上可能发生的其他情况,您可能需要调整这些和/或设置按钮的 x、y、宽度、高度属性。

编辑:2022 年 6 月 8 日

上面是一个非常令人不满意的答案,部分原因是,trackingButton 对象是 mkmapview 的 subview ,与 SwifUI 的布局机制不太兼容(实际上根本不与 SWIFTUI 兼容)。解决这个问题的一个更好的方法是通过 MapView 的模型类,尽管仍然感觉太老套了。这里是添加 MapType 按钮的简短代码布局。标准 map 类型按钮(例如跟踪按钮)不存在,因此这是提供跟踪按钮的更通用方法。

MapTypeButton 是一个简单的 UIButton,专门用于循环浏览 MKMapType 枚举选项以及您在按钮中选择的图像。由于 MKMapView 和 UIButton 都不属于 SWIFTUI 组件集,因此首先需要将它们包装在 UIViewRepresentable 中。这采用熟悉的形式:

Struct abcView:UIViewRepresentable {
  func makeUIView(context: Context) -> abcView {
  }

  func updateUIView(_ uiView: abcView, context: Context)
  }

  class Coordinator: NSObject {
    init(_ parent: abcView) {
      self.parent = parent
      super.init()
    }
    func makeCoordinator() -> Coordinator {
      Coordinator(self)
    }
  }
}

显然用你的类名替换“abcView”。下面给出了 MKMapView 和 MapType 目标 UIButton 的骨架版本以及它们的模型类,从而生成 MapView 和 MapTypeButtonView。

struct MapView : UIViewRepresentable {
  var mapViewModel:MapViewModel
  
  func makeUIView(context: Context) -> MKMapView {
    
    let mkMapView = MKMapView(frame: UIScreen.main.bounds)
    
    mkMapView.delegate = context.coordinator
    
    // any mkMapView set up stuff goes here
    mkMapView.region = mapViewModel.region
    mkMapView.showsCompass = false
    
    mapViewModel.mkMapView = mkMapView. // <— forms the bridge to MapTypeButtonView
    return mkMapView
  }
  
  func updateUIView(_ uiView: MKMapView, context: Context) {
  }
  
  class Coordinator: NSObject, MKMapViewDelegate {
    var parent: MapView
    
    init(_ parent: MapView) {
      self.parent = parent
      super.init()
    }
    
    func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
    }
  }
  
  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
}




struct MapTypeButtonView: UIViewRepresentable {
  @Binding var mkMapView:MKMapView?
  
  func makeUIView(context:Context) -> MapTypeButton {
    let button = MapTypeButton()
    return button
  }
  
  func updateUIView(_ uiView: MapTypeButton, context: Context) {
    uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
    uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
    uiView.mkMapView = mkMapView. // <— makes MKMapView available to the button
  }
  
  class Coordinator : NSObject {
    var parent : MapTypeButtonView
    
    init (_ parent:MapTypeButtonView) {
      self.parent = parent
      super.init()
    }
  }
  
  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
}


final class MapViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
  let startLocation = CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060)
  let defaultSpan = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
  @Published var region = MKCoordinateRegion(center: startLocation, span: defaultSpan)
  
  @Published var mkMapView:MKMapView?
  
  @discardableResult
  func checkIfLocationServicesEnabled() -> Bool {    // <— customize this to your liking
    if CLLocationManager.locationServicesEnabled() {
      locationMgr = CLLocationManager()
      if locationMgr == nil { return false }
      locationMgr!.desiredAccuracy = kCLLocationAccuracyBest
      locationMgr!.delegate = self
      locationMgr!.requestLocation()
      locationMgr!.startUpdatingLocation()
      locationMgr!.startUpdatingHeading()
      
      return true
    }
    else {
      // show alert ?
      return false
    }
  }
  
  private func checkLocationMgrAuthorization() {
    guard let mgr = locationMgr else { return }
    
    switch mgr.authorizationStatus {
    case .notDetermined: mgr.requestWhenInUseAuthorization()
    case .restricted: print("ALERT: LOCMGR RESTRICTED")
    case .denied: print("ALERT: LOCMGR DENIED")
    case .authorizedAlways:
      region = MKCoordinateRegion(center: mgr.location!.coordinate, span: self.defaultSpan)
      
    case .authorizedWhenInUse:
      // Info.plist must contain both “NSLocationAlwaysAndWhenInUseUsageDescription”
      // and “NSLocationWhenInUseUsageDescription”
      mgr.requestAlwaysAuthorization()
      
    @unknown default:
      break
    }
  }
  
  
  func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    checkLocationMgrAuthorization()
  }
  
  func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    //
  }
  
}

我设置了 MapType 按钮,使其仅在 .standard 和 .hybrid map 类型之间循环,调用 nextMapType() 进行循环。根据您的喜好进行更改。

class MapTypeButton : UIView {
  let buttonWidthHeight = 32
  weak open var mkMapView: MKMapView?
  private var mapTypes:[MKMapType] = [.standard, .hybrid]
  private var mapTypeImageNames:[String] = [“map”, "map.fill"]
  private var button:UIButton?
  
  override var intrinsicContentSize: CGSize {
    return CGSize(width: buttonWidthHeight, height: buttonWidthHeight)
  }
  
  var mapType : MKMapType {
    get { return mkMapView?.mapType ?? .standard }
    set { mkMapView?.mapType = newValue }
  }
  
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  
  init() {
    super.init(frame: CGRect(x: 0, y: 0, width: buttonWidthHeight, height: buttonWidthHeight))
    self.setup()
  }
  
  private func setup() {
    let action = UIAction(image: UIImage(systemName: mapTypeImageNames[0]),
                          handler: { _ in self.nextMapType() })
    self.button = UIButton(primaryAction: action)
    
    if let btn = self.button {
      self.addSubview(btn)
      
      btn.contentVerticalAlignment = .fill
      btn.contentHorizontalAlignment = .fill
      
      btn.translatesAutoresizingMaskIntoConstraints = false
      _ = btn.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
      _ = btn.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
      _ = btn.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
      _ = btn.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
    }
  }
  
  func nextMapType() {
    guard let mapView = self.mkMapView else { return }
    if let idx = self.mapTypes.firstIndex(of: mapView.mapType) {
      var i = mapTypes.distance(from: mapTypes.startIndex, to: idx)
      i += 1
      i = i % self.mapTypes.count
      mapView.mapType = mapTypes[i]
      self.button?.setImage(UIImage(systemName: mapTypeImageNames[i]), for:.normal)
      self.button?.contentMode = .scaleAspectFit
      self.button?.imageView?.contentMode = .scaleAspectFit
    }
  }
}

最后,将它们放在一个内容 View 中,其中 map 位于背景中,顶部的 MapTypeButton(以及用于显示和讲述的其他一些按钮)水平 float 在中心);

struct ContentView: View {
  @StateObject var mapViewModel = MapViewModel()
  
  var body: some View {
    ZStack {
      MapView(mapViewModel: mapViewModel)
      HStack {
        Button(action: {}) { Image(systemName: "heart.fill") }
        Spacer()
        MapTypeButtonView(mkMapView: $mapViewModel.mkMapView)
      }
    }
  }
}

您现在可以为 TrackingButton 创建类似的设置(我不会使用 MKTrackingButton)。 MapTypeButtonView 应在 SWIFTUI 中正常运行并遵循所有预期的布局要求,但可以直接连接到 MKMapView 来控制它。

是的,有大量代码,其中大部分也需要用于其他目的,因此并非完全出乎意料,但仍然比应有的更麻烦。希望它对您或其他人有所帮助。

关于swiftui - 如何将 MKUserTrackingButton 链接到 SwiftUI map View ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71151969/

相关文章:

swift - 有没有办法在 SwiftUI 中更改列表行的背景?

ios - 以编程方式更改为 SwiftUI 中的另一个选项卡

ios - 在 SwiftUI 中创建项目后,我们可以将 Root View Controller 更改为 Storybord 吗?

ios - 有没有办法使用 Geometry Reader 将我的 SwiftUI 图像的宽度定义为相对于屏幕尺寸?

ios - swift 用户界面 4 : navigationDestination( )'s destination view isn' t updated when state changes

swift - 将数据从 UIKit 传递到 SwiftUI(容器 UIHostingController)

swiftui - 为什么我没有得到键盘 SwiftUI TextField

SwiftUI 预览显示错误 - 无法在此文件中预览 - 当前目标需要调整build设置

swiftui - 在 SwiftUI 中将多行文本包装在一个形状内

ios - SwiftUI 中 UIView readableContentGuide 等价物是什么