iOS 原生开发
1. iOS 架构演进
1.1 iOS 系统层
iOS 采用分层架构,从底层到顶层依次为 Core OS、Core Services、Media 和 Cocoa Touch。
1.1.1 四层架构
Core OS 层:基于 Mach 微内核和 XNU(X is Not Unix)混合内核,提供最底层的系统服务,包括内存管理、进程调度、文件系统、网络协议栈、安全加密(Keychain/Apple Mobile File Integrity)、硬件驱动等。XNU 融合了 Mach 的微内核特性(IPC、虚拟内存管理)和 FreeBSD 的服务(POSIX API、进程管理、网络栈)。
Core Services 层:提供核心系统服务,包括 Foundation 框架(集合、字符串、日期、文件管理、网络、URL 加载)、Core Data(对象图管理与持久化)、Core Location(定位服务)、Core Motion(运动传感器)、CloudKit(iCloud 云服务)、StoreKit(应用内购)等。
Media 层:提供图形、音频和视频服务,包括 Core Graphics(2D 渲染引擎)、Core Animation(动画合成引擎)、Core Image(图像滤镜处理)、Metal(GPU 加速图形 API)、AVFoundation(音视频录制与播放)、Core Audio(音频处理)等。
Cocoa Touch 层:最上层的 UI 框架层,提供 UIKit(iOS 原生 UI 框架)、SwiftUI(声明式 UI 框架)、UserNotifications(本地和远程通知)、MapKit(地图服务)、GameKit(游戏中心)等。
1.1.2 Mach 内核与 XNU
XNU 是 iOS 和 macOS 的核心操作系统内核,采用混合内核设计:
- Mach 微内核:负责基础抽象——任务(进程)、线程、虚拟内存、IPC(消息传递/端口)、时钟和计时器。Mach 提供
mach_vm_allocate、mach_msg等底层 API。 - BSD 层:在 Mach 之上提供 POSIX 兼容的进程管理(fork/exec)、文件系统(VFS)、网络协议栈(BSD sockets)、安全策略(MAC/entitlements)、用户和权限管理。
- IOKit:驱动框架,管理硬件设备枚举、电源管理、设备匹配与驱动加载。
- libkern:C++ 运行时库,为内核扩展提供面向对象的基础设施。
1.2 Xcode 开发
1.2.1 工程结构
Xcode 项目遵循以下主要结构:
- 工程文件(.xcodeproj):包含 pbxproj 文件,存储项目配置、文件引用、构建阶段、目标(target)配置等。
- xcworkspace:工作空间,可包含多个项目和 XCFrameworks,通过 CocoaPods/Swift Package Manager 管理依赖时使用。
- Info.plist:应用配置清单,包含 Bundle Identifier、版本号、启动故事板、权限声明、App Transport Security 配置等。
- Entitlements 文件(.entitlements):声明应用的授权能力,如 App Groups、iCloud、Push Notifications、Keychain Sharing、Associated Domains 等,与 Apple Developer Portal 上的 App ID 配置对应。
1.2.2 AppDelegate 与 SceneDelegate
iOS 13+ 引入多窗口场景(Scene)支持,将应用生命周期管理从 AppDelegate 拆分到 SceneDelegate:
// AppDelegate:应用级生命周期
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 应用启动时的初始化(SDK 初始化、推送注册等)
return true
}
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// 返回场景配置
return UISceneConfiguration(name: "Default Configuration",
sessionRole: connectingSceneSession.role)
}
}
// SceneDelegate:场景级生命周期
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = ContentViewController()
window?.makeKeyAndVisible()
}
func sceneDidEnterBackground(_ scene: UIScene) {
// 进入后台,保存状态
}
}1.2.3 Capabilities 与 Entitlements
Capabilities 是 Xcode 中可视化配置应用能力的界面,自动生成对应的 Entitlements 文件。常见能力包括:
- Push Notifications:启用远程推送通知,生成 APS Environment entitlement。
- App Groups:允许多个应用(或应用与 Extension)共享数据容器,需配置
group.<identifier>。 - iCloud:启用 iCloud 容器,支持 CloudKit 和键值存储。
- Associated Domains:配置 Universal Links 和 Handoff,格式为
applinks:<domain>或webcredentials:<domain>。 - Sign in with Apple:启用 Apple 登录能力。
- Keychain Sharing:允许多个应用共享钥匙串数据。
1.2.4 Swift Packages 与依赖管理
Swift Package Manager(SPM):Apple 官方提供的依赖管理工具,集成在 Xcode 中。通过 Package.swift 声明依赖:
// Package.swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLibrary",
platforms: [.iOS(.v16)],
products: [
.library(name: "MyLibrary", targets: ["MyLibrary"]),
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-composable-architecture",
from: "1.0.0"),
],
targets: [
.target(
name: "MyLibrary",
dependencies: [
.product(name: "ComposableArchitecture",
package: "swift-composable-architecture"),
]
),
]
)Swift Package Dependencies:在 Xcode 中通过 File > Add Package Dependencies 添加,支持版本规则(精确版本、版本范围、分支、提交)。
Xcode Cloud:Apple 提供的 CI/CD 服务,直接在 Xcode 中配置,支持自动构建、测试、分发到 TestFlight。配置文件为 .xcodecloud.yml。
云编译:通过 GitHub Actions、GitLab CI 等第三方 CI 工具,配合 xcodebuild 命令行工具完成远程编译和签名。
1.2.5 苹果开发者计划
苹果开发者计划提供开发、测试和分发 iOS 应用所需的环境:
- 个人/组织开发者账号:$99/年,支持 App Store 分发。
- 企业开发者账号:$299/年,支持企业内部应用分发,不上架 App Store。
- 证书(Certificate):iOS 开发证书和分发证书,通过 Keychain 或 Xcode 管理。
- 描述文件(Provisioning Profile):绑定证书、App ID 和设备 ID,分为 Development(调试)和 Distribution(发布/Ad Hoc)两类。
1.3 Swift 语言
1.3.1 值类型与引用类型
Swift 中的类型分为值类型(Value Type)和引用类型(Reference Type),这是区分 struct 和 class 的核心概念:
- 值类型(struct、enum、tuple):赋值时进行值拷贝,每个实例独立持有数据。存储在栈上(可被内联),线程安全。
- 引用类型(class、actor、closure):赋值时拷贝引用,多个变量指向同一内存地址。存储在堆上,ARC(自动引用计数)管理生命周期。
// 值类型 - struct
struct Point {
var x: Double
var y: Double
}
var p1 = Point(x: 1, y: 2)
var p2 = p1 // 值拷贝
p2.x = 10 // 仅修改 p2,p1 不受影响
print(p1.x) // 1.0
// 引用类型 - class
class Person {
var name: String
init(name: String) { self.name = name }
}
let person1 = Person(name: "Alice")
let person2 = person1 // 引用拷贝
person2.name = "Bob" // person1.name 也变为 "Bob"1.3.2 struct 与 class 的选择
| 特性 | struct | class |
|---|---|---|
| 类型 | 值类型 | 引用类型 |
| 继承 | 不支持(可遵循协议) | 支持单继承 |
| 堆栈分配 | 优先栈分配(可内联) | 堆分配 + 引用计数 |
| let 变异 | 不可修改属性 | 可修改属性(let 常量引用) |
| mutating | 需要 mutating 关键字 | 不需要 |
| 线程安全 | 天然安全(独立拷贝) | 需要同步机制 |
| deinit | 不支持 | 支持 |
| 适用场景 | 数据模型、值语义 | 需要引用共享、生命周期管理 |
Swift 标准库优先使用 struct:String、Array、Dictionary、Int、Double 都是 struct。
1.3.3 Protocol 与 Protocol-Oriented Programming
Swift 的面向协议编程(POP)强调通过协议(protocol)定义接口,通过协议扩展(protocol extension)提供默认实现,取代类继承作为代码复用的主要方式。
protocol Drawable {
var area: Double { get }
mutating func draw()
}
// 协议扩展提供默认实现
extension Drawable {
mutating func draw() {
print("Drawing shape with area: \(area)")
}
}
struct Circle: Drawable {
var radius: Double
var area: Double { .pi * radius * radius }
// 使用默认 draw 实现
}
struct Rectangle: Drawable {
var width: Double
var height: Double
var area: Double { width * height }
mutating func draw() {
print("Drawing rectangle with area: \(area)")
}
}
// 协议作为类型
let shapes: [Drawable] = [Circle(radius: 5), Rectangle(width: 3, height: 4)]1.3.4 extension 与 protocol extension
extension 为已有类型添加新功能,protocol extension 为协议提供默认方法和默认属性:
// 为系统类型添加扩展
extension String {
var isEmail: Bool {
let pattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
return range(of: pattern, options: .regularExpression) != nil
}
func trimmed() -> String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
}
// 协议扩展
protocol Identifiable {
var id: String { get }
func identify()
}
extension Identifiable {
func identify() {
print("My ID is \(id).")
}
}
struct User: Identifiable {
let id: String
// 自动获得 identify() 默认实现
}1.3.5 枚举与关联值
Swift 枚举功能强大,支持关联值和原始值:
// 关联值枚举
enum NetworkResult<T> {
case success(T)
case failure(Error)
case loading(progress: Double)
}
// 使用枚举
let result: NetworkResult<[String]> = .loading(progress: 0.5)
switch result {
case .success(let data):
print("Got data: \(data)")
case .failure(let error):
print("Error: \(error.localizedDescription)")
case .loading(let progress):
print("Loading: \(Int(progress * 100))%")
}
// 枚举遵循协议
enum HTTPMethod: String, CaseIterable {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
print(HTTPMethod.allCases.map(\.rawValue)) // ["GET", "POST", "PUT", "DELETE"]1.3.6 可选值 Optional
Optional 是 Swift 的核心特性,本质是 Optional<T> 枚举:
// Optional 本质
@frozen enum Optional<Wrapped> {
case none
case some(Wrapped)
}
var name: String? = "Hello"
name = nil
// 可选链(Optional Chaining)
let length = name?.count // 若 name 为 nil 则返回 nil
let uppercased = name?.uppercased()
// 强制解包(尽量避免)
if name != nil {
print(name!.count)
}
// 空合并运算符
let displayName = name ?? "Default"1.3.7 guard let 与 if let
两种可选解包方式各有适用场景:
// if let - 在条件分支中解包
if let unwrappedName = name {
print("Name is \(unwrappedName)")
} else {
print("Name is nil")
}
// guard let - 提前退出,适用于需要解包后继续执行的场景
func processUser(id: String?) {
guard let userId = id else {
print("Invalid user ID")
return
}
// userId 在此作用域可用
print("Processing user \(userId)")
// 更多后续代码...
}
// 多重解包
guard let data = try? fetchData(),
let user = try? JSONDecoder().decode(User.self, from: data)
else {
return
}1.3.8 高阶函数 map filter reduce
Swift 标准库提供强大的集合操作函数:
let numbers = [1, 2, 3, 4, 5, 6]
// map - 转换每个元素
let doubled = numbers.map { $0 * 2 } // [2, 4, 6, 8, 10, 12]
let strings = numbers.map { "\($0)" } // ["1", "2", "3", "4", "5", "6"]
// compactMap - 转换并过滤 nil
let possibleNumbers = ["1", "two", "3", "four", "5"]
let ints = possibleNumbers.compactMap { Int($0) } // [1, 3, 5]
// flatMap - 展平嵌套集合
let nested = [[1, 2], [3, 4], [5, 6]]
let flattened = nested.flatMap { $0 } // [1, 2, 3, 4, 5, 6]
// filter - 过滤
let even = numbers.filter { $0.isMultiple(of: 2) } // [2, 4, 6]
// reduce - 归约
let sum = numbers.reduce(0, +) // 21
let product = numbers.reduce(1, *) // 720
let sentence = ["Hello", "World"].reduce("") { $0 + " " + $1 } // " Hello World"
// 方法链式调用
let result = numbers
.filter { $0 > 2 }
.map { $0 * $0 }
.reduce(0, +) // 16 + 25 + 36 = 771.3.9 async await 并发
Swift 5.5 引入结构化并发,使用 async/await 简化异步编程:
// 定义异步函数
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
func fetchAvatar(url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw URLError(.badServerResponse)
}
return image
}
// 调用异步函数
Task {
do {
let user = try await fetchUser(id: 1)
let avatar = try await fetchAvatar(url: user.avatarURL)
// 更新 UI
} catch {
print("Failed: \(error)")
}
}Task:创建一个异步任务单元,在 Task 闭包中调用 async 函数。Task.detached 创建不继承父级优先级的独立任务。
结构化并发:使用 async let 并行执行多个异步任务:
func loadUserProfile(id: Int) async throws -> Profile {
async let user = fetchUser(id: id)
async let posts = fetchPosts(userId: id)
async let friends = fetchFriends(userId: id)
// 等待所有任务完成
return try await Profile(user: user, posts: posts, friends: friends)
}任务组(TaskGroup):动态创建并行任务:
func fetchAllUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask {
try await fetchUser(id: id)
}
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}1.3.10 actor 与 Sendable 协议
actor:保护可变状态免受数据竞争的并发类型,所有属性和方法自动序列化访问:
actor BankAccount {
private var balance: Double = 0
func deposit(amount: Double) {
balance += amount
}
func withdraw(amount: Double) throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
}
func getBalance() -> Double {
balance
}
}
// 使用 actor
let account = BankAccount()
Task {
await account.deposit(amount: 100)
let balance = await account.getBalance()
print(balance)
}Sendable 协议:标记可以在并发域之间安全传递的类型。编译器强制检查 Sendable 约束:
// struct 自动满足 Sendable(如果所有属性都是 Sendable)
struct UserInfo: Sendable {
let id: Int
let name: String
}
// class 需要显式声明
final class SafeConfig: @unchecked Sendable {
let timeout: Int
init(timeout: Int) { self.timeout = timeout }
}1.3.11 Swift 6 严格并发检查
Swift 6 引入了完整的并发安全模型,在编译时强制执行数据隔离:
- 完整数据隔离:在 Swift 6 模式下,所有全局变量和类属性的访问必须经过 actor 隔离或使用
@MainActor标注。 - Sendable 强制检查:跨并发域传递非 Sendable 类型时产生编译错误。
- @MainActor 传播:标记为
@MainActor的类型或函数自动在主队列执行。 - 区域隔离区域(Region Isolation):编译器在函数内部进行隔离分析,减少手动标注需求。
// Swift 6 模式下
@MainActor
class ViewModel: ObservableObject {
@Published var name = ""
// 自动在主线程执行
func updateName(_ newName: String) {
name = newName
}
}
// 非 @MainActor 的函数需要切换到主线程
func backgroundWork() async {
await viewModel.updateName("New")
}
// 编译时检查数据竞争
// 以下代码在 Swift 6 下会产生编译错误
var globalCounter = 0 // 错误:全局变量需要并发保护2. SwiftUI 声明式 UI
2.1 SwiftUI 核心
2.1.1 View 协议与 body
SwiftUI 中所有 UI 元素遵循 View 协议,通过描述性的 body 声明界面:
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
.font(.title)
.foregroundColor(.blue)
.padding()
}
}View 协议要求实现一个 body 计算属性,返回 some View(不透明返回类型)。SwiftUI 会按需渲染视图,而非每次都重新创建整个视图树。
2.1.2 @State 与 @Binding
@State 用于在视图内部管理可变状态,SwiftUI 自动追踪并在状态变化时更新 UI:
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
.font(.largeTitle)
Button("Increment") {
count += 1 // 修改 @State 自动触发视图刷新
}
}
}
}@Binding 创建对另一个视图的 @State 的可变引用,实现数据双向传递:
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: $isOn) // 传递 Binding<Bool>
}
}
struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}2.1.3 @ObservedObject 与 @StateObject
用于引用 ObservableObject(class 类型,通常来自 MVVM 的 ViewModel):
class UserViewModel: ObservableObject {
@Published var name = ""
@Published var age = 0
func loadUser() {
// 网络请求...
name = "Alice"
age = 30
}
}
// @StateObject - 视图拥有该对象生命周期(只应在创建时使用)
struct ProfileView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
VStack {
Text(viewModel.name)
Text("\(viewModel.age)")
}
.onAppear {
viewModel.loadUser()
}
}
}
// @ObservedObject - 视图不拥有,由外部传入
struct DetailView: View {
@ObservedObject var viewModel: UserViewModel
var body: some View {
Text(viewModel.name)
}
}2.1.4 @EnvironmentObject 与 @Environment
@EnvironmentObject 通过环境传递 ObservableObject,避免层层传递:
// 在父视图注入
let appState = AppState()
WindowGroup {
ContentView()
.environmentObject(appState)
}
// 子视图直接访问
struct DetailView: View {
@EnvironmentObject var appState: AppState
var body: some View {
Text(appState.userName)
}
}@Environment 读取系统环境值:
struct MyView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@Environment(\.locale) var locale
@Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
if colorScheme == .dark {
Text("Dark mode")
}
}
}2.1.5 属性包装器总结
| 属性包装器 | 类型 | 数据源 | 所有权 | 刷新触发 |
|---|---|---|---|---|
| @State | 值类型 | 视图内部 | 视图拥有 | 值变化 |
| @Binding | 引用 | 外部传入 | 不拥有 | 源变化 |
| @StateObject | 引用类型 | 视图创建 | 视图拥有 | @Published |
| @ObservedObject | 引用类型 | 外部传入 | 不拥有 | @Published |
| @EnvironmentObject | 引用类型 | 环境注入 | 不拥有 | @Published |
| @Environment | 只读 | 系统提供 | 不拥有 | 系统变化 |
2.1.6 布局容器
VStack/HStack/ZStack:基础布局容器,分别表示垂直、水平和深度堆叠:
VStack(alignment: .leading, spacing: 16) {
Text("Title").font(.title)
HStack(spacing: 8) {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text("Rating")
}
ZStack(alignment: .bottomTrailing) {
Image("photo")
.resizable()
.frame(height: 200)
Text("© 2024")
.padding(4)
.background(.black.opacity(0.5))
.foregroundColor(.white)
}
}
.padding()LazyVStack/LazyHStack:惰性加载的堆栈视图,只在元素即将可见时才创建,适用于长列表:
ScrollView {
LazyVStack(alignment: .leading, pinnedViews: [.sectionHeaders]) {
Section(header: Text("Section 1")) {
ForEach(0..<1000, id: \.self) { index in
Text("Row \(index)")
}
}
}
}Grid:网格布局,LazyVGrid 和 LazyHGrid 支持惰性加载:
let columns = [
GridItem(.flexible(), spacing: 8),
GridItem(.adaptive(minimum: 80), spacing: 8),
GridItem(.fixed(100)),
]
LazyVGrid(columns: columns, spacing: 16) {
ForEach(0..<50, id: \.self) { item in
RoundedRectangle(cornerRadius: 8)
.fill(.blue)
.frame(height: 80)
.overlay(Text("\(item)"))
}
}
.padding()Form:表单容器,自动适配 iOS 样式,支持 Section 分组:
Form {
Section("个人信息") {
TextField("姓名", text: $name)
DatePicker("生日", selection: $birthday, displayedComponents: .date)
Picker("性别", selection: $gender) {
Text("男").tag(0)
Text("女").tag(1)
}
}
Section("偏好设置") {
Toggle("开启通知", isOn: $notificationsEnabled)
Stepper("音量: \(volume)", value: $volume, in: 0...100)
}
}List:列表视图,支持动态数据、侧滑操作、Section、EditButton 等:
List {
ForEach(items) { item in
HStack {
Text(item.title)
Spacer()
Text(item.date, style: .date)
.foregroundColor(.secondary)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteItem(item)
} label: {
Label("删除", systemImage: "trash")
}
}
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItems)
}ScrollView:可滚动容器,配合 LazyVStack/LazyHStack 实现高性能滚动:
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(0..<20) { index in
RoundedRectangle(cornerRadius: 12)
.fill(Color(hue: Double(index) / 20, saturation: 0.5, brightness: 0.8))
.frame(width: 120, height: 160)
}
}
.padding()
}Section/Group/GroupBox/ControlGroup:
- Section:在 List/Form 中添加分组标题和分隔。
- Group:容器,将多个视图组合为单一逻辑实体(可突破 10 个子视图限制)。
- GroupBox:带标题标签的视觉分组容器。
- ControlGroup:将相关控件视觉分组并统一样式。
Form {
Section("基本信息") {
Text("Name: \(user.name)")
Text("Age: \(user.age)")
}
GroupBox(label: Label("网络状态", systemImage: "wifi")) {
HStack {
Text("连接状态:")
if isConnected {
Image(systemName: "checkmark.circle.fill").foregroundColor(.green)
}
}
}
}
// Group 解决 10 个子视图限制
var body: some View {
Group {
Text("A")
Text("B")
Text("C")
Text("D")
Text("E")
Text("F")
Text("G")
Text("H")
Text("I")
Text("J")
Text("K") // 第 11 个, 在 Group 内合法
}
}2.2 数据流
2.2.1 @State 与 @Binding
@State 管理视图私有状态,@Binding 实现父子视图数据共享:
struct Parent: View {
@State private var text = ""
var body: some View {
VStack {
Text("输入:\(text)")
Child(text: $text)
}
}
}
struct Child: View {
@Binding var text: String
var body: some View {
TextField("输入文字", text: $text)
.textFieldStyle(.roundedBorder)
}
}2.2.2 ObservableObject 与 Combine
Combine 框架提供响应式编程能力,核心组件如下:
import Combine
class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [String] = []
@Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
init() {
// 监听输入变化,防抖后执行搜索
$query
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.filter { $0.count >= 2 }
.sink { [weak self] query in
self?.search(query: query)
}
.store(in: &cancellables)
}
func search(query: String) {
isLoading = true
// 模拟网络请求
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.results = ["Result for \(query)"]
self?.isLoading = false
}
}
}
// CurrentValueSubject / PassthroughSubject
class NetworkMonitor {
let connectionStatus = CurrentValueSubject<Bool, Never>(true)
let eventStream = PassthroughSubject<String, Never>()
func startMonitoring() {
// 监听网络变化
eventStream.send("Monitoring started")
}
}eraseToAnyPublisher:将具体 Publisher 类型擦除为 AnyPublisher,用于隐藏实现细节:
protocol UserServiceProtocol {
func fetchUser(id: Int) -> AnyPublisher<User, Error>
}
class UserService: UserServiceProtocol {
func fetchUser(id: Int) -> AnyPublisher<User, Error> {
URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/users/\(id)")!)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
.eraseToAnyPublisher() // 隐藏内部 Publisher 类型
}
}onReceive:在 SwiftUI 视图中订阅 Publisher:
struct TimerDisplay: View {
@State private var time = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(time, style: .time)")
.onReceive(timer) { input in
time = input
}
}
}Task:在视图生命周期中管理异步任务:
struct UserProfileView: View {
@State private var user: User?
@State private var error: Error?
var body: some View {
Group {
if let user = user {
Text(user.name)
} else if let error = error {
Text("Error: \(error.localizedDescription)")
} else {
ProgressView()
}
}
.task {
// 视图出现时自动执行,视图消失时自动取消
do {
user = try await fetchUser(id: 1)
} catch {
self.error = error
}
}
}
}2.2.3 @EnvironmentObject 对象传递
@EnvironmentObject 通过 SwiftUI 环境传递 ObservableObject,避免手动逐级传递:
// 1. 定义数据模型
class AppSettings: ObservableObject {
@Published var isLoggedIn = false
@Published var theme: AppTheme = .system
}
// 2. 在根视图注入
@main
struct MyApp: App {
@StateObject private var settings = AppSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}
// 3. 任意子视图获取
struct ProfileView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
Toggle("登录状态", isOn: $settings.isLoggedIn)
}
}
// 4. 检查环境对象是否存在(避免崩溃的可选方式)
struct OptionalEnvView: View {
@EnvironmentObject var settings: AppSettings?
var body: some View {
if let settings = settings {
Text("Theme: \(settings.theme.rawValue)")
}
}
}2.3 导航
2.3.1 NavigationStack 与 NavigationLink
iOS 16+ 使用 NavigationStack 替代旧的 NavigationView:
struct ContentView: View {
var body: some View {
NavigationStack {
List(products) { product in
NavigationLink(value: product) {
ProductRow(product: product)
}
}
.navigationTitle("产品列表")
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
}
}
}2.3.2 NavigationPath 与程序化导航
NavigationPath 支持通过路径进行程序化导航:
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
Button("Go to A") {
path.append("A")
}
Button("Go to Detail") {
path.append(Product(id: 1, name: "iPhone"))
}
Button("Go to Settings") {
path.append(Destination.settings)
}
}
.navigationDestination(for: String.self) { string in
Text("String destination: \(string)")
}
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
.navigationDestination(for: Destination.self) { dest in
switch dest {
case .settings: SettingsView()
}
}
}
}
}
enum Destination: Hashable {
case settings
case profile
case about
}2.3.3 深层链接
处理 URL Scheme 和 Universal Links:
// App 端处理深层链接
@main
struct MyApp: App {
@StateObject private var appRouter = AppRouter()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appRouter)
.onOpenURL { url in
appRouter.handleDeepLink(url)
}
}
}
}
class AppRouter: ObservableObject {
@Published var path = NavigationPath()
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let host = components.host else { return }
switch host {
case "product":
if let idString = components.queryItems?.first(where: { $0.name == "id" })?.value,
let id = Int(idString) {
path.append(Product(id: id, name: "Product \(id)"))
}
case "profile":
path.append(Destination.profile)
default:
break
}
}
}2.3.4 NavigationSplitView 三栏/自适应
NavigationSplitView 适用于 iPad 的列式导航:
struct ContentView: View {
@State private var selectedCategory: Category?
@State private var selectedItem: Item?
@State private var columnVisibility = NavigationSplitViewVisibility.all
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
// 侧边栏(第一列)
List(categories, selection: $selectedCategory) { category in
Text(category.name)
}
.navigationTitle("分类")
} content: {
// 内容列(第二列)
if let category = selectedCategory {
List(category.items, selection: $selectedItem) { item in
Text(item.title)
}
.navigationTitle(category.name)
} else {
Text("选择一个分类")
}
} detail: {
// 详情列(第三列)
if let item = selectedItem {
ItemDetailView(item: item)
} else {
Text("选择一个项目")
}
}
}
}2.4 Swift Charts
iOS 16+ 引入 Swift Charts 框架,声明式创建图表。
2.4.1 基本图表
import Charts
struct SalesData: Identifiable {
let id = UUID()
let month: String
let sales: Double
let category: String
}
struct SalesChart: View {
let data: [SalesData]
var body: some View {
Chart(data) { item in
BarMark(
x: .value("月份", item.month),
y: .value("销售额", item.sales)
)
.foregroundStyle(by: .value("品类", item.category))
}
.chartForegroundStyleScale([
"电子产品": .blue,
"服装": .green,
"食品": .orange,
])
.frame(height: 300)
.padding()
}
}2.4.2 各种 Mark 类型
Chart {
// 柱状图
BarMark(x: .value("Month", "Jan"), y: .value("Value", 30))
// 折线图
LineMark(x: .value("Month", "Feb"), y: .value("Value", 45))
.foregroundStyle(.red)
// 散点图
PointMark(x: .value("Month", "Mar"), y: .value("Value", 60))
// 面积图
AreaMark(x: .value("Month", "Apr"), y: .value("Value", 40))
// 参考线
RuleMark(y: .value("目标", 50))
.foregroundStyle(.red)
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
.annotation(position: .top, alignment: .trailing) {
Text("目标线: 50").font(.caption).foregroundColor(.red)
}
}
// SectorMark 饼图/环形图
Chart(data, innerRadius: .ratio(0.6)) { item in
SectorMark(
angle: .value("Value", item.sales),
innerRadius: .ratio(0.6),
angularInset: 2
)
.foregroundStyle(by: .value("Category", item.category))
.annotation(position: .overlay) {
Text("\(Int(item.sales))%")
.font(.caption)
.foregroundColor(.white)
}
}2.4.3 图表交互与 ChartProxy
struct InteractiveChart: View {
@State private var selectedMonth: String?
var body: some View {
Chart(data) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Sales", item.sales)
)
.opacity(selectedMonth == nil || selectedMonth == item.month ? 1 : 0.3)
}
.chartXSelection(value: $selectedMonth) // 选择交互
.chartOverlay { proxy in
GeometryReader { geometry in
Rectangle()
.fill(.clear)
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
let x = value.location.x - geometry[proxy.plotAreaFrame].origin.x
if let month: String = proxy.value(atX: x) {
selectedMonth = month
}
}
)
}
}
.frame(height: 300)
}
}2.4.4 可访问性
图表自动支持 VoiceOver,可添加显式可访问性描述:
Chart {
BarMark(
x: .value("Month", item.month),
y: .value("Sales", item.sales)
)
.accessibilityLabel("\(item.month) 销售额")
.accessibilityValue("\(Int(item.sales)) 元")
}
.chartXAxis {
AxisMarks(values: .automatic) { value in
AxisGridLine()
AxisTick()
AxisValueLabel {
if let month = value.as(String.self) {
Text(month.prefix(3))
.font(.caption)
}
}
}
}3. UIKit 兼容
3.1 UIViewRepresentable
在 SwiftUI 中包装 UIKit UIView,使用 UIViewRepresentable 协议。
3.1.1 基础原理
struct UIKitViewWrapper: UIViewRepresentable {
// 1. 创建 UIKit View
func makeUIView(context: Context) -> some UIView {
let view = UIView()
view.backgroundColor = .systemBlue
view.layer.cornerRadius = 12
return view
}
// 2. 更新 UIKit View
func updateUIView(_ uiView: UIViewType, context: Context) {
// 当 SwiftUI 状态变化时更新 UIKit 视图
}
// 3. Coordinator(可选)- 处理 UIKit 事件回调
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: UIKitViewWrapper
init(_ parent: UIKitViewWrapper) {
self.parent = parent
}
}
}3.1.2 WebView 示例
struct WebView: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView,
didFinish navigation: WKNavigation!) {
// 页面加载完成回调
}
func webView(_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error) {
// 加载失败回调
}
}
}
// 在 SwiftUI 中使用
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://apple.com")!)
.frame(height: 400)
}
}3.1.3 UIImagePickerController 示例
struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
@Environment(\.dismiss) private var dismiss
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[.originalImage] as? UIImage {
parent.selectedImage = image
}
parent.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.dismiss()
}
}
}
// 使用
struct PhotoPickerView: View {
@State private var showPicker = false
@State private var image: UIImage?
var body: some View {
VStack {
if let image = image {
Image(uiImage: image)
.resizable()
.frame(width: 200, height: 200)
}
Button("选择照片") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ImagePicker(selectedImage: $image)
}
}
}3.1.4 MKMapView 包装示例
import MapKit
struct MapView: UIViewRepresentable {
@Binding var region: MKCoordinateRegion
let annotations: [MKPointAnnotation]
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.setRegion(region, animated: true)
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(annotations)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView,
regionDidChangeAnimated animated: Bool) {
parent.region = mapView.region
}
func mapView(_ mapView: MKMapView,
viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let identifier = "Pin"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKMarkerAnnotationView
if view == nil {
view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
}
view?.canShowCallout = true
return view
}
}
}3.2 UIViewControllerRepresentable
包装 UIViewController 供 SwiftUI 使用:
// 包装系统邮件视图控制器
struct MailComposer: UIViewControllerRepresentable {
let recipients: [String]
let subject: String
let body: String
@Binding var isPresented: Bool
func makeUIViewController(context: Context) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
vc.setToRecipients(recipients)
vc.setSubject(subject)
vc.setMessageBody(body, isHTML: false)
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
var parent: MailComposer
init(_ parent: MailComposer) {
self.parent = parent
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
parent.isPresented = false
}
}
}3.3 UIKit 与 SwiftUI 混编
UIKit 项目中嵌入 SwiftUI 视图:
// 在 UIKit 中使用 SwiftUI
import SwiftUI
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swiftUIView = UIHostingController(rootView: SwiftUIView())
addChild(swiftUIView)
swiftUIView.view.frame = view.bounds
view.addSubview(swiftUIView.view)
swiftUIView.didMove(toParent: self)
}
}SwiftUI 中嵌入 UIKit 视图(使用 UIViewControllerRepresentable/UIViewRepresentable),完整混编示例:
// SwiftUI 中混合使用 UIKit 和 SwiftUI
struct HybridView: View {
@State private var showCamera = false
@State private var capturedImage: UIImage?
var body: some View {
ScrollView {
VStack(spacing: 20) {
// SwiftUI 原生视图
Text("SwiftUI 与 UIKit 混编")
.font(.title)
// UIKit MKMapView 包装
MapView(
region: .constant(MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 39.9, longitude: 116.4),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
)),
annotations: []
)
.frame(height: 200)
.cornerRadius(12)
// UIKit UIImagePicker 包装
if let image = capturedImage {
Image(uiImage: image)
.resizable()
.frame(height: 200)
.cornerRadius(12)
}
Button("拍摄照片") {
showCamera = true
}
.buttonStyle(.borderedProminent)
}
.padding()
}
.sheet(isPresented: $showCamera) {
ImagePicker(selectedImage: $capturedImage)
}
}
}4. 网络与持久化
4.1 异步网络请求
4.1.1 URLSession 与 async/await
struct APIClient {
private let session = URLSession.shared
private let decoder = JSONDecoder()
// 基础请求
func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw APIError.httpError(statusCode: httpResponse.statusCode)
}
return try decoder.decode(T.self, from: data)
}
// POST 请求
func post<T: Decodable, U: Encodable>(_ type: T.Type,
to url: URL,
body: U) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(body)
let (data, _) = try await session.data(for: request)
return try decoder.decode(T.self, from: data)
}
}
// 使用
Task {
do {
let url = URL(string: "https://api.example.com/users")!
let users: [User] = try await APIClient().fetch([User].self, from: url)
print("Fetched \(users.count) users")
} catch {
print("Network error: \(error)")
}
}4.1.2 Codable 与自定义解码
// 基础 Codable
struct User: Codable, Identifiable {
let id: Int
let name: String
let email: String
let createdAt: Date
}
// CodingKeys 自定义映射
struct Product: Codable {
let id: Int
let name: String
let price: Double
let isAvailable: Bool
enum CodingKeys: String, CodingKey {
case id
case name = "product_name" // JSON 字段名映射
case price
case isAvailable = "is_available" // snake_case 转 camelCase
}
}
// 自定义解码 - 处理不规则 JSON
struct Article: Codable {
let id: Int
let title: String
let publishedAt: Date
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
title = try container.decode(String.self, forKey: .title)
// 自定义日期格式解码
let dateString = try container.decode(String.self, forKey: .publishedAt)
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
guard let date = formatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(
forKey: .publishedAt,
in: container,
debugDescription: "Date string does not match format")
}
publishedAt = date
}
}
// 自定义解码 - 处理动态类型
enum Either<T: Codable, U: Codable>: Codable {
case left(T)
case right(U)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let leftValue = try? container.decode(T.self) {
self = .left(leftValue)
} else if let rightValue = try? container.decode(U.self) {
self = .right(rightValue)
} else {
throw DecodingError.typeMismatch(
Either<T, U>.self,
DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: "Neither type matched"))
}
}
}4.1.3 错误处理与重试拦截器
enum APIError: LocalizedError {
case invalidResponse
case httpError(statusCode: Int)
case decodingFailed(Error)
case noNetwork
case unauthorized
case retryExhausted
var errorDescription: String? {
switch self {
case .invalidResponse:
return "Invalid response from server"
case .httpError(let code):
return "HTTP error: \(code)"
case .decodingFailed(let error):
return "Failed to decode response: \(error.localizedDescription)"
case .noNetwork:
return "No network connection"
case .unauthorized:
return "Unauthorized - token may be expired"
case .retryExhausted:
return "Request failed after all retries"
}
}
}
// 带重试和拦截的 HTTP 客户端
actor HTTPClient {
private let session: URLSession
private let maxRetries = 3
init(session: URLSession = .shared) {
self.session = session
}
func request<T: Decodable>(_ url: URL,
retryCount: Int = 0) async throws -> T {
do {
// 请求拦截器:添加认证头
var request = URLRequest(url: url)
if let token = await TokenManager.shared.validToken {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
let (data, response) = try await session.data(for: request)
// 响应拦截器:处理 HTTP 状态码
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
return try JSONDecoder().decode(T.self, from: data)
case 401:
// Token 过期,尝试刷新
let refreshed = try await TokenManager.shared.refreshToken()
if refreshed, retryCount < maxRetries {
return try await request(url, retryCount: retryCount + 1)
}
throw APIError.unauthorized
case 429:
// 限流,退避重试
guard retryCount < maxRetries else {
throw APIError.retryExhausted
}
let backoffSeconds = pow(2.0, Double(retryCount))
try await Task.sleep(nanoseconds: UInt64(backoffSeconds * 1_000_000_000))
return try await request(url, retryCount: retryCount + 1)
default:
throw APIError.httpError(statusCode: httpResponse.statusCode)
}
} catch let error as APIError {
throw error
} catch {
// 网络错误重试
guard retryCount < maxRetries else {
throw APIError.retryExhausted
}
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(retryCount)) * 1_000_000_000))
return try await request(url, retryCount: retryCount + 1)
}
}
}4.2 Core Data
Core Data 是 Apple 官方的对象图管理与持久化框架。
4.2.1 核心组件
// 1. NSPersistentContainer - 管理 Core Data 栈
class CoreDataManager {
static let shared = CoreDataManager()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyApp")
if inMemory {
container.persistentStoreDescriptions.first?.url =
URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Core Data failed to load: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
var context: NSManagedObjectContext {
container.viewContext
}
func save() {
if context.hasChanges {
do {
try context.save()
} catch {
print("Failed to save: \(error)")
}
}
}
}
// 2. NSManagedObject 模型
// 通常通过 Core Data 模型编辑器 (.xcdatamodeld) 生成
// 或手动定义:
@objc(MyEntity)
class MyEntity: NSManagedObject {
@NSManaged var id: String
@NSManaged var name: String
@NSManaged var createdAt: Date
@NSManaged var category: String?
}
// 3. NSFetchRequest 查询
extension MyEntity {
@nonobjc class func fetchRequest() -> NSFetchRequest<MyEntity> {
return NSFetchRequest<MyEntity>(entityName: "MyEntity")
}
// 使用 NSPredicate 和 NSSortDescriptor
static func fetch(in category: String,
sortBy: String = "createdAt",
ascending: Bool = false) -> NSFetchRequest<MyEntity> {
let request = NSFetchRequest<MyEntity>(entityName: "MyEntity")
request.predicate = NSPredicate(format: "category == %@", category)
request.sortDescriptors = [
NSSortDescriptor(key: sortBy, ascending: ascending)
]
return request
}
}
// 4. @FetchRequest 在 SwiftUI 中使用
struct ContentView: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \MyEntity.createdAt, ascending: false)],
predicate: NSPredicate(format: "category == %@", "news"),
animation: .default
) private var items: FetchedResults<MyEntity>
var body: some View {
List(items) { item in
Text(item.name ?? "")
}
}
// CRUD 操作
func addItem(name: String, category: String) {
let context = CoreDataManager.shared.context
let newItem = MyEntity(context: context)
newItem.id = UUID().uuidString
newItem.name = name
newItem.category = category
newItem.createdAt = Date()
CoreDataManager.shared.save()
}
func deleteItem(_ item: MyEntity) {
CoreDataManager.shared.context.delete(item)
CoreDataManager.shared.save()
}
}4.2.2 CloudKit 同步
Core Data 与 CloudKit 集成实现 iCloud 同步:
// 配置 NSPersistentCloudKitContainer
class CloudSyncManager {
static let shared = CloudSyncManager()
let container: NSPersistentCloudKitContainer
init() {
container = NSPersistentCloudKitContainer(name: "MyApp")
guard let storeDescription = container.persistentStoreDescriptions.first else {
fatalError("No store description")
}
// 启用 CloudKit 同步
storeDescription.setOption(true as NSNumber,
forKey: NSPersistentHistoryTrackingKey)
storeDescription.setOption(true as NSNumber,
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores { _, error in
if let error = error {
print("CloudKit container load error: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
// 监听远程变化
func observeRemoteChanges() {
NotificationCenter.default.addObserver(
self,
selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator
)
}
@objc private func storeRemoteChange(_ notification: Notification) {
// CloudKit 数据同步后处理
DispatchQueue.main.async {
// 刷新 UI
}
}
}4.3 SwiftData
iOS 17+ 引入 SwiftData,基于 Swift 宏(Macro)的声明式持久化框架。
4.3.1 基本使用
import SwiftData
// 定义模型
@Model
final class Item {
@Attribute(.unique) var id: UUID
var name: String
var timestamp: Date
var category: String?
@Relationship(deleteRule: .cascade, inverse: \Tag.item)
var tags: [Tag]
init(name: String, category: String? = nil) {
self.id = UUID()
self.name = name
self.timestamp = Date()
self.category = category
self.tags = []
}
}
@Model
final class Tag {
@Attribute(.unique) var id: UUID
var name: String
var item: Item?
init(name: String) {
self.id = UUID()
self.name = name
}
}
// 配置 ModelContainer
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Item.self, Tag.self])
}
}
// 在视图中使用 ModelContext
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Item.timestamp, order: .reverse) private var items: [Item]
var body: some View {
NavigationStack {
List {
ForEach(items) { item in
VStack(alignment: .leading) {
Text(item.name)
Text(item.timestamp, style: .date)
.font(.caption)
.foregroundColor(.secondary)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
Button("Add") { addItem() }
}
}
}
private func addItem() {
let item = Item(name: "New Item", category: "general")
modelContext.insert(item)
try? modelContext.save()
}
private func deleteItems(_ indexSet: IndexSet) {
for index in indexSet {
modelContext.delete(items[index])
}
try? modelContext.save()
}
}4.3.2 Predicate 与 FetchDescriptor
// 使用 Predicate 宏(iOS 17+)
struct FilteredView: View {
@State private var searchText = ""
var body: some View {
ItemListView(filter: searchText)
}
}
struct ItemListView: View {
@Query private var items: [Item]
init(filter: String) {
if filter.isEmpty {
_items = Query(sort: \Item.timestamp, order: .reverse)
} else {
// 使用 Predicate 宏进行类型安全过滤
let predicate = #Predicate<Item> { item in
item.name.localizedStandardContains(filter) ||
(item.category?.localizedStandardContains(filter) ?? false)
}
_items = Query(filter: predicate, sort: \Item.timestamp, order: .reverse)
}
}
var body: some View {
List(items) { item in
Text(item.name)
}
}
}
// 使用 FetchDescriptor 手动查询
func fetchItems(in context: ModelContext,
category: String) throws -> [Item] {
var descriptor = FetchDescriptor<Item>(
predicate: #Predicate { $0.category == category },
sortBy: [SortDescriptor(\Item.timestamp, order: .reverse)]
)
descriptor.fetchLimit = 10
descriptor.relationshipKeyPathsForPrefetching = [\.tags]
return try context.fetch(descriptor)
}4.3.3 SwiftData 对比 Core Data 的优势
| 特性 | SwiftData | Core Data |
|---|---|---|
| 声明方式 | @Model 宏声明式 | NSManagedObject 子类 + .xcdatamodeld |
| 泛型支持 | 原生支持(@Query, FetchDescriptor) | 有限支持(泛型 NSFetchRequest) |
| 并发安全 | actor 隔离,安全模型内置 | 需要手动管理 NSManagedObjectContext |
| 代码量 | 显著减少模板代码 | 需要大量模板代码 |
| 类型安全 | 编译时类型检查 | 运行时键路径(String 类型) |
| Swift 集成 | 原生 Swift 语法 | ObjC 桥接,@objc 标记 |
| 宏驱动 | @Model, @Attribute, @Relationship | 无 |
| 最小支持版本 | iOS 17+ | iOS 3+ |
4.4 Keychain
Keychain 是 iOS 的安全存储服务,用于敏感数据(密码、Token、证书、密钥对)。
4.4.1 基础操作
import Security
import LocalAuthentication
enum KeychainError: LocalizedError {
case saveFailed(OSStatus)
case readFailed(OSStatus)
case updateFailed(OSStatus)
case deleteFailed(OSStatus)
case itemNotFound
var errorDescription: String? {
switch self {
case .saveFailed(let status):
return "Keychain save failed: \(status)"
case .readFailed(let status):
return "Keychain read failed: \(status)"
case .updateFailed(let status):
return "Keychain update failed: \(status)"
case .deleteFailed(let status):
return "Keychain delete failed: \(status)"
case .itemNotFound:
return "Keychain item not found"
}
}
}
final class KeychainManager {
static let shared = KeychainManager()
private let service = "com.example.app.keychain"
// 保存数据
func save(key: String, data: Data) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
]
// 先删除已有项
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
}
// 读取数据
func read(key: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
throw KeychainError.itemNotFound
}
throw KeychainError.readFailed(status)
}
guard let data = result as? Data else {
throw KeychainError.readFailed(errSecConversionError)
}
return data
}
// 更新数据
func update(key: String, data: Data) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status == errSecSuccess else {
throw KeychainError.updateFailed(status)
}
}
// 删除数据
func delete(key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.deleteFailed(status)
}
}
// 便捷方法:存储 Codable 对象
func saveObject<T: Encodable>(_ object: T, key: String) throws {
let data = try JSONEncoder().encode(object)
try save(key: key, data: data)
}
func readObject<T: Decodable>(_ type: T.Type, key: String) throws -> T {
let data = try read(key: key)
return try JSONDecoder().decode(type, from: data)
}
// 生物识别保护
func saveWithBiometrics(key: String, data: Data) throws {
let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
nil
)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessControl as String: accessControl as Any,
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
}
// 密钥对生成
func generateKeyPair(tag: String) throws -> (privateKey: SecKey, publicKey: SecKey) {
let attributes: [String: Any] = [
kSecAttrType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: 2048,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "\(service).\(tag).private".data(using: .utf8)!,
],
kSecPublicKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "\(service).\(tag).public".data(using: .utf8)!,
],
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw KeychainError.saveFailed(0)
}
let publicKey = SecKeyCopyPublicKey(privateKey)!
return (privateKey, publicKey)
}
}
## 5. 性能优化
### 5.1 启动优化
#### 5.1.1 pre-main 阶段
App 启动分为 pre-main 和 main 之后两个阶段。pre-main 阶段指 `main()` 函数执行之前,由系统内核和 dyld 完成的工作:
- **dyld(Dynamic Linker)加载**:解析应用和所有依赖库的 Mach-O 文件,执行符号绑定和重定位。
- **静态库数量影响**:每个静态库的加载和初始化都增加启动时间,应合并或减少静态库数量。
- **+load 方法**:ObjC 的 `+load` 方法在 main 之前执行,应避免使用,改用 `+initialize` 或现代 Swift 初始化方式。
- **ObjC 类注册**:大量 ObjC 类会导致启动阶段的元数据注册时间增加。
#### 5.1.2 二进制重排
缺页中断(Page Fault)是 App 启动慢的主要原因之一。二进制重排通过重新排列函数在二进制中的顺序,将启动路径上的函数集中到连续的页面中,减少 page fault 次数:
```swift
// 使用 LinkMap 分析启动阶段调用的函数
// 生成 order 文件,按调用顺序排列符号
// .order 文件示例
_$s14MyApp11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_So0kL14DictionaryKeyOSg2lF
_$s14MyApp6LoggerV4logyyF
_$s14MyApp10DataLoaderC5fetchyyF
_$s14MyApp14ConfigManagerC6sharedACvgZ
_$s14MyApp12RootViewControllercACycfc
// Build Settings 中配置 order 文件路径
// Order File: $(SRCROOT)/MyApp.order
// Clang 和 Swift 编译器会将 order 文件中的符号前置到二进制文件的低偏移地址,
// 从而减少启动过程中因页面数据未加载导致的 page fault。LinkMap 分析:Xcode 构建产物中包含 LinkMap 文件,记录所有符号的地址和大小,用于分析二进制体积和检测无用代码。
无用类扫描:通过 Mach-O 文件分析或运行时的 objc_getClassList 对比,检测项目中未被引用的类。
CocoaPods 二进制组件化:将 Pod 依赖预编译为静态 Framework(xcframework),减少项目编译时的源码编译量,加快编译和链接速度。
5.1.3 启动优化实践
// 1. 懒加载非关键初始化
@main
struct MyApp: App {
@StateObject private var appDelegate = AppDelegate()
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
// 首屏渲染完成后,异步执行非关键初始化
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
setupBackgroundServices()
}
}
}
}
private func setupBackgroundServices() {
// 延迟初始化非关键服务
Logger.configure()
AnalyticsManager.start()
}
}
// 2. 减少动态库依赖
// 使用静态库代替动态库(静态库直接合并到主二进制,减少 dyld 加载时间)
// 3. 合并 ObjC 分类
// 使用 -ObjC 链接器标志时,所有分类都会被加载。
// 尽量减少分类数量,或直接合并到主类实现中。
// 4. 优化 +load 方法(Swift 中不存在,ObjC 中避免使用)
// +load 方法在 main 之前执行,每个 +load 都会阻塞启动。
// 替代方案:使用 dispatch_once 或现代的 +initialize。5.2 内存优化
5.2.1 引用循环
Swift 使用 ARC 管理内存,引用循环导致内存泄漏:
// 引用循环示例
class Parent {
var child: Child?
}
class Child {
weak var parent: Parent? // weak 打破循环引用
}
// weak:引用对象可 nil,可选类型
// unowned:假定引用对象始终存在,非可选类型
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
}
class CreditCard {
let number: String
unowned let customer: Customer // unowned,card 生命周期不超出 customer
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
}
// 闭包引用循环
class ViewModel {
var onUpdate: (() -> Void)?
func setup() {
// 弱引用捕获
onUpdate = { [weak self] in
guard let self = self else { return }
self.handleUpdate()
}
}
func handleUpdate() {
// ...
}
}5.2.2 循环引用检测工具
Xcode Memory Graph Debugger:在 Xcode 中点击 Debug Memory Graph 按钮,查看所有存活对象和引用关系,红色警告标记泄漏对象。
MLeaksFinder:腾讯开源的内存泄漏检测工具,基于 UIViewController 和 UIView 的泄漏检测:
// MLeaksFinder 自动检测泄漏的 ViewController
// 在 Podfile 中集成:
// pod 'MLeaksFinder'
// 集成后运行时自动检测,无需手动调用。
// 手动检测泄漏
func checkMemoryLeak() {
weak var weakRef = self
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if weakRef == nil {
print("对象已正常释放")
} else {
print("警告:对象可能存在内存泄漏")
}
}
}5.2.3 图片缓存
// NSCache - 系统提供的缓存容器,自动响应内存警告
final class ImageCache {
static let shared = ImageCache()
private let cache = NSCache<NSString, UIImage>()
private init() {
cache.countLimit = 100 // 最大缓存数量
cache.totalCostLimit = 50 * 1024 * 1024 // 最大缓存 50MB
}
func set(_ image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString,
cost: Int(image.size.width * image.size.height * 4))
}
func get(forKey key: String) -> UIImage? {
cache.object(forKey: key as NSString)
}
func removeAll() {
cache.removeAllObjects()
}
}
// Temporary 缓存 - 使用临时文件目录
extension URLSession {
static let cachedSession: URLSession = {
let config = URLSessionConfiguration.default
config.urlCache = URLCache(
memoryCapacity: 20 * 1024 * 1024, // 20MB 内存缓存
diskCapacity: 100 * 1024 * 1024, // 100MB 磁盘缓存
diskPath: "image_cache"
)
return URLSession(configuration: config)
}()
}5.2.4 大图优化
Downsampling(降采样):避免将大图完整解码到内存,生成缩略图:
import ImageIO
extension UIImage {
// 缩略图生成 - 使用 ImageIO 避免完整解码
static func thumbnail(from url: URL, targetSize: CGSize) -> UIImage? {
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }
let options: [CFString: Any] = [
kCGImageSourceThumbnailMaxPixelSize: max(targetSize.width, targetSize.height),
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
]
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
return nil
}
return UIImage(cgImage: cgImage)
}
// 异步解码 - 在后台线程解码,避免主线程卡顿
func decodedAsync() async -> UIImage? {
return Task.detached(priority: .background) { [self] in
// 强制解码
UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
defer { UIGraphicsEndImageContext() }
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.interpolationQuality = .high
self.draw(at: .zero)
let decodedImage = UIGraphicsGetImageFromCurrentImageContext()
return decodedImage
}.value
}
}
// 后台解码使用
struct OptimizedImageView: View {
let imageURL: URL
@State private var thumbnail: UIImage?
var body: some View {
Group {
if let thumbnail = thumbnail {
Image(uiImage: thumbnail)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
ProgressView()
.task {
// 只解码缩略图大小,不加载完整图
thumbnail = UIImage.thumbnail(
from: imageURL,
targetSize: CGSize(width: 200, height: 200)
)
}
}
}
}
}5.2.5 字体与文本优化
// NSAttributedString 性能优化
// 1. 避免频繁创建 NSAttributedString
// 2. 使用缓存复用
class AttributedStringCache {
static let shared = AttributedStringCache()
private var cache = NSCache<NSString, NSAttributedString>()
func attributedString(for key: String, builder: () -> NSAttributedString) -> NSAttributedString {
if let cached = cache.object(forKey: key as NSString) {
return cached
}
let result = builder()
cache.setObject(result, forKey: key as NSString)
return result
}
}
// 使用 TextKit 的 NSTextStorage 批量处理富文本
// 避免在 cell 中频繁计算行高,使用自动布局或缓存行高
// 字体文件应压缩后随包发布,延迟加载非默认字体5.3 ANR 卡顿
5.3.1 主线程监控
iOS 应用中所有 UI 操作必须在主线程执行,长时间的主线程阻塞导致应用无响应(ANR)。
RunLoop 监控:通过监听主线程 RunLoop 的状态变化检测卡顿:
// RunLoop 监控
final class LagMonitor {
static let shared = LagMonitor()
private let mainRunLoop = CFRunLoopGetMain()
private var observer: CFRunLoopObserver?
private var timeoutCount = 0
func start() {
// 创建 RunLoop 观察者
observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
CFIndex(CFRunLoopActivity.beforeWaiting.rawValue |
CFRunLoopActivity.afterWaiting.rawValue),
true,
0
) { [weak self] observer, activity in
if activity == .afterWaiting {
// 进入等待状态后的第一次运行,记录时间
self?.timeoutCount = 0
}
}
if let observer = observer {
CFRunLoopAddObserver(mainRunLoop, observer, .defaultMode)
}
}
func stop() {
if let observer = observer {
CFRunLoopRemoveObserver(mainRunLoop, observer, .defaultMode)
}
}
}CADisplayLink FPS:通过 CADisplayLink 监控帧率:
final class FPSMonitor {
private var displayLink: CADisplayLink?
private var lastTimestamp: CFTimeInterval = 0
private var frameCount = 0
var onFPSUpdate: ((Int) -> Void)?
func start() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .common)
}
@objc private func tick(link: CADisplayLink) {
if lastTimestamp == 0 {
lastTimestamp = link.timestamp
return
}
frameCount += 1
let elapsed = link.timestamp - lastTimestamp
if elapsed >= 1 {
let fps = Int(Double(frameCount) / elapsed)
frameCount = 0
lastTimestamp = link.timestamp
if fps < 45 {
// 出现卡顿,采集堆栈
captureLagStack()
}
DispatchQueue.main.async {
self.onFPSUpdate?(fps)
}
}
}
private func captureLagStack() {
// 使用 BSBacktraceLogger 或 PLCrashReporter 采集堆栈
let symbols = Thread.callStackSymbols
// 上报堆栈符号
LagReporter.report(symbols: symbols)
}
func stop() {
displayLink?.invalidate()
displayLink = nil
}
}5.3.2 卡顿堆栈采集
// 卡顿采集,信息收集与符号化
final class LagReporter {
static let shared = LagReporter()
// 采集当前主线程堆栈
func captureMainThreadStack() -> [String] {
return Thread.callStackSymbols
}
// 符号化堆栈(将地址符号化为方法名)
func symbolicate(stackSymbols: [String]) -> [String] {
// 使用 dladdr 进行符号解析
return stackSymbols.map { symbol in
guard let range = symbol.range(of: "0x[0-9a-fA-F]+", options: .regularExpression) else {
return symbol
}
let addressString = symbol[range]
var info = Dl_info()
let address = strtoul(String(addressString), nil, 16)
if dladdr(UnsafeRawPointer(bitPattern: address), &info) != 0,
let symbolName = info.dli_sname {
return String(cString: info.dli_sname)
}
return symbol
}
}
// 聚合分析上报
struct LagReport: Codable {
let timestamp: Date
let duration: TimeInterval
let mainThreadStack: [String]
let appVersion: String
let deviceModel: String
let osVersion: String
}
func report(duration: TimeInterval, symbols: [String]) {
let report = LagReport(
timestamp: Date(),
duration: duration,
mainThreadStack: symbols,
appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "",
deviceModel: UIDevice.current.model,
osVersion: UIDevice.current.systemVersion
)
// 序列化并上报
guard let data = try? JSONEncoder().encode(report) else { return }
// 上报到服务器或日志系统
// APMService.send(data)
}
}5.3.3 Main Thread Checker
Xcode 内置的 Main Thread Checker 在运行时检测非主线程的 UI 操作:
- 在 Xcode Scheme > Run > Diagnostics 中启用 Main Thread Checker。
- 检测到在主线程之外更新 UI 时,在调试控制台输出警告并中断调试器。
- 常见违规:
UIImage创建、UIView属性修改、UIControl事件触发在非主线程。
5.3.4 卡顿优化实践
// 1. 异步处理耗时操作
func loadHeavyData() {
Task.detached(priority: .background) { [weak self] in
let processedData = await self?.processLargeDataset()
await MainActor.run {
// 回到主线程更新 UI
self?.updateUI(with: processedData)
}
}
}
// 2. 避免 cell 中动态布局开销
// 使用 Auto Layout 时保证约束简单,或使用 frame 布局
// 3. 图片异步解码(见 5.2.4 节)
// 4. 文本异步绘制(使用 TextKit 或 AsyncDisplayKit)
// 5. 预加载(Prefetching)
// 6. 使用 UICollectionView 的 prefetching API
class ViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.isPrefetchingEnabled = true
collectionView?.prefetchDataSource = self
}
}
extension ViewController: UICollectionViewDataSourcePrefetching {
func collectionView(_ collectionView: UICollectionView,
prefetchItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
// 预先加载 indexPath 对应的数据
let model = dataSource[indexPath.item]
ImageCache.shared.prefetch(url: model.imageURL)
}
}
}
// 7. 减少离屏渲染
// 使用 layer.masksToBounds 和 layer.shadow* 时会产生离屏渲染
// 使用系统提供的高性能组件,避免复杂自定义 drawRect