第69天 项目 14 第二部分
在本项目技术概述的第二部分,我们将介绍iOS上两个非常重要的框架:用于在应用中渲染地图的MapKit,以及用于使用Touch ID(指纹识别)和Face ID(面容识别)的LocalAuthentication。
想必你不难理解,位置信息、指纹和面容识别数据对用户而言具有高度私密性,这意味着我们在任何时候都必须尊重这些数据。请记住,用户信任我们会始终以最谨慎、最有诚意的态度对待他们的数据,因此,我们应当树立这样一种理念:隐私、安全和信任是核心价值观,而非可有可无的附加项。
据称,猫王埃尔维斯·普雷斯利曾说过:“价值观就像指纹,每个人的都独一无二,但你所触碰过的一切,都会留下它的痕迹。”而本项目恰好处于“价值观”与“指纹(此处双关,也指生物识别)”的交集核心,所以请务必专注——这类内容至关重要。
今天你需要学习两个主题,通过这些内容,你将掌握如何在SwiftUI应用中嵌入地图、如何使用Face ID解锁应用等知识。
- 在SwiftUI中集成MapKit
- 在SwiftUI中使用Touch ID和Face ID
记得向外界分享你的进展——你正在取得实实在在的进步。
在SwiftUI中集成MapKit
作者:Paul Hudson 2023年12月23日
自2007年首款iPhone设备问世以来,地图功能就一直是iPhone的核心功能之一,而其底层框架也几乎在同一时期向开发者开放。更便捷的是,苹果提供了一个SwiftUI的Map视图,该视图完美封装了底层地图框架,让我们能够将地图、标注等元素与SwiftUI视图层级中的其他元素无缝结合。
我们先从简单的功能入手:仅显示一张地图。地图及其所有配置数据都来自一个专门的框架——MapKit,因此第一步我们需要导入这个框架:
import MapKit接下来,只需添加以下代码,就能在SwiftUI视图中放置一张地图:
Map()这样就足以在屏幕上显示地图了,你可以尝试运行应用,并花点时间了解模拟器中的一些关键操作快捷键:
- 按住Option键,可触发双指捏合操作。按住Option键的同时点击并拖动,虚拟手指会相互靠近或远离(用于缩放地图)。
- 按住Option键和Shift键,可触发双指平移操作。按住这两个组合键并上下拖动,可调整地图的倾斜角度。
- 你也可以模拟单指缩放:先轻点一下,然后再次轻点并上下拖动。
掌握了地图的基本操作后,还有大量自定义选项可供探索。
例如,你可以使用mapStyle()修饰符来控制地图的显示样式。若要显示卫星地图,可使用以下代码:
Map()
.mapStyle(.imagery)若要同时显示卫星地图和街道地图(混合模式),代码如下:
Map()
.mapStyle(.hybrid)若要在混合模式基础上添加真实的海拔信息,呈现3D地图效果,可使用:
.mapStyle(.hybrid(elevation: .realistic))你还可以调整用户与地图的交互方式,比如是否允许用户缩放地图或旋转地图视角。例如,我们可以创建一个始终以特定位置为中心,但允许用户调整旋转角度和缩放比例的地图:
Map(interactionModes: [.rotate, .zoom])若要完全禁止交互(地图固定不变),只需将交互模式设为空数组:
Map(interactionModes: [])以上都是较为简单的自定义选项,不过还有三个选项需要多花些心思:控制地图位置、添加标注以及处理点击事件。
首先,你可以自定义相机位置。既可以设置地图的初始显示位置,也可以通过绑定(binding)跟踪地图的实时位置变化。
例如,我们可以创建一个常量属性,存储伦敦的位置信息,并将跨度(span)设置为1度×1度:
let position = MapCameraPosition.region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275),
span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1)
)
)然后,将这个位置用作地图的初始显示位置:
Map(initialPosition: position)需要注意的是,上述值仅作为初始位置。如果想要实时更改地图位置,需要将其标记为@State属性,然后以绑定的形式传入。
首先,将其定义为@State属性:
@State private var position = MapCameraPosition.region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275),
span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1)
)
)接着,以绑定形式传入地图:
Map(position: $position)现在,该位置已存储为程序状态,我们可以通过添加按钮来跳转到其他位置。例如,将地图包裹在VStack中,并在下方添加以下按钮:
HStack(spacing: 50) {
Button("巴黎") {
position = MapCameraPosition.region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1)
)
)
}
Button("东京") {
position = MapCameraPosition.region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 35.6897, longitude: 139.6922),
span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1)
)
)
}
}尽管我们现在已将绑定传入地图,但无法直接读取地图移动时的实时位置。不过,我们可以使用onMapCameraChange()修饰符,该修饰符会在地图位置发生变化时(即时变化或移动结束后)通知我们。
例如,我们可以在用户停止拖动地图后获取更新,并将其打印出来:
Map(position: $position)
.onMapCameraChange { context in
print(context.region)
}此外,也可以设置为持续获取更新:
Map(position: $position)
.onMapCameraChange(frequency: .continuous) { context in
print(context.region)
}你可能会认为持续模式总是更优,但实际情况并非如此。例如,如果你需要根据用户定位的地图位置进行搜索,这类操作更适合在用户停止移动地图后执行。
接下来要介绍的第二个自定义功能是添加标注。
要实现这一功能,至少需要三个步骤(具体步骤取决于你的需求):定义一个包含位置信息的新数据类型、创建该类型的数组(存储所有需要标注的位置)、将这些标注添加到地图中。无论你创建哪种用于存储位置的新数据类型,它都必须遵循Identifiable协议,以便SwiftUI能够唯一识别每个地图标记。
例如,我们可以先定义一个Location结构体:
struct Location: Identifiable {
let id = UUID()
var name: String
var coordinate: CLLocationCoordinate2D
}然后,定义一个位置数组,存储所有需要在地图上添加标注的位置:
let locations = [
Location(name: "白金汉宫", coordinate: CLLocationCoordinate2D(latitude: 51.501, longitude: -0.141)),
Location(name: "伦敦塔", coordinate: CLLocationCoordinate2D(latitude: 51.508, longitude: -0.076))
]第三步是关键:我们可以将这个位置数组作为内容传入Map视图。SwiftUI提供了多种内容类型,其中较为简单的是Marker(标记)——它是一个带有标题和经纬度坐标的气球状标记。
例如,我们可以在两个位置添加标记,代码如下:
Map {
ForEach(locations) { location in
Marker(location.name, coordinate: location.coordinate)
}
}运行代码后,你会在地图上看到两个红色气球标记,更便捷的是,地图会自动调整位置和缩放比例,确保两个标记都能显示在视野中。
如果你想更好地控制标记在地图上的显示样式,可以使用Annotation(注解)。它允许你自定义视图来替代系统默认的气球标记,并且你可以根据需要隐藏默认标题,用自定义内容替代,例如:
Annotation(location.name, coordinate: location.coordinate) {
Text(location.name)
.font(.headline)
.padding()
.background(.blue)
.foregroundStyle(.white)
.clipShape(.capsule)
}
.annotationTitles(.hidden)最后,你可以使用onTapGesture()来处理地图上的点击事件。该修饰符会返回用户点击的屏幕坐标(例如,距离顶部50个点,距离左侧100个点)。
要获取点击位置对应的地图实际位置,需要使用一个特殊的视图——MapReader。将MapReader包裹在地图外侧后,你会获得一个MapProxy对象,该对象能够实现屏幕坐标与地图坐标之间的相互转换。
使用方法如下:
MapReader { proxy in
Map()
.onTapGesture { position in
if let coordinate = proxy.convert(position, from: .local) {
print(coordinate)
}
}
}提示: .local表示我们正在转换地图本地坐标空间中的位置,即所处理的点击位置是相对于地图左上角(而非整个屏幕或其他坐标空间)的位置。
在SwiftUI中使用Touch ID和Face ID
作者:Paul Hudson 2023年12月23日
绝大多数苹果设备都标配了生物识别认证功能,这意味着这些设备可以通过指纹、面容甚至虹膜识别进行解锁。我们的应用也可以使用这一功能,确保敏感数据只有在用户通过有效认证后才能被访问。
LocalAuthentication是一个Objective-C API,不过在SwiftUI中使用它时,体验还算“不太糟糕”——相比我们之前接触过的某些框架,这已经算是不错的了。
在编写代码之前,你需要在项目设置中添加一个新的键,向用户说明获取Face ID访问权限的原因。有趣的是(原因只有苹果知晓),Touch ID的请求原因需要在代码中设置,而Face ID的请求原因则需要在项目设置中配置。
具体操作如下:选择当前的目标(target),进入“Info”标签页,右键单击现有键,然后选择“Add Row”(添加行)。在键列表中滚动,找到“Privacy - Face ID Usage Description”(隐私 - Face ID使用说明),并为其设置值“We need to unlock your data.”(我们需要解锁你的数据。)。
回到ContentView.swift文件,在文件顶部添加以下导入语句:
import LocalAuthentication至此,我们已准备好编写生物识别相关代码。
我之前提到过,在SwiftUI中使用这个API“还算不太糟糕”,原因如下:Swift开发者通常使用Error协议来表示运行时错误,而Objective-C则使用一个特殊的类NSError。我们需要将NSError传入函数,并允许函数在内部修改它(而非返回新值)——尽管这在Objective-C中是标准操作,但在Swift中却显得有些陌生,因此需要使用&来标记这种操作行为。
我们将编写一个authenticate()方法,将所有生物识别功能集中在这个方法中。实现该方法需要四个步骤:
- 创建
LAContext实例,该实例可用于查询生物识别状态并执行认证检查。 - 询问该上下文是否支持执行生物识别认证——这一点很重要,因为iPod touch既没有Touch ID,也没有Face ID。
- 如果支持生物识别,则启动实际的认证请求,并传入一个闭包(closure),用于在认证完成后执行。
- 当用户完成认证(成功或失败)后,会调用我们传入的完成闭包,并告知认证结果;若认证失败,还会返回错误信息。
请在ContentView中添加以下方法:
func authenticate() {
let context = LAContext()
var error: NSError?
// 检查是否支持生物识别认证
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
// 支持生物识别,执行认证请求
let reason = "We need to unlock your data."(我们需要解锁你的数据。)
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
// 认证已完成
if success {
// 认证成功
} else {
// 认证失败(存在问题)
}
}
} else {
// 不支持生物识别
}
}这个方法本身不会产生任何效果,因为它还没有与SwiftUI关联。要解决这个问题,我们需要添加一个状态属性,用于在认证成功时更新状态;同时,还需要添加onAppear()修饰符来触发认证流程。
首先,在ContentView中添加以下属性:
@State private var isUnlocked = false这个简单的布尔值将存储应用当前是否显示受保护数据的状态,因此在认证成功时,我们会将其设为true。将// authenticated successfully(// 认证成功)注释替换为以下代码:
isUnlocked = true最后,我们可以在body属性中显示当前的认证状态,并在视图出现时启动认证流程,代码如下:
VStack {
if isUnlocked {
Text("已解锁")
} else {
Text("已锁定")
}
}
.onAppear(perform: authenticate)运行应用时,你很可能只会看到“已锁定”文本,而没有其他反应。这是因为模拟器默认未启用生物识别功能,而且我们没有添加错误提示,所以认证失败时不会有任何反馈。
要在模拟器中测试Face ID,可进入“Features”(功能)菜单,选择“Face ID > Enrolled”(Face ID > 已注册),然后重新启动应用。此时,你应该会看到Face ID验证弹窗;若要模拟认证成功或失败,可再次进入“Features”菜单,选择“Face ID > Matching Face”(Face ID > 匹配面容)或“Non-matching Face”(不匹配面容)。
一切正常的话,Face ID验证弹窗会消失,下方会显示“已解锁”文本——我们的应用已检测到认证成功,现在可以正常使用了。
重要提示: 在使用生物识别认证时,务必提供备用认证方案,允许用户在生物识别失败时通过其他方式认证。通常的做法是添加一个密码输入界面,作为生物识别失败后的备用选项,但这需要你自行开发实现。