一文掌握新框架:关于MapKit的核心概念

812 阅读9分钟

这是我投稿【2024年创作者训练营第四期】首篇文章,也希望借此机会作为动力,加快自己的学习进度!

image.png

前言

主要就是自己在学习的过程中发现目前很多关于 MapKit的讲解教程中使用的 API 大多数都是在 ios17 已经废弃,所以自己想梳理下当前框架的使用,有问题地方欢迎指出 👋!

你可能已经知道但是可能忘记的知识点:经度纬度 & 解密

经度纬度
CleanShot 2024-08-21 at 23.06.22@2x.pngCleanShot 2024-08-21 at 23.06.41@2x.png

当前文章主要讲解 MapKitSwiftUI中的使用场景,在AppKit中的使用持续更新中...

概述

MapKit主要作用就是在您的应用程序中显示地图或卫星图像,你可以跟地图上的位置进行交互(例如:缩放,旋转,拖拽,调整俯仰视角),通过交互你可以在地图上获得你想要的信息。

使用 MapKit 为您的应用程序提供地图和位置信息的位置感。您可以使用 MapKit 框架来:

  • 将地图直接嵌入到应用程序的窗口和视图中。
  • 向地图添加注释和叠加层以标出兴趣点。
  • 响应用户与众所周知的兴趣点、地理特征和边界的交互。
  • 提供文本补全功能,以便用户轻松搜索目的地或兴趣点。

MapKit for SwiftUI 允许您利用从卫星图像以及丰富的3D透视图像的地图样式来呈现生动的地图。使用 MapContentBuilder,您可以配置地图以显示标记和注释视图,或者对于更专业的内容,您可以设计自己的 SwiftUI 视图以放置在地图上。为了增加更多的交互性,MapKit for SwiftUI 支持叠加来突出显示地图上的区域,使您能够使用 MapPolyline 制作路径方向的动画,或者让人们可以轻松地通过可点击的兴趣点更深入地挖掘地面细节。使用您的应用程序的用户还可以使用 LookAroundPreviewLook around 查看器在街道层面进行探索。

我这边主要从以下五个角度去进行讲解当前 MapKit的具体使用.

地图构造器 MapContentBuilder

首先我们可以通过 Xcode查看到目前MapKitAPI文档,显示之前的接口已经标注在 ios17 废弃,请使用 MapContentBuilder,我们可以先看看新的 APIMapcontentBuilder 倒地是怎么样的。

CleanShot 2024-08-22 at 10.33.11@2x.png

通过上图标注我们可以知道 MapContentBuilderMap 函数的尾随闭包,且其闭包的返回值为泛型 C,C遵循MapContent协议。

所以其构造器除了构造器前参数外,其尾随闭包MapContentBuilder内支持的 SwiftUI 符合 MapContent协议类型的 SwiftUI组件。符合其协议的 SwiftUI 有如下,都可以在其构造器中使用!

  1. Annotation 用户可以自定标注内容的标注地图位置
  2. MapCircle:以地理坐标为中心,可配置半径的圆形图像叠加图。
  3. MapPolygon:封闭多边形叠加图
  4. MapPolyline:由一条或多条 [相连线段] 组成的开放多边形叠加图
  5. UserAnnotation:表示和管理地图上用户当前位置的标注
  6. Marker:标记地图位置的气球状标注。

现在我们示例代码演示如何使用 MapContentBuilder 构造器闭包创建地图上的标注:

  1. 首先我们定义下位置坐标信息
**extension** CLLocationCoordinate2D {
    /* 故宫坐标 latitude: 纬度, 经度:longitude*/
    static let palaceMuseum = CLLocationCoordinate2D(latitude: 39.9163, longitude: 116.3972)
    /* 漓江坐标1 */
    static let liRiver1 = CLLocationCoordinate2D(latitude: 25.2740, longitude: 110.2901)
    /* 漓江坐标2 */
    static let liRiver2 = CLLocationCoordinate2D(latitude: 25.2740, longitude: 110.2902)
    /* 漓江坐标3 */
    static let liRiver3 = CLLocationCoordinate2D(latitude: 25.27409, longitude: 110.29009)
    /* 漓江坐标4 */
    static let liRiver4 = CLLocationCoordinate2D(latitude: 25.27419, longitude: 110.29009)
    /* 漓江坐标5 */
    static let liRiver5 = CLLocationCoordinate2D(latitude: 25.27409, longitude: 110.29020)
}
  1. 详细代码
