前言
ios中可以直接使用苹果官方提供的map——MapKit。在SwiftUI中如何使用MapKit网上有也有不少文章,但是大部分不详细,大部分只是简单的展示出地图。所以本文来详细的讲解一下如何使用MapKit的各项功能。
官方地址:https://developer.apple.com/documentation/mapkit
1、Map
在SwiftUI中可以直接使用Map组件,如下:
import SwiftUI
import MapKit
struct ContentView: View {
@State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 39.915, longitude: 116.397), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
@State var trackingMode = MapUserTrackingMode.follow
var body: some View {
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, userTrackingMode: $trackingMode)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
这样可以直接显示地图,其中coordinateRegion就是当前的地图区域(中心点,经纬度的跨度等等)。
但是地图的各种功能怎么使用?答案是不能使用。通过官网的介绍:
Map
A view that displays an embedded map interface.
…
Overview
A map view displays a region. Use this native SwiftUI view to optionally configure user-allowed interactions, display the user’s location, and track location.
Also create maps that display annotations at specific locations.
These annotated maps use one of the following types of annotation views:
MapPin
MapMarker
MapAnnotation
Maps only show annotation views of the same type, backed by a single collection.
可以看到这个view只是一个简单版本,可以展示一些预定义好的annotation,其他功能都无法使用。
所以一般情况下我们不使用这个,而是使用MKMapView。
2、MKMapView
MKMapView就无法直接像Map那样使用了,因为它并不继承View,而是继承UIView,所以需要用UIViewRepresentable来包装,代码如下:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
typealias UIViewType = MKMapView
func makeUIView(context: Context) -> MKMapView {
return MKMapView()
}
func updateUIView(_ uiView: MKMapView, context: Context) {
uiView.showsUserLocation = true
let loc = CLLocationCoordinate2D(latitude: 39.915352, longitude: 116.397105)
let region = MKCoordinateRegion(center: loc, span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
uiView.setRegion(uiView.regionThatFits(region), animated: true)
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
同样为MKMapView设置了初始的Region。然后我们就可以直接在SwiftUI中使用MapView了。
3、MKMapView的一些设置
下面是一些比较常用的设置
- mapType:地图类型。包括标准、卫星等,具体看MKMapType这个枚举即可。
- isZoomEnabled:允许缩放
- isScrollEnabled:地图是否可以拖动
- isRotateEnabled:地图是否可以旋转
- isPitchEnabled:是否可以调整俯视视角
- showsScale:是否展示比例尺(只有当缩放地图的时候才显示,缩放完成后会自动隐藏)
- showsCompass:是否展示罗盘(当正上方是正北的时候罗盘自动隐藏)
- showsUserLocation:显示当前位置(需要有定位权限并且用户已经允许该权限,否则不显示)
- showsTraffic:显示交通信息
- showsBuildings:是否显示建筑(当camera视角不在正上方时,如果这个为true则会显示建筑物。视角在正上方则无差别)
- showsPointsOfInterest:是否显示POI(这个方法已经不推荐使用了)
这里简单说一下ios simulator的使用,因为缩放、旋转、俯视需要双指操作,在simulator中需要按住option键可以进行双指相对操作(即两个点相对运动),比如缩放、旋转;同时按住option+shift键才可以进行双指同向操作,比如俯视,或者比如在桌面上切屏。
4、处理Map上的操作
前面我们展示了地图,如果要在地图上的进行操作,比如点击,获取中心点等,这就需要使用UIViewRepresentable的Coordinator——协调器。
我们创建一个类,继承NSObject和MKMapViewDelegate,这里MKMapViewDelegate是一个protocol,定义了一些地图的操作有关的函数,比如
optional func mapViewDidChangeVisibleRegion(_ mapView: MKMapView)
是地图可见范围改变时回调,比如拖动、缩放等行为。在这里我们作一些更新地图的操作,比如可以获取地图的中心点,代码如下:
class MapCoordinator : NSObject, MKMapViewDelegate{
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
print(mapView.centerCoordinate)
}
}
MKMapViewDelegate还有很多函数对应不同的回调,这里就不一一列举了。
然后我们需要重写UIViewRepresentable的makeCoordinator函数,新建一个MapCoordinator类的对象并返回,这样就可以通过context.coordinator来获取这个对象了。
然后将这个协调器绑定到map上,代码如下:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
func makeCoordinator() -> MapCoordinator {
MapCoordinator()
}
typealias UIViewType = MKMapView
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator //绑定协调器到map
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
uiView.showsUserLocation = true
uiView.showsScale = true
uiView.showsBuildings = false
let loc = CLLocationCoordinate2D(latitude: 39.915352, longitude: 116.397105)
let region = MKCoordinateRegion(center: loc, span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
uiView.setRegion(uiView.regionThatFits(region), animated: true)
}
class MapCoordinator : NSObject, MKMapViewDelegate{
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
print(mapView.centerCoordinate)
}
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
这样当拖动或缩放地图的时候,就会回调到mapViewDidChangeVisibleRegion,然后打印出当前中心点的经纬度。
点击操作
MKMapViewDelegate只能被动的接受地图的回调,如果我们主动操作怎么办?比如点击地图获取点击位置的经纬度(或者添加地图覆盖物),这时候需要为地图设置GestureRecognizer,并通过协调器进行处理。
首先创建一个UITapGestureRecognizer,并添加到map上,如下:
let gRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(MapCoordinator.touch(gestureReconizer:)))
mapView.addGestureRecognizer(gRecognizer)
这里UITapGestureRecognizer的action执行的是MapCoordinator的touch函数,所以我们需要给MapCoordinator添加一个touch函数:
class MapCoordinator : NSObject, MKMapViewDelegate{
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
print(mapView.centerCoordinate)
}
@objc func touch(gestureReconizer: UITapGestureRecognizer) {
print('click')
}
}
这个函数必须添加@objc标识,表示它可以供OC进行调用,因为最终实际上是OC调用的UITapGestureRecognizer的action。
然后我们点击地图,就可以看到打印出click了。
注意:只能在MKMapViewDelegate中使用@objc标识,如果我们直接给MapView添加一个touch函数,然后赋值给UITapGestureRecognizer,就会报错,因为UITapGestureRecognizer的action必须是一个被@objc标识的函数,而在MapView中不能给函数添加@objc。所以我们要通过MKMapViewDelegate来实现。
现在我们可以响应点击了,但是怎么获取点击位置的经纬度?touch函数只传入了一个UITapGestureRecognizer对象,通过它可以获取到点击的位置,如下:
let point = gestureReconizer.location(in: gestureReconizer.view)
但是这个位置是屏幕位置,如果想换成经纬度还需要MKMapView才行,代码如下:
let point = gestureReconizer.location(in: gestureReconizer.view)
let loc = mMapView?.convert(point, toCoordinateFrom: mMapView)
但是在MapCoordinator中无法得到MKMapView对象,这里我添加了一个initMap(_ mapView : MKMapView)函数,在makeUIView阶段执行这个函数,将MKMapView对象传入,这样就可以获取经纬度了,最终整体代码如下:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
func makeCoordinator() -> MapCoordinator {
MapCoordinator()
}
typealias UIViewType = MKMapView
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
//添加GestureRecognizer
let gRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(MapCoordinator.touch(gestureReconizer:)))
mapView.addGestureRecognizer(gRecognizer)
//绑定协调器
mapView.delegate = context.coordinator
//传入MKMapView对象
context.coordinator.initMap(mapView)
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
uiView.showsUserLocation = true
uiView.showsScale = true
uiView.showsBuildings = false
let loc = CLLocationCoordinate2D(latitude: 39.915352, longitude: 116.397105)
let region = MKCoordinateRegion(center: loc, span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
uiView.setRegion(uiView.regionThatFits(region), animated: true)
}
class MapCoordinator : NSObject, MKMapViewDelegate{
var mMapView : MKMapView?
func initMap(_ mapView : MKMapView) {
mMapView = mapView
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
print(mapView.centerCoordinate)
}
@objc func touch(gestureReconizer: UITapGestureRecognizer) {
let point = gestureReconizer.location(in: gestureReconizer.view)
let loc = mMapView?.convert(point, toCoordinateFrom: mMapView)
print(loc!)
}
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
5、总结
通过这两天的体验,感觉自带的MapKit使用起来并不是很便捷,而且我希望可以点击选中地图上的兴趣点,但是经过查找并没有发现任何可用的api。于是请教了ios的大佬,大佬说MapKit很少使用,因为MapKit功能不全(比如之前根本不支持路线,是近期才新增的),所以国内开发一般还是使用百度或高德。再联想这几天使用MapKit的各种问题,我果断放弃继续深入的想法。