本章讲解 iOS 系统级框架的调用:推送通知(UserNotifications/APNs)、相机与照片库(PhotosUI/AVFoundation)、地图与定位(MapKit/CoreLocation)、生物识别(LocalAuthentication)、后台任务(BackgroundTasks)。
9.1 推送通知
本地通知
swift
复制代码
import UserNotifications
class NotificationManager: NSObject {
static let shared = NotificationManager()
// 请求通知权限
func requestPermission() async -> Bool {
do {
let granted = try await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .badge, .sound])
return granted
} catch {
return false
}
}
// 检查当前权限状态
func checkPermissionStatus() async -> UNAuthorizationStatus {
await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
}
// 发送即时本地通知
func sendLocalNotification(
title: String,
body: String,
userInfo: [String: Any] = [:],
delay: TimeInterval = 1,
badge: Int? = nil,
categoryIdentifier: String? = nil
) async throws -> String {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
content.userInfo = userInfo
if let badge { content.badge = NSNumber(value: badge) }
if let category { content.categoryIdentifier = category }
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: delay,
repeats: false
)
let identifier = UUID().uuidString
let request = UNNotificationRequest(
identifier: identifier,
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
return identifier
}
// 每日定时通知(每天 9:00)
func scheduleDailyReminder(title: String, body: String) throws {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
var components = DateComponents()
components.hour = 9
components.minute = 0
let trigger = UNCalendarNotificationTrigger(
dateMatching: components,
repeats: true // 每天重复
)
let request = UNNotificationRequest(
identifier: "daily-reminder",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request)
}
// 取消特定通知
func cancelNotification(identifier: String) {
UNUserNotificationCenter.current()
.removePendingNotificationRequests(withIdentifiers: [identifier])
}
// 取消所有通知
func cancelAllNotifications() {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}
}
// 注册通知交互类别(可操作通知按钮)
func registerNotificationCategories() {
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY_ACTION",
title: "回复",
textInputButtonTitle: "发送",
textInputPlaceholder: "输入回复..."
)
let markReadAction = UNNotificationAction(
identifier: "MARK_READ",
title: "标记已读",
options: []
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE",
actions: [replyAction, markReadAction],
intentIdentifiers: [],
options: []
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}
远程推送(APNs)
swift
复制代码
// AppDelegate 处理 APNs
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
application.registerForRemoteNotifications() // 注册远程推送
return true
}
// 获取到 Device Token
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("Device Token: \(token)")
Task {
// 上传 token 到服务器
try? await PushService.shared.registerToken(token)
}
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("推送注册失败:\(error)")
}
// App 在前台时收到通知
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completion: @escaping (UNNotificationPresentationOptions) -> Void
) {
completion([.banner, .badge, .sound]) // 前台也显示
}
// 用户点击通知
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completion: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// 点击通知主体
handleNotificationTap(userInfo: userInfo)
case "REPLY_ACTION":
if let textResponse = response as? UNTextInputNotificationResponse {
handleReply(text: textResponse.userText, userInfo: userInfo)
}
case "MARK_READ":
handleMarkRead(userInfo: userInfo)
default:
break
}
completion()
}
private func handleNotificationTap(userInfo: [AnyHashable: Any]) {
if let articleId = userInfo["article_id"] as? String {
AppRouter.shared.navigate(to: .articleDetail(articleId))
}
}
private func handleReply(text: String, userInfo: [AnyHashable: Any]) { }
private func handleMarkRead(userInfo: [AnyHashable: Any]) { }
}
9.2 相机与照片库
swift
复制代码
import PhotosUI
import AVFoundation
import SwiftUI
// PhotosPicker(SwiftUI 原生,iOS 16+)
struct PhotoPickerDemo: View {
@State private var selectedItems: [PhotosPickerItem] = []
@State private var images: [UIImage] = []
@State private var isLoading = false
var body: some View {
VStack {
// 显示选中图片
ScrollView(.horizontal) {
HStack(spacing: 8) {
ForEach(images, id: \.self) { image in
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipped()
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
.padding()
}
// 选择照片(最多 9 张)
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 9,
matching: .images,
photoLibrary: .shared()
) {
Label("选择照片", systemImage: "photo.stack")
.frame(maxWidth: .infinity)
.padding()
.background(.blue)
.foregroundStyle(.white)
.cornerRadius(12)
}
}
.overlay {
if isLoading { ProgressView() }
}
.onChange(of: selectedItems) { _, items in
Task { await loadImages(from: items) }
}
}
func loadImages(from items: [PhotosPickerItem]) async {
isLoading = true
var newImages: [UIImage] = []
for item in items {
if let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
// 压缩图片(上传前)
let compressed = image.jpegData(compressionQuality: 0.8)
.flatMap { UIImage(data: $0) } ?? image
newImages.append(compressed)
}
}
images = newImages
isLoading = false
}
}
// 相机拍照(UIImagePickerController 桥接)
struct CameraView: UIViewControllerRepresentable {
@Binding var capturedImage: UIImage?
@Environment(\.dismiss) var dismiss
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.cameraCaptureMode = .photo
picker.cameraFlashMode = .auto
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: Context) { }
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
var parent: CameraView
init(_ parent: CameraView) { self.parent = parent }
func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
parent.capturedImage = info[.editedImage] as? UIImage
?? info[.originalImage] as? UIImage
parent.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.dismiss()
}
}
}
9.3 地图与定位
swift
复制代码
import MapKit
import CoreLocation
// 定位管理器
@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
var currentLocation: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .notDetermined
var errorMessage: String?
private let clManager = CLLocationManager()
override init() {
super.init()
clManager.delegate = self
clManager.desiredAccuracy = kCLLocationAccuracyBest
}
func requestPermission() {
switch clManager.authorizationStatus {
case .notDetermined:
clManager.requestWhenInUseAuthorization()
case .denied, .restricted:
errorMessage = "定位权限被拒绝,请在设置中开启"
default:
break
}
}
func startUpdating() { clManager.startUpdatingLocation() }
func stopUpdating() { clManager.stopUpdatingLocation() }
func requestOnce() { clManager.requestLocation() }
// 反地理编码(坐标 → 地址)
func reverseGeocode(location: CLLocation) async -> String? {
let geocoder = CLGeocoder()
let placemarks = try? await geocoder.reverseGeocodeLocation(location)
let placemark = placemarks?.first
return [placemark?.subLocality, placemark?.locality, placemark?.administrativeArea]
.compactMap { $0 }.joined(separator: " ")
}
// 正地理编码(地址 → 坐标)
func geocode(address: String) async -> CLLocation? {
let geocoder = CLGeocoder()
let placemarks = try? await geocoder.geocodeAddressString(address)
return placemarks?.first?.location
}
// Delegate
nonisolated func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
Task { @MainActor in self.currentLocation = location }
}
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
Task { @MainActor in self.authorizationStatus = manager.authorizationStatus }
}
nonisolated func locationManager(_ manager: CLLocationManager,
didFailWithError error: Error) {
Task { @MainActor in self.errorMessage = error.localizedDescription }
}
}
// 地图视图(iOS 17 新 API)
struct MapDemo: View {
@State private var locationManager = LocationManager()
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
@State private var selectedPOI: PointOfInterest?
let pois = [
PointOfInterest(name: "东方明珠",
coordinate: CLLocationCoordinate2D(latitude: 31.2397, longitude: 121.4998),
category: .landmark),
PointOfInterest(name: "外滩",
coordinate: CLLocationCoordinate2D(latitude: 31.2399, longitude: 121.4905),
category: .attraction),
]
var body: some View {
Map(position: $position, selection: $selectedPOI) {
UserAnnotation() // 用户位置蓝点
ForEach(pois) { poi in
Annotation(poi.name, coordinate: poi.coordinate, anchor: .bottom) {
VStack(spacing: 0) {
Image(systemName: poi.category.icon)
.padding(8)
.background(.white)
.clipShape(Circle())
.shadow(radius: 4)
Triangle()
.fill(.white)
.frame(width: 10, height: 6)
}
}
.tag(poi)
}
}
.mapControls {
MapUserLocationButton() // 定位按钮
MapCompass() // 指南针
MapScaleView() // 比例尺
MapPitchToggle() // 2D/3D 切换
}
.mapStyle(.standard(elevation: .realistic)) // 地图样式
.onAppear { locationManager.requestPermission() }
.safeAreaInset(edge: .bottom) {
if let poi = selectedPOI {
POIDetailCard(poi: poi)
.transition(.move(edge: .bottom))
}
}
}
}
9.4 生物识别(Face ID / Touch ID)
swift
复制代码
import LocalAuthentication
@Observable
class BiometricManager {
var isAuthenticated = false
var errorMessage: String?
var isAvailable: Bool {
let context = LAContext()
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
error: nil)
}
var biometricType: LABiometryType {
let context = LAContext()
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
error: nil)
return context.biometryType
}
var biometricName: String {
switch biometricType {
case .faceID: return "Face ID"
case .touchID: return "Touch ID"
case .opticID: return "Optic ID"
default: return "生物识别"
}
}
// 生物识别(支持密码 fallback)
func authenticate(reason: String = "请验证身份") async {
let context = LAContext()
context.localizedFallbackTitle = "使用密码" // Fallback 按钮文字
do {
let success = try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason
)
isAuthenticated = success
} catch let error as LAError {
errorMessage = handleBiometricError(error)
} catch {
errorMessage = error.localizedDescription
}
}
// 设备密码认证(不限于生物识别)
func authenticateWithDevicePasscode(reason: String) async {
let context = LAContext()
do {
let success = try await context.evaluatePolicy(
.deviceOwnerAuthentication, // 包含密码 fallback
localizedReason: reason
)
isAuthenticated = success
} catch {
errorMessage = error.localizedDescription
}
}
private func handleBiometricError(_ error: LAError) -> String {
switch error.code {
case .biometryNotAvailable: return "设备不支持生物识别"
case .biometryNotEnrolled: return "未设置生物识别,请在设置中配置"
case .biometryLockout: return "生物识别已锁定,请使用密码解锁"
case .authenticationFailed: return "识别失败,请重试"
case .userCancel: return "用户取消验证"
case .userFallback: return "用户选择使用密码"
default: return "验证失败:\(error.localizedDescription)"
}
}
}
9.5 后台任务
swift
复制代码
import BackgroundTasks
// 在 App 入口注册后台任务标识符
// 同时需要在 Info.plist 的 BGTaskSchedulerPermittedIdentifiers 中声明
@main
struct iOSDemosApp: App {
init() {
registerBackgroundTasks()
}
var body: some Scene {
WindowGroup { ContentView() }
}
func registerBackgroundTasks() {
// 后台刷新(短时间,~30s)
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.refresh",
using: .main
) { task in
handleRefresh(task: task as! BGAppRefreshTask)
}
// 后台处理(长时间,数分钟,需要充电/WiFi)
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.process",
using: .main
) { task in
handleProcessing(task: task as! BGProcessingTask)
}
}
func handleRefresh(task: BGAppRefreshTask) {
scheduleNextRefresh() // 调度下次刷新
let syncTask = Task {
do {
try await SyncManager.shared.syncLatestData()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
syncTask.cancel()
task.setTaskCompleted(success: false)
}
}
func scheduleNextRefresh() {
let request = BGAppRefreshTaskRequest(
identifier: "com.example.app.refresh"
)
// 最早 15 分钟后执行
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("后台任务调度失败:\(error)")
}
}
func handleProcessing(task: BGProcessingTask) {
let processingTask = Task {
await DataProcessor.shared.processAll()
task.setTaskCompleted(success: true)
}
task.expirationHandler = {
processingTask.cancel()
task.setTaskCompleted(success: false)
}
}
}
章节总结
| 系统能力 |
框架 |
关键 API |
| 本地通知 |
UserNotifications |
UNUserNotificationCenter |
| 远程推送 |
APNs |
registerForRemoteNotifications |
| 照片库 |
PhotosUI |
PhotosPicker |
| 相机 |
AVFoundation / UIKit |
UIImagePickerController |
| 地图 |
MapKit |
Map / MapKit |
| 定位 |
CoreLocation |
CLLocationManager |
| 生物识别 |
LocalAuthentication |
LAContext.evaluatePolicy |
| 后台任务 |
BackgroundTasks |
BGTaskScheduler |
Demo 说明
| 文件 |
演示内容 |
PushNotificationDemo.swift |
本地通知 + 可操作通知 |
CameraPhotoDemo.swift |
PhotosPicker 多选 + 相机拍照 |
MapLocationDemo.swift |
地图标注 + 定位 + 反地理编码 |
BiometricDemo.swift |
Face ID / Touch ID 认证流程 |
BackgroundTaskDemo.swift |
后台刷新调度演示 |
📎 扩展内容补充
来源:第九章_系统能力.md
本章概述:学习调用 iOS 系统能力,包括推送通知(APNs)、相机与照片库(AVFoundation/PhotosUI)、地图与定位(MapKit/CoreLocation)、生物识别认证(LocalAuthentication)、后台任务(BackgroundTasks)。
9.1 推送通知
概念讲解
swift
复制代码
import UserNotifications
import UIKit
// 1. 请求推送权限
class NotificationManager {
static let shared = NotificationManager()
func requestPermission() async -> Bool {
let center = UNUserNotificationCenter.current()
let options: UNAuthorizationOptions = [.alert, .badge, .sound]
do {
return try await center.requestAuthorization(options: options)
} catch {
return false
}
}
// 本地推送(类比 Flutter 的 flutter_local_notifications)
func scheduleLocalNotification(
title: String,
body: String,
afterSeconds: TimeInterval,
badge: Int? = nil
) async throws {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
if let badge { content.badge = badge as NSNumber }
// 触发器(时间间隔)
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: afterSeconds,
repeats: false
)
// 日历触发(每天早上9点)
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
let calendarTrigger = UNCalendarNotificationTrigger(
dateMatching: dateComponents,
repeats: true
)
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: trigger
)
try await UNUserNotificationCenter.current().add(request)
}
// 取消推送
func cancelNotification(identifier: String) {
UNUserNotificationCenter.current()
.removePendingNotificationRequests(withIdentifiers: [identifier])
}
}
// 2. AppDelegate 处理远程推送(APNs)
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// 上传 Device Token 到服务器
let tokenString = deviceToken.map { String(format: "%02x", $0) }.joined()
print("Device Token: \(tokenString)")
Task { await ServerAPI.updatePushToken(tokenString) }
}
// 前台收到推送时调用
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .badge, .sound]) // 前台也显示推送
}
// 点击推送时调用
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
// 解析推送数据,导航到对应页面
if let articleId = userInfo["article_id"] as? String {
NotificationCenter.default.post(
name: .navigateToArticle,
object: nil,
userInfo: ["id": articleId]
)
}
completionHandler()
}
}
9.2 相机与照片库
概念讲解
swift
复制代码
import PhotosUI
import SwiftUI
// PhotosPicker - SwiftUI 原生照片选择器(iOS 16+)
struct PhotoPickerDemo: View {
@State private var selectedItem: PhotosPickerItem?
@State private var selectedImage: Image?
var body: some View {
VStack {
// 显示选中的图片
if let selectedImage {
selectedImage
.resizable()
.scaledToFit()
.frame(maxHeight: 300)
.cornerRadius(16)
}
// 选择照片按钮
PhotosPicker(selection: $selectedItem,
matching: .images, // 仅图片
photoLibrary: .shared()) {
Label("选择照片", systemImage: "photo.on.rectangle")
}
.buttonStyle(.borderedProminent)
.onChange(of: selectedItem) { _, newItem in
Task {
if let data = try? await newItem?.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImage = Image(uiImage: uiImage)
}
}
}
}
}
}
// 多选照片
struct MultiPhotoPickerDemo: View {
@State private var selectedItems: [PhotosPickerItem] = []
@State private var images: [UIImage] = []
var body: some View {
VStack {
PhotosPicker(selection: $selectedItems,
maxSelectionCount: 9,
matching: .images) {
Label("选择最多9张", systemImage: "photo.stack")
}
ScrollView(.horizontal) {
HStack {
ForEach(images, id: \.self) { image in
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipped()
.cornerRadius(8)
}
}
}
}
.onChange(of: selectedItems) { _, items in
Task {
images = []
for item in items {
if let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
images.append(image)
}
}
}
}
}
}
// 相机拍照(使用 UIImagePickerController)
struct CameraView: UIViewControllerRepresentable {
@Binding var image: UIImage?
@Environment(\.dismiss) var dismiss
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = context.coordinator
picker.cameraCaptureMode = .photo
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
let parent: CameraView
init(_ parent: CameraView) { self.parent = parent }
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
parent.image = info[.originalImage] as? UIImage
parent.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.dismiss()
}
}
}
9.3 地图与定位
概念讲解
swift
复制代码
import MapKit
import CoreLocation
import SwiftUI
// 定位管理器
@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
var currentLocation: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .notDetermined
var errorMessage: String?
private let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
}
func requestPermission() {
manager.requestWhenInUseAuthorization()
}
func startUpdating() {
manager.startUpdatingLocation()
}
func stopUpdating() {
manager.stopUpdatingLocation()
}
// 单次获取位置
func requestLocation() {
manager.requestLocation()
}
// Delegate
nonisolated func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
Task { @MainActor in
currentLocation = locations.last
}
}
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
Task { @MainActor in
authorizationStatus = manager.authorizationStatus
}
}
nonisolated func locationManager(_ manager: CLLocationManager,
didFailWithError error: Error) {
Task { @MainActor in
errorMessage = error.localizedDescription
}
}
}
// 地图视图(iOS 17 新 API)
struct MapDemo: View {
@State private var locationManager = LocationManager()
@State private var position: MapCameraPosition = .automatic
@State private var selectedAnnotation: PointOfInterest?
let pois: [PointOfInterest] = [
PointOfInterest(name: "上海东方明珠",
coordinate: CLLocationCoordinate2D(latitude: 31.2397, longitude: 121.4998)),
PointOfInterest(name: "外滩",
coordinate: CLLocationCoordinate2D(latitude: 31.2399, longitude: 121.4905)),
]
var body: some View {
Map(position: $position, selection: $selectedAnnotation) {
// 用户当前位置
UserAnnotation()
// 自定义标注
ForEach(pois) { poi in
Annotation(poi.name, coordinate: poi.coordinate) {
Image(systemName: "mappin.circle.fill")
.font(.system(size: 32))
.foregroundStyle(.red)
}
.tag(poi)
}
// 路线覆盖层
// MapPolyline(coordinates: routeCoordinates)
// .stroke(.blue, lineWidth: 3)
}
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
.onAppear {
locationManager.requestPermission()
}
.safeAreaInset(edge: .bottom) {
if let poi = selectedAnnotation {
POIDetailCard(poi: poi)
.padding()
}
}
}
}
9.4 生物识别认证(Face ID / Touch ID)
概念讲解
swift
复制代码
import LocalAuthentication
class BiometricAuthManager {
static let shared = BiometricAuthManager()
// 检查是否支持生物识别
var isBiometricAvailable: Bool {
let context = LAContext()
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
error: nil)
}
var biometricType: LABiometryType {
let context = LAContext()
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
return context.biometryType
}
// 执行生物识别
func authenticate(reason: String = "验证你的身份") async -> Bool {
let context = LAContext()
// 允许 Face ID + 密码 fallback
context.localizedFallbackTitle = "使用密码"
do {
return try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason
)
} catch {
print("认证失败:\(error.localizedDescription)")
return false
}
}
}
// 生物识别登录视图
struct BiometricLoginView: View {
@State private var isAuthenticated = false
@State private var showError = false
var biometricIcon: String {
switch BiometricAuthManager.shared.biometricType {
case .faceID: return "faceid"
case .touchID: return "touchid"
default: return "lock.fill"
}
}
var body: some View {
VStack(spacing: 40) {
Image(systemName: biometricIcon)
.font(.system(size: 80))
.foregroundStyle(.blue)
Text(isAuthenticated ? "认证成功" : "请进行身份验证")
.font(.title2)
if !isAuthenticated {
Button {
Task {
let success = await BiometricAuthManager.shared.authenticate()
isAuthenticated = success
if !success { showError = true }
}
} label: {
Label("使用 Face ID 登录", systemImage: "faceid")
.font(.headline)
}
.buttonStyle(.borderedProminent)
}
}
.alert("认证失败", isPresented: $showError) {
Button("确定", role: .cancel) {}
} message: {
Text("请重试或使用密码登录")
}
}
}
9.5 后台任务
概念讲解
swift
复制代码
import BackgroundTasks
// 注册后台任务(在 App 启动时)
func registerBackgroundTasks() {
// 后台刷新(类比 Flutter 的 WorkManager)
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.refresh",
using: nil
) { task in
handleAppRefresh(task: task as! BGAppRefreshTask)
}
// 后台处理任务
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.process",
using: nil
) { task in
handleProcessingTask(task: task as! BGProcessingTask)
}
}
// 处理后台刷新
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleNextRefresh() // 调度下次刷新
Task {
do {
try await SyncManager.shared.syncData()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
// 系统要终止任务时调用
SyncManager.shared.cancelSync()
}
}
// 调度后台任务
func scheduleNextRefresh() {
let request = BGAppRefreshTaskRequest(
identifier: "com.example.app.refresh"
)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15分钟后
try? BGTaskScheduler.shared.submit(request)
}
章节总结
| 系统能力 |
框架 |
对应Flutter |
| 推送通知 |
UserNotifications / APNs |
firebase_messaging |
| 照片库 |
PhotosUI |
image_picker |
| 相机 |
AVFoundation |
camera |
| 地图 |
MapKit |
flutter_map / google_maps |
| 定位 |
CoreLocation |
geolocator |
| 生物识别 |
LocalAuthentication |
local_auth |
| 后台任务 |
BackgroundTasks |
workmanager |
Demo 说明
| Demo 文件 |
演示内容 |
PushNotificationDemo.swift |
本地推送 + 权限申请 |
CameraPhotoDemo.swift |
PhotosPicker + 相机拍照 |
MapLocationDemo.swift |
MapKit + 定位 + 自定义标注 |
BiometricDemo.swift |
Face ID / Touch ID 认证 |
BackgroundTaskDemo.swift |
后台刷新任务调度 |