struct MapPracticesContent: View {
    var body: some View {
        Map() {
            Annotation("漓江臻美🚀", coordinate: .liRiver1, anchor: .zero) {
                Image(systemName: "paperplane.circle")
                    .foregroundStyle(Color.green)
                    .fontWeight(.semibold)
                    .padding(5)
                    .background(Color.black)
                    .clipShape(Circle())
            }
            Marker("漓江旁边", systemImage: "backpack", coordinate: .liRiver2)
            Marker("标记 3", systemImage: "graduationcap.fill", coordinate: .liRiver3)
            Marker("标记 4", systemImage: "graduationcap.fill", coordinate: .liRiver4)
            Marker("标记 5", systemImage: "paperclip.circle", coordinate: .liRiver5)
            MapCircle(center: .liRiver4, radius: CLLocationDistance(1))
                .foregroundStyle(Color.blue.opacity(0.6))

            MapPolygon(coordinates: [
                CLLocationCoordinate2D.liRiver2,
                CLLocationCoordinate2D.liRiver5,
                CLLocationCoordinate2D.liRiver3])
            .foregroundStyle(Color.orange.opacity(0.7))
        }
    }
}
CleanShot 2024-08-22 at 12.27.42@2x.png

我们简单介绍了下 MapContentBuilder, 现在我们熟悉下Map的其他几个参数:

Map(position: Binding<MapCameraPosition>, 
    bounds: MapCameraBounds?, 
    interactionModes: MapInteractionModes, 
    selection: Binding<MapFeature?>, 
    scope: Namespace.ID?, 
    content: ()->C)
  • postion:位置信息,通过MapCameraPosition结构体去进行初始化位置以及确定地图中的位置,我们可以通过传入状态值跟地图进行数据的双向绑定,实时更改地图中显示的相机窗口位置。

MapCameraPosition是一个结构主要是描述如何在地图中定位地图相机的位置。当您将MapCameraPosition作为绑定传递给地图时,地图会调整其相机以框定请求的内容,或与MapCameraPosition指定的相机完全匹配。如果一个人以移动地图的方式与地图交互,地图会将位置重置为指定positedByUser的值。 它具有很多属性方法,用的比较多的是.region以及.camera,这两方法都是返回MapCaremaPosition,后续将使用其一进行演示。

  • bounds: 设置最大距离以及最小距离,用于定义地图的相机视图边界(视角限制),可以限制用户在地图上浏览的区域,确保他们无法将地图平移或缩放到指定区域之外。

假设你有一个应用,需要限制用户只能在特定区域内浏览地图,例如某个城市或特定的地理范围。你可以通过设置 MapCameraBounds 来实现这一点。

Map(
...
/* 
center: 定义地图视角的中心点,在这个示例中设为旧金山。
span: 定义中心点的经度以及纬度位置的偏差范围(纬度差以及经度差)。
minimumDistance: 最小缩放距离(以米为单位)
maximumDistance: 最大的缩放距离(以米为单位)
主要这里创建时不要使用MKCoordinateSpanMake,因为已经被废弃,直接使用MKCoordinateSpan即可
*/
bounds: MapCameraBounds( 
    centerCoordinateBounds: MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), 
        span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.2)
    ),
    minimumDistance: 100, 
    maximumDistance: 100),
...
)
  • interactionModes:跟地图的交互形式,MapInteractionModes类型定义了如平移、俯仰、旋转和缩放交互形式。默认情况下,它启用所有可用的交互类型。
  • selection:绑定地图上被选择的符合MapFeature特征的元素。

MapFeature: 用于在地图上添加描述和操作地图上的可进行点击的特定元素(例如:标记、覆盖物、区域等),它是MapKit上的功能。

MapKit 中,你可以使用以下组件来添加和管理地图上的特性:

  1. MKPointAnnotation: 用于在地图上添加标记。
  2. MKOverlay: 用于在地图上添加覆盖物(例如,圆形、矩形、线条等)。
  3. MKGeodesicPolyline: 用于绘制地理折线。
  4. MKCircle: 用于绘制圆形覆盖物。

