第九章:iOS系统框架与能力

本章讲解 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 后台刷新任务调度
相关推荐
iAnMccc1 天前
Swift Codable 的 5 个生产环境陷阱,以及如何优雅地解决它们
ios
iAnMccc1 天前
从 HandyJSON 迁移到 SmartCodable:我们团队的实践
ios
kerli1 天前
基于 kmp/cmp 的跨平台图片加载方案 - 适配 Android View/Compose/ios
android·前端·ios
懋学的前端攻城狮1 天前
第三方SDK集成沉思录:在便捷与可控间寻找平衡
ios·前端框架
冰凌时空1 天前
Swift vs Objective-C:语言设计哲学的全面对比
ios·openai
花间相见1 天前
【大模型微调与部署03】—— ms-swift-3.12 命令行参数(训练、推理、对齐、量化、部署全参数)
开发语言·ios·swift
SameX1 天前
删掉ML推荐、砍掉五时段分析——做专注App时我三次推翻自己,换来了什么
ios
爱吃香蕉的阿豪1 天前
Mac 远程操作 Windows 开发:ZeroTier + JetBrains 实战指南
windows·macos·zerotoer
YJlio1 天前
2026年4月19日60秒读懂世界:从学位扩容到人形机器人夺冠,今天最值得关注的6个信号
python·安全·ios·机器人·word·iphone·7-zip
90后的晨仔2 天前
《SwiftUI 高级特性第1章:自定义视图》
ios