关于 MapFeature 更多细节后续讲解...其实目前还没研究明白,基础使用还可以!后续继续更新~

  • scope: 定义命名空间,主要作用就是我们可以创建命名空间绑定到地图组件上,后续 SwiftUI可以跟其进行关联从而达到控制地图操作的作用!

地图样式 MapStyle

指定地图的样式,当前遵循MapStyle结构类型,MapStyle有预先配置好的地图样式可以选择,如果没有特别要求,基本可以满足。

  • .standard: 标准样式
  • .imagery: 基于图像的风格,如使用卫星图像的风格。
  • .hybrid: 混合风格,例如使用一个地区的卫星图像,并在其上添加道路和路名信息层。

其次我们可以添加额外配置,控制是否渲染高度(2D, 3D切换)、地图强调风格,显示兴趣点和显示交通情况等。

Map(){
...
}
.mapStyle(
    MapStyle.standard(
        elevation: .flat, 
        emphasis: .muted, 
        pointsOfInterest: PointOfInterestCategories(arrayLiteral: .publicTransport), 
        showsTraffic: true))

地图交互

Map地图上的交互主要通过配置interactionModes进行设置,有下面几种类型:

  • .all: 允许所有的交互类型
  • .pin: 允许用户平移到地图的不同区域
  • .zoom: 允许用户放大或缩小地图位置
  • .pitch: 允许用户设置地图的间距,从不同的角度查看地图
  • .rotate: 允许用户对地图进行旋转操作
Map(
    ...
    interactionModes: [.pitch, .pin],
    ...){}

地图控件

我们都知道在现有的地图应用中大都会有这些地图的配件,如指南针、当前用户位置、倾斜、比例尺和缩放等控件,在SwiftUI中都实现为SwiftUI视图形式,这就表示我们可以将他们放置的位置不一定要在Map结构里,但是我们需要通过定义一个地图范围命名空间,以将它们与它们控制的地图关联起来。

通过命名空间关联地图控件

import SwiftUI
import MapKit

extension CLLocationCoordinate2D {
    static let liRiver1 = CLLocationCoordinate2D(latitude: 25.2740, longitude: 110.2901)
}
struct MapPracticesContent: View {
    @Namespace var mapSpace
    
    @State private var position: MapCameraPosition = MapCameraPosition.camera(MapCamera(centerCoordinate: .liRiver1, distance: 0.1))
    let mapCameraBounds: MapCameraBounds = MapCameraBounds(centerCoordinateBounds: MKCoordinateRegion(center: .liRiver1, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.2)), minimumDistance: 100, maximumDistance: 100)
    
    let mapInteractionModes: MapInteractionModes = .all
    
    @State private var selection: MapFeature? = .none
    var body: some View {
        Map(position: $position, bounds: mapCameraBounds, interactionModes: mapInteractionModes, selection: $selection, scope: mapSpace) {
            Marker(item: MKMapItem(placemark: MKPlacemark(coordinate: .liRiver1)))
        }
        .mapScope(mapSpace)
        /* 指南针 */
        MapCompass(scope: mapSpace)
    }
}

CleanShot 2024-08-22 at 16.34.34@2x.png

其实还有一种标准的方式统一位置定义我们的地图空间,不需要借助命名空间的帮助:

Map(){}
    .mapControls({
        /* 指南针 */
        MapCompass()
        /* 仅适用于 WatchOS的指南针 */
        #if os(watchOS)
        MapLocationCompass()
        #endif
        #if os(macOS)
        /* 控制地图是否显示高度,也就是 2D / 3D */
        MapPitchSlider()
        /* 允许用户调节缩放比例 */
        MapZoomStepper()
        #endif
        MapPitchToggle()
        /* 用于显示地图比例尺的一个控件。比例尺可以帮助用户了解地图上显示的距离,与实际世界中的距离的比例关系 */
        MapScaleView(anchorEdge: .trailing)
        /* 设置关联的地图位置为当前用户的位置: 右上角的箭头 */
        MapUserLocationButton()
    })
CleanShot 2024-08-22 at 16.52.05@2x.png

简单应用演示

后续更新...

欢迎你的阅读查看,希望对你有帮住,文档持续更新中!