普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月17日首页

第5章:基础状态管理

作者 90后晨仔
2026年4月16日 21:57

Snip20260416_2.png

示例代码都放这里啦,有需要的可以下载学习。swiftUIDemo

5.1 @State:本地视图状态

@State 介绍

@State 是 SwiftUI 中最基本的状态管理工具,用于管理视图的本地状态。它是一个属性包装器,允许我们在结构体中创建可变状态。

基本用法

import SwiftUI

struct CounterView: View {
    // 使用 @State 声明本地状态
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            // 显示状态值
            Text("Count: \(count)")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            // 修改状态
            Button("Increment") {
                count += 1  // 状态改变,UI 自动更新
            }
            .buttonStyle(.borderedProminent)
            
            Button("Reset") {
                count = 0  // 状态重置
            }
            .buttonStyle(.bordered)
        }
        .padding()
    }
}

#Preview {
    CounterView()
}

工作原理

@State 的工作原理:

  1. 当你使用 @State 标记一个属性时,SwiftUI 会在底层为这个属性创建一个独立的存储
  2. 这个存储不受结构体值类型特性的影响,即使结构体被重新创建,状态也会保持
  3. 当状态值改变时,SwiftUI 会自动重新计算视图的 body 属性
  4. 系统会对比新旧视图树,只更新发生变化的部分

最佳实践

  1. 标记为 private@State 应该只在当前视图内部使用,所以应该标记为 private
  2. 初始值:必须为 @State 属性提供初始值
  3. 避免在 body 中修改:不要在 body 计算属性中直接修改 @State
  4. 简单类型@State 适合存储简单类型(如 Bool、Int、String 等)

5.2 @Binding:父子视图双向绑定

@Binding 介绍

@Binding 用于在父子视图之间创建双向绑定,允许子视图修改父视图的状态。

基本用法

import SwiftUI

// 父视图
struct ParentView: View {
    // 父视图的状态
    @State private var isPlaying = false
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Parent View")
                .font(.headline)
            
            Text("Is Playing: \(isPlaying ? "Yes" : "No")")
            
            // 使用 $ 符号创建绑定并传递给子视图
            PlayButton(isPlaying: $isPlaying)
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

// 子视图
struct PlayButton: View {
    // 使用 @Binding 接收父视图的状态引用
    @Binding var isPlaying: Bool
    
    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            // 修改绑定值,会同步更新父视图的状态
            isPlaying.toggle()
        }
        .buttonStyle(.borderedProminent)
        .tint(isPlaying ? .red : .green)
        .padding()
        .background(Color.gray.opacity(0.05))
        .cornerRadius(8)
    }
}

#Preview {
    ParentView()
}

工作原理

@Binding 的工作原理:

  1. 它不是存储状态,而是创建一个对现有状态的引用
  2. 当子视图修改绑定值时,实际上是修改了原始的 @State 状态
  3. 状态的所有权仍然在父视图中
  4. 这种机制确保了单一数据源(SSOT)原则

实际应用

// 表单输入示例
struct FormView: View {
    @State private var username = ""
    @State private var email = ""
    
    var body: some View {
        VStack(spacing: 16) {
            Text("User Form")
                .font(.headline)
            
            TextFieldView(
                title: "Username",
                text: $username,
                placeholder: "Enter your username"
            )
            
            TextFieldView(
                title: "Email",
                text: $email,
                placeholder: "Enter your email"
            )
            
            Text("Username: \(username)")
            Text("Email: \(email)")
        }
        .padding()
    }
}

struct TextFieldView: View {
    let title: String
    @Binding var text: String
    let placeholder: String
    
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.subheadline)
                .fontWeight(.medium)
            
            TextField(
                placeholder,
                text: $text
            )
            .textFieldStyle(.roundedBorder)
        }
    }
}

5.3 @StateObject:可观察对象状态

@StateObject 介绍

@StateObject 用于管理符合 ObservableObject 协议的对象,适用于需要在多个视图之间共享的复杂状态。

基本用法

import SwiftUI
import Combine

// 可观察对象模型
class UserViewModel: ObservableObject {
    // 使用 @Published 标记需要发布的属性
    @Published var username = ""
    @Published var email = ""
    @Published var isLoggedIn = false
    
    func login() {
        // 模拟登录操作
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.isLoggedIn = true
        }
    }
    
    func logout() {
        username = ""
        email = ""
        isLoggedIn = false
    }
}

struct UserView: View {
    // 使用 @StateObject 管理可观察对象
    @StateObject private var viewModel = UserViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("User Profile")
                .font(.headline)
            
            if viewModel.isLoggedIn {
                Text("Welcome, \(viewModel.username)!")
                    .font(.title)
                Text("Email: \(viewModel.email)")
                
                Button("Logout") {
                    viewModel.logout()
                }
                .buttonStyle(.borderedProminent)
                .tint(.red)
            } else {
                TextField("Username", text: $viewModel.username)
                    .textFieldStyle(.roundedBorder)
                TextField("Email", text: $viewModel.email)
                    .textFieldStyle(.roundedBorder)
                
                Button("Login") {
                    viewModel.login()
                }
                .buttonStyle(.borderedProminent)
                .disabled(viewModel.username.isEmpty || viewModel.email.isEmpty)
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

#Preview {
    UserView()
}

工作原理

@StateObject 的工作原理:

  1. 它会创建并拥有一个符合 ObservableObject 协议的对象
  2. 当对象的 @Published 属性改变时,所有使用该对象的视图都会自动更新
  3. 即使视图被重新创建,@StateObject 也会保持对象的生命周期
  4. 适用于需要在多个视图之间共享的复杂状态

最佳实践

  1. 用于复杂状态:适用于包含多个相关属性的复杂状态
  2. 单一数据源:作为状态的唯一来源
  3. 生命周期管理:由 SwiftUI 管理对象的生命周期
  4. 性能考虑:对于大型对象,考虑使用更细粒度的状态管理

5.4 @ObservedObject:观察外部对象

@ObservedObject 介绍

@ObservedObject 用于观察外部传入的符合 ObservableObject 协议的对象,适用于从父视图传递的可观察对象。

基本用法

import SwiftUI

// 父视图
struct ParentWithObservedObject: View {
    // 父视图拥有状态对象
    @StateObject private var userViewModel = UserViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Parent View")
                .font(.headline)
            
            // 传递给子视图
            ChildView(viewModel: userViewModel)
        }
        .padding()
    }
}

// 子视图
struct ChildView: View {
    // 使用 @ObservedObject 观察外部对象
    @ObservedObject var viewModel: UserViewModel
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Child View")
                .font(.subheadline)
            
            TextField("Username", text: $viewModel.username)
                .textFieldStyle(.roundedBorder)
            
            TextField("Email", text: $viewModel.email)
                .textFieldStyle(.roundedBorder)
            
            Button("Login") {
                viewModel.login()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

#Preview {
    ParentWithObservedObject()
}

工作原理

@ObservedObject 的工作原理:

  1. 它不拥有对象,只是观察外部传入的对象
  2. 当对象的 @Published 属性改变时,视图会自动更新
  3. 对象的生命周期由其拥有者管理
  4. 适用于从父视图传递的可观察对象

与 @StateObject 的区别

特性 @StateObject @ObservedObject
所有权 拥有对象,管理生命周期 观察对象,不管理生命周期
初始化 在视图中直接初始化 从外部传入
适用场景 作为状态的唯一来源 观察父视图传递的对象
性能 更高效,避免重复创建 可能会因父视图重建而重复创建

5.5 @EnvironmentObject:全局环境对象

@EnvironmentObject 介绍

@EnvironmentObject 用于访问通过环境传递的全局可观察对象,适用于跨多个视图层级共享的状态。

基本用法

import SwiftUI

// 全局状态模型
class AppState: ObservableObject {
    @Published var isDarkMode = false
    @Published var userLanguage = "zh"
    
    func toggleDarkMode() {
        isDarkMode.toggle()
    }
    
    func changeLanguage(to language: String) {
        userLanguage = language
    }
}

// 主视图 - 设置环境对象
struct MainView: View {
    @StateObject private var appState = AppState()
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Main View")
                    .font(.headline)
                
                NavigationLink("Settings", destination: SettingsView())
                NavigationLink("Profile", destination: ProfileView())
            }
            .padding()
        }
        // 通过环境传递对象
        .environmentObject(appState)
    }
}

// 设置视图 - 访问环境对象
struct SettingsView: View {
    // 通过 @EnvironmentObject 访问全局对象
    @EnvironmentObject private var appState: AppState
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Settings")
                .font(.headline)
            
            Toggle("Dark Mode", isOn: $appState.isDarkMode)
            
            Picker("Language", selection: $appState.userLanguage) {
                Text("English").tag("en")
                Text("中文").tag("zh")
                Text("日本語").tag("ja")
            }
            .pickerStyle(.segmented)
        }
        .padding()
        .background(appState.isDarkMode ? Color.black : Color.white)
        .foregroundColor(appState.isDarkMode ? Color.white : Color.black)
    }
}

// 个人资料视图 - 访问环境对象
struct ProfileView: View {
    @EnvironmentObject private var appState: AppState
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Profile")
                .font(.headline)
            
            Text("Current Language: \(appState.userLanguage)")
            Text("Dark Mode: \(appState.isDarkMode ? "On" : "Off")")
        }
        .padding()
        .background(appState.isDarkMode ? Color.black : Color.white)
        .foregroundColor(appState.isDarkMode ? Color.white : Color.black)
    }
}

#Preview {
    MainView()
}

工作原理

@EnvironmentObject 的工作原理:

  1. 它从环境中查找指定类型的可观察对象
  2. 当对象的 @Published 属性改变时,所有使用该对象的视图都会自动更新
  3. 不需要手动传递对象,通过环境自动注入
  4. 适用于跨多个视图层级共享的全局状态

最佳实践

  1. 全局状态:用于应用级别的全局状态
  2. 依赖注入:通过环境进行依赖注入,避免层层传递
  3. 类型安全:基于类型查找,确保类型正确
  4. 错误处理:确保在使用前在环境中设置了对象

5.6 @Environment:环境值

@Environment 介绍

@Environment 用于访问 SwiftUI 环境中的系统值,如布局方向、颜色方案、字体大小等。

基本用法

import SwiftUI

struct EnvironmentValuesView: View {
    // 访问环境值
    @Environment(\.colorScheme) private var colorScheme
    @Environment(\.layoutDirection) private var layoutDirection
    @Environment(\.dynamicTypeSize) private var dynamicTypeSize
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Environment Values")
                .font(.headline)
            
            Text("Color Scheme: \(colorScheme == .dark ? "Dark" : "Light")")
            Text("Layout Direction: \(layoutDirection == .leftToRight ? "LTR" : "RTL")")
            Text("Dynamic Type Size: \(dynamicTypeSize.description)")
            Text("Horizontal Size Class: \(horizontalSizeClass == .regular ? "Regular" : "Compact")")
            
            // 根据环境值调整布局
            if horizontalSizeClass == .regular {
                HStack {
                    Text("Wide Layout")
                    Spacer()
                    Text("More Content")
                }
            } else {
                VStack {
                    Text("Narrow Layout")
                    Text("Content Below")
                }
            }
        }
        .padding()
        .background(colorScheme == .dark ? Color.black : Color.white)
        .foregroundColor(colorScheme == .dark ? Color.white : Color.black)
    }
}

#Preview {
    EnvironmentValuesView()
}

常用环境值

环境值 类型 描述
\.colorScheme ColorScheme 当前颜色方案(浅色/深色)
\.layoutDirection LayoutDirection 布局方向(LTR/RTL)
\.dynamicTypeSize DynamicTypeSize 动态字体大小
\.horizontalSizeClass UserInterfaceSizeClass? 水平尺寸类
\.verticalSizeClass UserInterfaceSizeClass? 垂直尺寸类
\.locale Locale 当前区域设置
\.calendar Calendar 当前日历
\.timeZone TimeZone 当前时区
\.accessibilityEnabled Bool 是否启用辅助功能
\.scenePhase ScenePhase 场景阶段(活跃/非活跃/背景)

工作原理

@Environment 的工作原理:

  1. 它从 SwiftUI 环境中读取指定的环境值
  2. 当环境值改变时,视图会自动更新
  3. 环境值由系统或父视图设置
  4. 适用于响应系统设置和环境变化

5.7 @SceneStorage:场景存储

@SceneStorage 介绍

@SceneStorage 用于在场景级别持久化存储简单数据,适用于保存用户偏好设置和状态。

基本用法

import SwiftUI

struct SceneStorageView: View {
    // 使用 @SceneStorage 存储数据
    @SceneStorage("username") private var username = ""
    @SceneStorage("isDarkMode") private var isDarkMode = false
    @SceneStorage("counter") private var counter = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Scene Storage")
                .font(.headline)
            
            TextField("Username", text: $username)
                .textFieldStyle(.roundedBorder)
            
            Toggle("Dark Mode", isOn: $isDarkMode)
            
            VStack {
                Text("Counter: \(counter)")
                HStack {
                    Button("Increment") {
                        counter += 1
                    }
                    .buttonStyle(.bordered)
                    
                    Button("Reset") {
                        counter = 0
                    }
                    .buttonStyle(.bordered)
                }
            }
            
            Text("Note: Data persists across app restarts")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
        .background(isDarkMode ? Color.black : Color.white)
        .foregroundColor(isDarkMode ? Color.white : Color.black)
    }
}

#Preview {
    SceneStorageView()
}

工作原理

@SceneStorage 的工作原理:

  1. 它将数据存储在场景的 UserDefaults 中
  2. 数据会在场景重启后保持
  3. 每个场景有自己的存储,不同场景之间数据隔离
  4. 适用于存储用户偏好设置和临时状态

最佳实践

  1. 简单数据:适合存储简单类型(String、Int、Bool 等)
  2. 场景隔离:每个场景有独立的存储
  3. 自动持久化:数据自动保存,无需手动管理
  4. 键名唯一性:使用唯一的键名避免冲突

5.8 @AppStorage:应用存储

@AppStorage 介绍

@AppStorage 用于在应用级别持久化存储简单数据,适用于保存全局用户偏好设置。

基本用法

import SwiftUI

struct AppStorageView: View {
    // 使用 @AppStorage 存储数据
    @AppStorage("userName") private var userName = "Guest"
    @AppStorage("appTheme") private var appTheme = "light"
    @AppStorage("notificationsEnabled") private var notificationsEnabled = true
    
    var body: some View {
        VStack(spacing: 20) {
            Text("App Storage")
                .font(.headline)
            
            TextField("User Name", text: $userName)
                .textFieldStyle(.roundedBorder)
            
            Picker("Theme", selection: $appTheme) {
                Text("Light").tag("light")
                Text("Dark").tag("dark")
                Text("Auto").tag("auto")
            }
            .pickerStyle(.segmented)
            
            Toggle("Enable Notifications", isOn: $notificationsEnabled)
            
            Text("Note: Data persists across app reinstalls")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
        .background(getThemeColor())
        .foregroundColor(appTheme == "dark" ? Color.white : Color.black)
    }
    
    private func getThemeColor() -> Color {
        switch appTheme {
        case "dark":
            return Color.black
        case "light":
            return Color.white
        default:
            return Color.white
        }
    }
}

#Preview {
    AppStorageView()
}

工作原理

@AppStorage 的工作原理:

  1. 它将数据存储在应用的 UserDefaults 中
  2. 数据会在应用重启后保持
  3. 所有场景共享相同的存储
  4. 适用于存储全局用户偏好设置

与 @SceneStorage 的区别

特性 @AppStorage @SceneStorage
存储范围 应用级别,所有场景共享 场景级别,每个场景独立
持久化 持久化到 UserDefaults 持久化到场景的 UserDefaults
适用场景 全局偏好设置 场景特定状态
数据共享 跨场景共享 场景隔离

5.9 @FocusedValue:聚焦值

@FocusedValue 介绍

@FocusedValue 用于在视图层次结构中传递聚焦状态相关的值,适用于处理键盘焦点和上下文相关操作。

基本用法

import SwiftUI

// 定义聚焦值键
struct EditModeKey: FocusedValueKey {
    typealias Value = Bool
}

// 扩展 FocusedValues
extension FocusedValues {
    var isEditMode: EditModeKey.Value? {
        get { self[EditModeKey.self] }
        set { self[EditModeKey.self] = newValue }
    }
}

struct FocusedValueView: View {
    @State private var isEditMode = false
    @State private var text = "Hello, SwiftUI"
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Focused Value")
                .font(.headline)
            
            // 设置聚焦值
            TextField("Enter text", text: $text)
                .textFieldStyle(.roundedBorder)
                .focusedValue(\.isEditMode, true)
            
            Button("Toggle Edit Mode") {
                isEditMode.toggle()
            }
            .buttonStyle(.borderedProminent)
            
            // 子视图访问聚焦值
            FocusedChildView()
        }
        .padding()
        .environment(\.isEditMode, isEditMode)
    }
}

struct FocusedChildView: View {
    // 访问聚焦值
    @FocusedValue(\.isEditMode) private var isEditMode
    
    var body: some View {
        VStack {
            Text("Child View")
                .font(.subheadline)
            
            Text("Edit Mode: \(isEditMode ?? false ? "On" : "Off")")
            
            if isEditMode ?? false {
                Text("Editing is enabled!")
                    .foregroundColor(.green)
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

#Preview {
    FocusedValueView()
}

工作原理

@FocusedValue 的工作原理:

  1. 它通过 FocusedValues 字典传递值
  2. 当焦点变化时,聚焦值会自动更新
  3. 适用于与焦点相关的上下文信息
  4. 可以自定义聚焦值键

适用场景

  1. 键盘焦点:跟踪当前聚焦的视图
  2. 上下文操作:根据聚焦状态显示不同的操作
  3. 编辑模式:在编辑模式下显示额外的控件
  4. 工具栏配置:根据当前聚焦的内容配置工具栏

5.10 状态驱动 UI 更新原理

核心原理

SwiftUI 的核心设计哲学是状态驱动

UI = f(State)

这意味着:

  1. UI 是状态的函数
  2. 当状态改变时,UI 会自动更新
  3. 给定相同的状态,总是渲染相同的 UI

更新流程

当状态改变时,SwiftUI 的更新流程如下:

  1. 状态改变:用户操作或其他因素导致状态值发生变化
  2. 检测变化:SwiftUI 检测到状态变化
  3. 重新计算 body:重新调用受影响视图的 body 计算属性
  4. 构建新视图树:生成新的视图层次结构
  5. 对比差异:对比新旧视图树,找出变化的部分
  6. 更新 UI:只更新发生变化的部分,保持其他部分不变

性能优化

SwiftUI 的状态驱动机制本身就很高效,因为:

  1. 增量更新:只更新变化的部分
  2. 值类型:视图是轻量级的值类型,创建成本低
  3. 智能对比:使用高效的差异算法
  4. 批处理:合并多个状态更新,减少渲染次数
  5. 懒加载:只渲染可见的部分

5.11 状态管理最佳实践

1. 选择合适的状态管理工具

状态类型 推荐工具 适用场景
本地简单状态 @State 单个视图的内部状态
父子视图共享 @Binding 子视图需要修改父视图状态
复杂对象状态 @StateObject 多个属性的复杂状态
外部对象引用 @ObservedObject 观察父视图传递的对象
全局共享状态 @EnvironmentObject 跨多个视图的全局状态
系统环境值 @Environment 访问系统设置和环境
场景级持久化 @SceneStorage 场景特定的持久状态
应用级持久化 @AppStorage 全局用户偏好设置
聚焦相关状态 @FocusedValue 与焦点相关的上下文信息

2. 状态管理原则

  1. 单一数据源:每个状态应该有唯一的来源
  2. 状态提升:将状态提升到需要访问它的所有视图的共同父视图
  3. 最小化状态:只存储必要的状态,避免冗余
  4. 状态隔离:将相关状态组织在一起,避免混乱
  5. 可测试性:状态管理应该易于测试
  6. 性能考虑:对于大型状态,考虑使用更细粒度的更新

3. 性能优化技巧

  1. 使用 Equatable:为模型实现 Equatable 协议,避免不必要的更新
  2. 视图分离:将复杂视图拆分为更小的子视图
  3. @State 用于简单类型@State 适合存储简单类型,复杂类型使用 @StateObject
  4. 避免在 body 中创建对象:不要在 body 计算属性中创建新对象
  5. 使用 .id() 强制更新:当需要强制视图更新时使用
  6. 考虑使用 Combine:对于复杂的异步操作,使用 Combine 框架

实战:创建一个完整的状态管理示例

需求分析

创建一个包含多种状态管理技术的应用,包括:

  1. 本地状态管理
  2. 父子视图绑定
  3. 可观察对象
  4. 环境对象
  5. 持久化存储

代码实现

import SwiftUI
import Combine

// 全局应用状态
class AppState: ObservableObject {
    @Published var isDarkMode = false
    @Published var currentUser: User? = nil
    
    func toggleTheme() {
        isDarkMode.toggle()
    }
    
    func login(user: User) {
        currentUser = user
    }
    
    func logout() {
        currentUser = nil
    }
}

// 用户模型
struct User: Identifiable, Equatable {
    let id = UUID()
    let name: String
    let email: String
}

// 主应用视图
struct StateManagementDemo: View {
    @StateObject private var appState = AppState()
    @AppStorage("lastLoggedInUser") private var lastLoggedInUser = ""
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                Text("状态管理演示")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                
                // 主题切换
                Toggle("深色模式", isOn: $appState.isDarkMode)
                
                // 用户登录状态
                if appState.currentUser != nil {
                    Text("欢迎, \(appState.currentUser?.name ?? "")!")
                    Button("退出登录") {
                        appState.logout()
                        lastLoggedInUser = ""
                    }
                    .buttonStyle(.borderedProminent)
                    .tint(.red)
                } else {
                    LoginView()
                }
                
                // 导航链接
                NavigationLink("计数器示例", destination: CounterView())
                NavigationLink("待办事项示例", destination: TodoApp())
                NavigationLink("环境对象示例", destination: EnvironmentObjectDemo())
            }
            .padding()
        }
        .environmentObject(appState)
        .preferredColorScheme(appState.isDarkMode ? .dark : .light)
    }
}

// 登录视图
struct LoginView: View {
    @State private var name = ""
    @State private var email = ""
    @EnvironmentObject private var appState: AppState
    @AppStorage("lastLoggedInUser") private var lastLoggedInUser = ""
    
    var body: some View {
        VStack(spacing: 16) {
            TextField("姓名", text: $name)
                .textFieldStyle(.roundedBorder)
            TextField("邮箱", text: $email)
                .textFieldStyle(.roundedBorder)
            Button("登录") {
                let user = User(name: name, email: email)
                appState.login(user: user)
                lastLoggedInUser = name
            }
            .buttonStyle(.borderedProminent)
            .disabled(name.isEmpty || email.isEmpty)
            
            if !lastLoggedInUser.isEmpty {
                Text("上次登录: \(lastLoggedInUser)")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

// 计数器视图
struct CounterView: View {
    @State private var count = 0
    @SceneStorage("counterValue") private var storedCount = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("计数器")
                .font(.headline)
            Text("Count: \(count)")
                .font(.largeTitle)
                .fontWeight(.bold)
            HStack(spacing: 16) {
                Button("减1") {
                    count -= 1
                    storedCount = count
                }
                .buttonStyle(.bordered)
                Button("重置") {
                    count = 0
                    storedCount = 0
                }
                .buttonStyle(.bordered)
                Button("加1") {
                    count += 1
                    storedCount = count
                }
                .buttonStyle(.borderedProminent)
            }
            Text("场景存储值: \(storedCount)")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
        .onAppear {
            // 从场景存储恢复
            count = storedCount
        }
    }
}

// 待办事项应用
struct TodoApp: View {
    @State private var todos: [TodoItem] = [
        TodoItem(title: "学习 SwiftUI 状态管理"),
        TodoItem(title: "完成本章练习"),
        TodoItem(title: "构建示例应用")
    ]
    @State private var newTodoTitle = ""
    
    var body: some View {
        VStack {
            HStack(spacing: 8) {
                TextField("输入新的待办事项", text: $newTodoTitle)
                    .textFieldStyle(.roundedBorder)
                Button("添加") {
                    addTodo()
                }
                .buttonStyle(.borderedProminent)
                .disabled(newTodoTitle.isEmpty)
            }
            .padding()
            List {
                ForEach($todos) { $todo in
                    HStack {
                        Button(action: {
                            todo.isCompleted.toggle()
                        }) {
                            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundColor(todo.isCompleted ? .green : .gray)
                        }
                        .buttonStyle(.plain)
                        Text(todo.title)
                            .strikethrough(todo.isCompleted, color: .gray)
                            .foregroundColor(todo.isCompleted ? .secondary : .primary)
                        Spacer()
                        Button(action: {
                            deleteTodo(todo)
                        }) {
                            Image(systemName: "trash.fill")
                                .foregroundColor(.red)
                        }
                        .buttonStyle(.plain)
                    }
                }
            }
        }
        .navigationTitle("待办事项")
    }
    
    private func addTodo() {
        guard !newTodoTitle.isEmpty else { return }
        todos.append(TodoItem(title: newTodoTitle))
        newTodoTitle = ""
    }
    
    private func deleteTodo(_ todo: TodoItem) {
        if let index = todos.firstIndex(where: { $0.id == todo.id }) {
            todos.remove(at: index)
        }
    }
}

// 待办事项模型
struct TodoItem: Identifiable, Equatable {
    let id = UUID()
    var title: String
    var isCompleted = false
}

// 环境对象演示
struct EnvironmentObjectDemo: View {
    @EnvironmentObject private var appState: AppState
    
    var body: some View {
        VStack(spacing: 20) {
            Text("环境对象演示")
                .font(.headline)
            Text("当前主题: \(appState.isDarkMode ? "深色" : "浅色")")
            Text("登录状态: \(appState.currentUser != nil ? "已登录" : "未登录")")
            if let user = appState.currentUser {
                Text("用户: \(user.name)")
                Text("邮箱: \(user.email)")
            }
            Button("切换主题") {
                appState.toggleTheme()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

#Preview {
    StateManagementDemo()
}

代码解析

  1. AppState:使用 @StateObject 管理全局应用状态
  2. User:符合 IdentifiableEquatable 协议的用户模型
  3. StateManagementDemo:主应用视图,设置环境对象
  4. LoginView:使用 @State@AppStorage 管理登录状态
  5. CounterView:使用 @State@SceneStorage 管理计数器
  6. TodoApp:使用 @State 管理待办事项列表
  7. EnvironmentObjectDemo:使用 @EnvironmentObject 访问全局状态

小结

本章详细介绍了 SwiftUI 中的状态管理系统,包括:

  • @State:用于管理视图的本地状态
  • @Binding:用于父子视图之间的双向绑定
  • @StateObject:用于管理可观察对象的状态
  • @ObservedObject:用于观察外部传入的可观察对象
  • @EnvironmentObject:用于访问全局环境对象
  • @Environment:用于访问系统环境值
  • @SceneStorage:用于场景级别的持久化存储
  • @AppStorage:用于应用级别的持久化存储
  • @FocusedValue:用于传递聚焦相关的值
  • 状态驱动 UI 更新的原理
  • 状态管理的最佳实践
  • 一个完整的状态管理示例应用

通过本章的学习,你已经掌握了 SwiftUI 中所有的状态管理技术,能够根据不同的场景选择合适的状态管理工具,创建具有复杂交互功能的应用。


参考资料


本内容为《SwiftUI 基础教程》第五章,欢迎关注后续更新。

昨天 — 2026年4月16日首页

第4章:基础布局系统

作者 90后晨仔
2026年4月16日 21:21

Snip20260416_1.png

Snip20260416_2.png

示例代码都放这里啦,有需要的可以下载学习。swiftUIDemo

第4章:基础布局系统

4.1 垂直布局:VStack

VStack 介绍

VStack 是 SwiftUI 中用于垂直堆叠视图的容器,它会将子视图按垂直方向排列。VStack 是构建垂直布局的基础组件,适用于需要从上到下排列的界面元素。

基本用法


// 基本垂直栈

VStack {

    Text("第一行")

    Text("第二行")

    Text("第三行")

}

  


// 带间距和对齐的垂直栈

VStack(alignment: .leading, spacing: 20) {

    Text("左对齐")

    Text("第二行")

    Text("第三行")

}

.padding()

对齐方式

VStack 提供了三种主要的对齐方式:

  • .leading:左对齐

  • .center:居中对齐(默认)

  • .trailing:右对齐

  • .top.bottom:在嵌套布局中使用


// 不同对齐方式

VStack(alignment: .leading) {

    Text("左对齐")

    Text("这是一行更长的文本")

    Text("短文本")

}

.padding()

  


VStack(alignment: .center) {

    Text("居中对齐")

    Text("这是一行更长的文本")

    Text("短文本")

}

.padding()

  


VStack(alignment: .trailing) {

    Text("右对齐")

    Text("这是一行更长的文本")

    Text("短文本")

}

.padding()

嵌套 VStack

VStack 可以嵌套使用,创建更复杂的布局结构。


// 嵌套垂直栈

VStack(spacing: 10) {

    Text("标题")

        .font(.headline)

    

    VStack(alignment: .leading, spacing: 8) {

        Text("项目 1")

        Text("项目 2")

        Text("项目 3")

    }

    .padding()

    .background(Color.gray.opacity(0.1))

    .cornerRadius(8)

    

    Button("确认") {}

}

.padding()

适用场景

  • 表单布局:从上到下排列的输入字段

  • 列表项:垂直排列的内容块

  • 页面结构:标题、内容、按钮的垂直布局

  • 卡片式布局:垂直堆叠的信息卡片

性能考虑

  • VStack 会根据子视图的大小自动调整高度

  • 对于大量子视图,考虑使用 LazyVStack 来提高性能

  • 避免过深的嵌套,可能会影响渲染性能

4.2 水平布局:HStack

HStack 介绍

HStack 是 SwiftUI 中用于水平堆叠视图的容器,它会将子视图按水平方向排列。HStack 是构建水平布局的基础组件,适用于需要从左到右排列的界面元素。

基本用法


// 基本水平栈

HStack {

    Text("左侧")

    Text("中间")

    Text("右侧")

}

  


// 带间距和对齐的水平栈

HStack(alignment: .top, spacing: 20) {

    Text("顶部对齐")

    Text("这是一行\n多行文本")

    Text("短文本")

}

.padding()

对齐方式

HStack 提供了三种主要的对齐方式:

  • .top:顶部对齐

  • .center:居中对齐(默认)

  • .bottom:底部对齐

  • .leading.trailing:在嵌套布局中使用


// 不同对齐方式

HStack(alignment: .top) {

    Text("顶部对齐")

    Text("这是一行\n多行文本")

    Text("短文本")

}

.padding()

  


HStack(alignment: .center) {

    Text("居中对齐")

    Text("这是一行\n多行文本")

    Text("短文本")

}

.padding()

  


HStack(alignment: .bottom) {

    Text("底部对齐")

    Text("这是一行\n多行文本")

    Text("短文本")

}

.padding()

空间分配

HStack 可以使用 Spacer 来分配空间,实现更灵活的布局。


// 空间分配

HStack {

    Text("左侧")

    Spacer()  // 占据剩余空间

    Text("右侧")

}

.padding()

  


// 带比例的空间分配

HStack {

    Text("1/4 宽度")

        .frame(maxWidth: .infinity)

    Spacer()

    Text("1/4 宽度")

        .frame(maxWidth: .infinity)

    Spacer()

    Text("1/4 宽度")

        .frame(maxWidth: .infinity)

    Spacer()

    Text("1/4 宽度")

        .frame(maxWidth: .infinity)

}

.padding()

适用场景

  • 工具栏:水平排列的操作按钮

  • 列表项内容:左侧图标、中间文本、右侧箭头

  • 表单行:标签和输入框的水平排列

  • 导航栏:左侧返回按钮、中间标题、右侧操作按钮

性能考虑

  • HStack 会根据子视图的大小自动调整宽度

  • 对于大量子视图,考虑使用 LazyHStack 来提高性能

  • 注意水平空间不足时的布局行为,可能需要使用 ScrollView

4.3 层叠布局:ZStack

ZStack 介绍

ZStack 是 SwiftUI 中用于层叠视图的容器,它会将子视图按层叠方式排列,后面的视图会覆盖前面的视图。ZStack 是构建叠加效果的基础组件,适用于需要层级关系的界面元素。

基本用法


// 基本层叠

ZStack {

    Color.blue  // 背景

    Text("前景文本")

        .foregroundStyle(.white)

        .font(.largeTitle)

}

.frame(height: 200)

  


// 多层叠

ZStack {

    // 底层

    Rectangle()

        .fill(Color.yellow)

        .frame(width: 200, height: 200)

    

    // 中层

    Circle()

        .fill(Color.green)

        .frame(width: 150, height: 150)

    

    // 顶层

    Text("ZStack")

        .font(.headline)

}

对齐方式

ZStack 提供了多种对齐方式,可以精确控制子视图的位置:

  • .topLeading.top.topTrailing

  • .leading.center.trailing

  • .bottomLeading.bottom.bottomTrailing


// 不同对齐方式

ZStack(alignment: .topLeading) {

    Rectangle()

        .fill(Color.gray.opacity(0.2))

        .frame(width: 300, height: 200)

    

    Text("左上角")

        .padding(10)

}

  


ZStack(alignment: .center) {

    Rectangle()

        .fill(Color.gray.opacity(0.2))

        .frame(width: 300, height: 200)

    

    Text("居中")

}

  


ZStack(alignment: .bottomTrailing) {

    Rectangle()

        .fill(Color.gray.opacity(0.2))

        .frame(width: 300, height: 200)

    

    Text("右下角")

        .padding(10)

}

实际应用


// 带徽章的图标

ZStack(alignment: .topTrailing) {

    Image(systemName: "bell")

        .font(.system(size: 24))

    

    Circle()

        .fill(Color.red)

        .frame(width: 16, height: 16)

        .overlay {

            Text("3")

                .font(.system(size: 10))

                .foregroundStyle(.white)

        }

        .offset(x: 4, y: -4)

}

  


// 卡片覆盖效果

ZStack {

    RoundedRectangle(cornerRadius: 12)

        .fill(Color.white)

        .shadow(radius: 4)

        .frame(width: 300, height: 200)

    

    VStack {

        Text("卡片标题")

            .font(.headline)

        Text("卡片内容")

            .foregroundStyle(.secondary)

    }

    .padding()

    

    // 右上角标签

    Text("NEW")

        .font(.caption)

        .foregroundStyle(.white)

        .padding(4)

        .background(Color.blue)

        .cornerRadius(4)

        .offset(x: 45, y: -10)

}

适用场景

  • 带背景的文本:文本叠加在背景之上

  • 徽章效果:通知图标上的数字徽章

  • 卡片布局:带有覆盖元素的信息卡片

  • 复杂 UI 组件:需要多层叠加的自定义控件

  • 模态视图:半透明覆盖层

性能考虑

  • ZStack 会按照添加顺序渲染视图,后面的视图会覆盖前面的

  • 对于复杂的叠加效果,注意渲染性能

  • 考虑使用 offset 修饰符来微调子视图位置

4.4 间距与对齐

间距设置

间距是布局中的重要因素,它决定了视图之间的关系和视觉舒适度。


// VStack 间距

VStack(spacing: 16) {

    Text("项目 1")

    Text("项目 2")

    Text("项目 3")

}

  


// HStack 间距

HStack(spacing: 20) {

    Text("左")

    Text("中")

    Text("右")

}

  


// 嵌套栈的间距

VStack(spacing: 20) {

    Text("标题")

    

    HStack(spacing: 10) {

        Button("按钮 1") {}

        Button("按钮 2") {}

    }

    

    Text("底部文本")

}

对齐设置

对齐决定了视图在容器中的位置,影响整体布局的一致性。


// 垂直对齐

VStack(alignment: .leading) {

    Text("左对齐")

    Text("这是一行更长的文本")

}

  


// 水平对齐

HStack(alignment: .center) {

    Text("顶部")

        .font(.largeTitle)

    Text("底部")

        .font(.footnote)

}

  


// 层叠对齐

ZStack(alignment: .bottom) {

    Image(systemName: "photo")

        .resizable()

        .aspectRatio(contentMode: .fit)

        .frame(height: 200)

    

    Text("图片标题")

        .padding()

        .background(Color.black.opacity(0.5))

        .foregroundStyle(.white)

        .frame(maxWidth: .infinity, alignment: .center)

}

内边距与外边距

内边距(padding)和外边距是控制视图与其他元素之间空间的重要工具。


// 内边距

VStack {

    Text("带内边距的文本")

        .padding()  // 四周内边距

    

    Text("自定义内边距")

        .padding(.horizontal, 20// 水平内边距

        .padding(.vertical, 10)    // 垂直内边距

}

  


// 外边距

VStack {

    Text("带外边距的文本")

}

.padding()  // 给整个 VStack 添加内边距

  


// 组合使用

Text("文本")

    .padding(10// 内边距

    .background(Color.yellow)

    .padding(10// 外边距(看起来像内边距)

    .background(Color.blue)

适用场景

  • 表单设计:通过间距和对齐创建整齐的表单

  • 卡片布局:使用内边距和外边距创建视觉层次感

  • 响应式设计:根据不同屏幕尺寸调整间距

  • 可访问性:适当的间距提高内容的可读性

最佳实践

  • 保持一致的间距系统:使用统一的间距值(如 8、16、24 等)

  • 考虑内容的重要性:重要内容之间应有更大的间距

  • 响应式调整:在不同屏幕尺寸上调整间距

  • 测试不同设备:确保在各种设备上布局都美观

4.5 垫片:Spacer

Spacer 介绍

Spacer 是 SwiftUI 中用于占据剩余空间的视图,它会自动扩展以填充可用空间。Spacer 是实现灵活布局的重要工具,特别适用于需要将元素推到容器边缘的场景。

基本用法


// 水平布局中的 Spacer

HStack {

    Text("左侧")

    Spacer()  // 占据中间的所有空间

    Text("右侧")

}

.padding()

  


// 垂直布局中的 Spacer

VStack {

    Text("顶部")

    Spacer()  // 占据中间的所有空间

    Text("底部")

}

.frame(height: 200)

.padding()

灵活使用


// 顶部对齐

VStack {

    Text("标题")

    Spacer()

}

.frame(height: 200)

.padding()

  


// 底部对齐

VStack {

    Spacer()

    Text("底部文本")

}

.frame(height: 200)

.padding()

  


// 两端对齐

HStack {

    Text("左侧")

    Spacer()

    Text("中间")

    Spacer()

    Text("右侧")

}

.padding()

实际应用


// 工具栏布局

HStack {

    Button("返回") {

        print("返回")

    }

    

    Spacer()

    

    Text("页面标题")

    

    Spacer()

    

    Button("更多") {

        print("更多")

    }

}

.padding()

.background(Color(.systemBackground))

  


// 表单底部按钮

VStack {

    // 表单内容

    ForEach(0..<3) {

        Text("表单项 \($0 + 1)")

            .padding()

            .background(Color.gray.opacity(0.1))

            .cornerRadius(8)

            .padding(.horizontal)

    }

    

    Spacer()

    

    // 底部按钮

    Button("提交") {

        print("提交")

    }

    .buttonStyle(.borderedProminent)

    .padding()

}

适用场景

  • 工具栏:将标题居中,按钮放在两侧

  • 表单:将提交按钮固定在底部

  • 卡片:将内容推到顶部,操作按钮放在底部

  • 导航栏:创建平衡的布局

性能考虑

  • Spacer 是轻量级视图,对性能影响很小

  • 合理使用 Spacer 可以减少不必要的几何计算

  • 避免在不需要的地方使用 Spacer,可能会导致意外的布局行为

4.6 布局修饰符

框架修饰符

frame 修饰符用于控制视图的大小和对齐方式。


// 设置固定大小

Text("固定大小")

    .frame(width: 200, height: 100)

    .background(Color.yellow)

  


// 设置最大和最小大小

Text("灵活大小")

    .frame(minWidth: 100, maxWidth: 300, minHeight: 50, maxHeight: 150)

    .background(Color.blue)

  


// 填充父容器

Text("填充")

    .frame(maxWidth: .infinity, maxHeight: .infinity)

    .background(Color.green)

  


// 带对齐的框架

Text("右对齐")

    .frame(width: 200, alignment: .trailing)

    .background(Color.gray.opacity(0.2))

位置修饰符

positionoffset 修饰符用于调整视图的位置。


// 绝对位置

Text("绝对位置")

    .position(x: 100, y: 100)

  


// 相对偏移

Text("相对偏移")

    .offset(x: 50, y: 20)

  


// 组合使用

ZStack {

    Text("基础位置")

        .background(Color.yellow)

    

    Text("偏移位置")

        .offset(x: 50, y: 30)

        .background(Color.red)

}

布局优先级

layoutPriority 修饰符用于设置视图的布局优先级。


HStack {

    Text("短文本")

        .layoutPriority(1// 高优先级

        .background(Color.yellow)

    

    Text("这是一段非常长的文本,会被截断")

        .background(Color.blue)

}

.frame(width: 200)

适用场景

  • 响应式设计:根据屏幕尺寸调整视图大小

  • 自定义布局:精确控制视图位置

  • 复杂界面:处理不同优先级的内容

  • 动态布局:根据内容自动调整

4.7 网格布局:LazyVGrid 和 LazyHGrid

网格布局介绍

LazyVGridLazyHGrid 是 SwiftUI 中用于创建网格布局的容器,它们支持延迟加载,适用于大量数据。网格布局使用 GridItem 来定义列或行的大小和间距。

基本用法


// 定义网格列

let columns = [

    GridItem(.flexible()),

    GridItem(.flexible()),

    GridItem(.flexible())

]

  


// 垂直网格

ScrollView {

    LazyVGrid(columns: columns, spacing: 10) {

        ForEach(1..<10) { index in

            Rectangle()

                .fill(Color(hue: Double(index)/10, saturation: 0.8, brightness: 0.8))

                .frame(height: 80)

                .overlay(

                    Text("\(index)")

                        .foregroundColor(.white)

                        .font(.headline)

                )

        }

    }

    .padding()

}

  


// 水平网格

let rows = [

    GridItem(.flexible()),

    GridItem(.flexible())

]

  


ScrollView(.horizontal) {

    LazyHGrid(rows: rows, spacing: 10) {

        ForEach(1..<10) { index in

            Rectangle()

                .fill(Color(hue: Double(index)/10, saturation: 0.8, brightness: 0.8))

                .frame(width: 100)

                .overlay(

                    Text("\(index)")

                        .foregroundColor(.white)

                        .font(.headline)

                )

        }

    }

    .padding()

}

GridItem 配置

GridItem 支持多种大小配置:

  • .fixed:固定大小

  • .flexible:灵活大小(默认)

  • .adaptive:自适应大小,尽可能多的列


// 固定大小的列

let fixedColumns = [

    GridItem(.fixed(100)),

    GridItem(.fixed(100)),

    GridItem(.fixed(100))

]

  


// 自适应列数

let adaptiveColumns = [

    GridItem(.adaptive(minimum: 80, maximum: 120))

]

  


// 混合配置

let mixedColumns = [

    GridItem(.fixed(80)),

    GridItem(.flexible()),

    GridItem(.fixed(80))

]

适用场景

  • 图片网格:相册、产品展示

  • 图标网格:应用图标、功能入口

  • 数据网格:表格数据展示

  • 响应式布局:自适应不同屏幕尺寸

4.8 列表和表单:List 和 Form

List 列表

List 是 SwiftUI 中用于显示滚动列表的容器,自动处理单元格布局和数据展示。


// 基本列表

List {

    Text("项目 1")

    Text("项目 2")

    Text("项目 3")

}

  


// 带分组的列表

List {

    Section(header: Text("分组 1")) {

        Text("项目 1")

        Text("项目 2")

    }

    

    Section(header: Text("分组 2")) {

        Text("项目 3")

        Text("项目 4")

    }

}

  


// 动态列表

let items = ["苹果", "香蕉", "橙子", "葡萄"]

  


List(items, id: \.self) { item in

    HStack {

        Image(systemName: "fruit")

        Text(item)

    }

}

Form 表单

Form 是专门用于表单布局的容器,提供了预设的样式和间距。


// 基本表单

Form {

    TextField("用户名", text: .constant(""))

    SecureField("密码", text: .constant(""))

    Toggle("记住密码", isOn: .constant(true))

    Button("登录") {}

}

  


// 带分组的表单

Form {

    Section(header: Text("个人信息")) {

        TextField("姓名", text: .constant(""))

        TextField("邮箱", text: .constant(""))

    }

    

    Section(header: Text("偏好设置")) {

        Toggle("接收通知", isOn: .constant(true))

        Picker("主题", selection: .constant("浅色")) {

            Text("浅色").tag("浅色")

            Text("深色").tag("深色")

        }

    }

    

    Section {

        Button("保存设置") {}

    }

}

适用场景

  • List:显示结构化数据列表、设置项、联系人

  • Form:创建用户输入表单、设置页面、注册登录页面

4.9 几何读取器:GeometryReader

GeometryReader 介绍

GeometryReader 是一个特殊的容器,它可以读取父视图的几何信息(尺寸和位置),并根据这些信息来布局子视图。


// 基本用法

GeometryReader { geometry in

    VStack {

        Text("宽度: \(geometry.size.width)")

        Text("高度: \(geometry.size.height)")

        Text("安全区域: \(geometry.safeAreaInsets.top)")

    }

    .frame(width: geometry.size.width, height: geometry.size.height)

    .background(Color.gray.opacity(0.1))

}

  


// 根据父视图大小调整子视图

GeometryReader { geometry in

    HStack(spacing: 0) {

        Color.red

            .frame(width: geometry.size.width * 0.3)

        Color.green

            .frame(width: geometry.size.width * 0.3)

        Color.blue

            .frame(width: geometry.size.width * 0.4)

    }

}

.frame(height: 100)

  


// 自适应网格

GeometryReader { geometry in

    let columns = Int(geometry.size.width / 100)

    let gridItems = Array(repeating: GridItem(.flexible()), count: max(columns, 1))

    

    LazyVGrid(columns: gridItems, spacing: 10) {

        ForEach(1..<10) { index in

            Rectangle()

                .fill(Color(hue: Double(index)/10, saturation: 0.8, brightness: 0.8))

                .frame(height: 80)

                .overlay(

                    Text("\(index)")

                        .foregroundColor(.white)

                )

        }

    }

    .padding()

}

适用场景

  • 响应式布局:根据屏幕尺寸调整布局

  • 自定义布局:需要精确控制尺寸的场景

  • 动画效果:基于几何信息创建动画

  • 复杂布局:需要基于父视图尺寸的布局

4.10 其他重要布局组件

Divider 分隔线

Divider 用于在视图之间创建水平或垂直的分隔线。


// 水平分隔线

VStack {

    Text("顶部")

    Divider()

    Text("底部")

}

  


// 垂直分隔线

HStack {

    Text("左侧")

    Divider()

    Text("右侧")

}

.frame(height: 50)

Group 视图分组

Group 用于将多个视图组合在一起,作为一个整体应用修饰符。


// 分组应用修饰符

Group {

    Text("项目 1")

    Text("项目 2")

    Text("项目 3")

}

.foregroundColor(.blue)

.font(.headline)

  


// 条件渲染

Group {

    if true {

        Text("显示这个")

    } else {

        Text("显示那个")

    }

}

自定义布局(iOS 16+)

iOS 16 引入了 Layout 协议,允许创建完全自定义的布局。


// 简单的自定义布局

struct SimpleLayout: Layout {

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {

        // 计算布局尺寸

        return CGSize(width: proposal.width ?? 300, height: proposal.height ?? 200)

    }

    

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {

        // 放置子视图

        for (index, subview) in subviews.enumerated() {

            let x = bounds.minX + CGFloat(index) * 50

            let y = bounds.midY

            subview.place(at: CGPoint(x: x, y: y), proposal: .unspecified)

        }

    }

}

  


// 使用自定义布局

SimpleLayout {

    Text("1")

    Text("2")

    Text("3")

    Text("4")

}

.frame(height: 100)

.background(Color.gray.opacity(0.1))

实战:创建一个登录页面

需求分析

创建一个包含以下元素的登录页面:

  1. 应用图标和标题

  2. 用户名输入框

  3. 密码输入框(带可见性切换)

  4. 登录按钮

  5. 忘记密码链接

  6. 注册链接

代码实现


import SwiftUI

  


struct LoginView: View {

    // 状态变量

    @State private var username = ""

    @State private var password = ""

    @State private var showPassword = false

    

    var body: some View {

        ZStack {

            // 背景

            LinearGradient(

                colors: [.blue.opacity(0.1), .purple.opacity(0.1)],

                startPoint: .top,

                endPoint: .bottom

            )

            .ignoresSafeArea()

            

            VStack(spacing: 24) {

                // 应用图标和标题

                VStack(spacing: 12) {

                    Image(systemName: "lock.fill")

                        .resizable()

                        .aspectRatio(contentMode: .fit)

                        .frame(width: 80, height: 80)

                        .foregroundStyle(.blue)

                    

                    Text("欢迎回来")

                        .font(.largeTitle)

                        .fontWeight(.bold)

                    

                    Text("请登录以继续")

                        .foregroundStyle(.secondary)

                }

                

                // 输入区域

                VStack(spacing: 16) {

                    // 用户名输入框

                    TextField(

                        "用户名",

                        text: $username,

                        prompt: Text("请输入用户名")

                    )

                    .textFieldStyle(.roundedBorder)

                    .padding(.horizontal)

                    

                    // 密码输入框

                    ZStack(alignment: .trailing) {

                        if showPassword {

                            TextField(

                                "密码",

                                text: $password,

                                prompt: Text("请输入密码")

                            )

                        } else {

                            SecureField(

                                "密码",

                                text: $password,

                                prompt: Text("请输入密码")

                            )

                        }

                        

                        Button(action: {

                            showPassword.toggle()

                        }) {

                            Image(systemName: showPassword ? "eye.slash.fill" : "eye.fill")

                                .foregroundStyle(.secondary)

                                .padding(.trailing, 16)

                        }

                    }

                    .textFieldStyle(.roundedBorder)

                    .padding(.horizontal)

                    

                    // 忘记密码

                    HStack {

                        Spacer()

                        Button("忘记密码?") {

                            print("忘记密码")

                        }

                        .foregroundStyle(.blue)

                        .padding(.trailing)

                    }

                }

                

                // 登录按钮

                Button("登录") {

                    print("登录")

                }

                .buttonStyle(.borderedProminent)

                .tint(.blue)

                .padding(.horizontal)

                .frame(maxWidth: .infinity)

                

                // 注册链接

                HStack {

                    Text("还没有账号?")

                    Button("立即注册") {

                        print("注册")

                    }

                    .foregroundStyle(.blue)

                }

                

                Spacer()

            }

            .padding(.top, 60)

        }

    }

}

  


#Preview {

    LoginView()

}

代码解析

  1. ZStack:用于层叠背景和内容,创建深度感

  2. VStack:用于垂直排列各个部分,保持页面结构清晰

  3. HStack:用于水平排列忘记密码链接和注册链接

  4. Spacer:用于底部填充空间,将内容推到顶部

  5. TextField 和 SecureField:用于用户输入

  6. Button:用于操作按钮

  7. LinearGradient:用于创建美观的背景渐变

  8. ** @State**:用于管理视图状态

实战:创建一个产品详情页

需求分析

创建一个产品详情页,包含以下元素:

  1. 产品图片

  2. 产品标题和价格

  3. 产品描述

  4. 规格选择

  5. 购买按钮

代码实现


import SwiftUI

  


struct ProductDetailView: View {

    // 状态变量

    @State private var selectedColor = "红色"

    @State private var selectedSize = "M"

    @State private var quantity = 1

    

    // 产品数据

    let productName = "SwiftUI 高级教程"

    let productPrice = "¥99.00"

    let productDescription = "本教程涵盖了 SwiftUI 的高级特性,包括动画、手势、布局和性能优化等内容。通过实际项目案例,帮助你掌握 SwiftUI 的核心概念和最佳实践。"

    let colors = ["红色", "蓝色", "黑色"]

    let sizes = ["S", "M", "L", "XL"]

    

    var body: some View {

        ScrollView {

            VStack(spacing: 20) {

                // 产品图片

                ZStack {

                    Color.gray.opacity(0.1)

                        .frame(height: 300)

                    

                    Image(systemName: "book.fill")

                        .resizable()

                        .aspectRatio(contentMode: .fit)

                        .frame(width: 150, height: 150)

                        .foregroundStyle(.blue)

                }

                

                // 产品信息

                VStack(alignment: .leading, spacing: 12) {

                    HStack {

                        Text(productName)

                            .font(.title)

                            .fontWeight(.bold)

                        Spacer()

                        Text(productPrice)

                            .font(.title)

                            .fontWeight(.bold)

                            .foregroundStyle(.red)

                    }

                    

                    // 产品描述

                    Text(productDescription)

                        .foregroundStyle(.secondary)

                        .lineLimit(nil)

                    

                    // 颜色选择

                    Text("颜色")

                        .font(.headline)

                    HStack(spacing: 10) {

                        ForEach(colors, id: \.self) {

                            color in

                            Button(action: {

                                selectedColor = color

                            }) {

                                Text(color)

                                    .padding(8)

                                    .background(selectedColor == color ? Color.blue : Color.gray.opacity(0.1))

                                    .foregroundStyle(selectedColor == color ? .white : .primary)

                                    .cornerRadius(4)

                            }

                        }

                    }

                    

                    // 尺寸选择

                    Text("尺寸")

                        .font(.headline)

                    HStack(spacing: 10) {

                        ForEach(sizes, id: \.self) {

                            size in

                            Button(action: {

                                selectedSize = size

                            }) {

                                Text(size)

                                    .padding(8)

                                    .background(selectedSize == size ? Color.blue : Color.gray.opacity(0.1))

                                    .foregroundStyle(selectedSize == size ? .white : .primary)

                                    .cornerRadius(4)

                            }

                        }

                    }

                    

                    // 数量选择

                    Text("数量")

                        .font(.headline)

                    HStack {

                        Button(action: {

                            if quantity > 1 {

                                quantity -= 1

                            }

                        }) {

                            Image(systemName: "minus.circle")

                                .font(.system(size: 24))

                        }

                        

                        Text("\(quantity)")

                            .font(.headline)

                            .padding(.horizontal, 20)

                        

                        Button(action: {

                            quantity += 1

                        }) {

                            Image(systemName: "plus.circle")

                                .font(.system(size: 24))

                        }

                    }

                }

                .padding()

                

                // 购买按钮

                Button("加入购物车") {

                    print("加入购物车")

                }

                .buttonStyle(.borderedProminent)

                .tint(.blue)

                .padding(.horizontal)

                .frame(maxWidth: .infinity)

                .padding(.bottom, 30)

            }

        }

        .navigationTitle("产品详情")

        .navigationBarTitleDisplayMode(.inline)

    }

}

  


#Preview {

    ProductDetailView()

}

代码解析

  1. ScrollView:用于滚动显示产品详情

  2. ZStack:用于显示产品图片和背景

  3. VStack:用于垂直排列产品信息

  4. HStack:用于水平排列价格、颜色选择、尺寸选择和数量控制

  5. Button:用于选择颜色、尺寸和调整数量

  6. ** @State**:用于管理用户选择的状态

小结

本章详细介绍了 SwiftUI 中的基础布局系统,包括:

  • VStack:垂直堆叠视图,适用于从上到下的布局

  • HStack:水平堆叠视图,适用于从左到右的布局

  • ZStack:层叠视图,适用于需要层级关系的布局

  • 间距与对齐:控制视图之间的空间和位置关系

  • Spacer:占据剩余空间,实现灵活布局

  • 布局修饰符:控制视图的大小、位置和优先级

  • 网格布局:LazyVGrid、LazyHGrid,用于创建网格

  • 列表和表单:List、Form,用于显示列表和表单

  • 几何读取器:GeometryReader,用于获取父视图尺寸

  • 其他布局组件:Divider、Group、自定义布局

  • 实战案例:登录页面和产品详情页的完整实现

布局最佳实践

  1. 保持简洁:使用最少的容器实现所需布局

  2. 嵌套合理:避免过深的布局嵌套

  3. 响应式设计:考虑不同屏幕尺寸的布局适配

  4. 性能优化:对于大量数据使用 Lazy 容器

  5. 一致性:保持间距和对齐的一致性

  6. 可访问性:确保布局对所有用户都友好

通过本章的学习,你已经掌握了 SwiftUI 中最基本的布局技巧,能够创建各种常见的布局结构。在实际开发中,你可以根据具体需求选择合适的布局容器和技术,创建美观、响应式的用户界面。

参考资料

本内容为《SwiftUI 基础教程》第四章,欢迎关注后续更新。

第3章:基础视图组件

作者 90后晨仔
2026年4月16日 20:46

Snip20260416_1.png

示例代码都放这里啦,有需要的可以下载学习。swiftUIDemo

3.1 文本显示:Text

Text 组件介绍

Text 是 SwiftUI 中最基本的视图组件,用于显示文本内容。它支持富文本、字体样式、颜色等多种属性。

基本用法

// 基本文本
Text("Hello, SwiftUI!")

// 带样式的文本
Text("Hello, SwiftUI!")
    .font(.largeTitle)         // 设置字体大小
    .fontWeight(.bold)         // 设置字重
    .foregroundStyle(.blue)    // 设置文本颜色
    .italic()                  // 斜体
    .underline()               // 下划线
    .strikethrough()           // 删除线

富文本

// 富文本
Text("Hello, \(Text("SwiftUI").foregroundStyle(.blue).bold())!")

// 多行文本
Text("这是一段多行文本,\n可以通过反斜杠 n 来换行,\n或者直接在字符串中换行。")
    .multilineTextAlignment(.center)  // 多行文本对齐方式
    .lineLimit(3)                     // 限制行数
    .truncationMode(.tail)            // 截断方式

本地化

// 本地化文本
Text("welcome_message")  // 从 Localizable.strings 文件中读取

// 带参数的本地化
Text("greeting", comment: "欢迎语")

// 格式化文本
let name = "张三"
Text("欢迎 %@", name)

日期和数字格式化

// 日期格式化
let date = Date()
Text(date, style: .date)           // 仅日期
Text(date, style: .time)           // 仅时间
Text(date, style: .relative)       // 相对时间
Text(date, style: .offset)         // 时间偏移
Text(date, style: .timer)          // 计时器

// 数字格式化
let number = 123456.789
Text(number, format: .number)
Text(number, format: .currency(code: "CNY"))
Text(number, format: .percent)

3.2 图片显示:Image

Image 组件介绍

Image 用于显示图片,可以从系统图标、资源文件或网络加载图片。

基本用法

// 系统图标
Image(systemName: "star.fill")

// 资源文件图片
Image("avatar")

// 网络图片 (iOS 15+)
AsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in
    switch phase {
    case .empty:
        ProgressView()  // 加载中
    case .success(let image):
        image
            .resizable()
            .aspectRatio(contentMode: .fill)
    case .failure:
        Image(systemName: "photo")  // 加载失败
    @unknown default:
        EmptyView()
    }
}

图片修饰符

Image("avatar")
    .resizable()                 // 可调整大小
    .aspectRatio(contentMode: .fit)  // 内容模式
    .frame(width: 100, height: 100)  // 设置大小
    .clipShape(Circle())         // 裁剪形状
    .overlay(                    // 叠加内容
        Circle()
            .stroke(Color.blue, lineWidth: 2)
    )
    .shadow(radius: 5)           // 阴影
    .opacity(0.8)                // 透明度

系统图标

// 系统图标
Image(systemName: "heart.fill")
    .foregroundStyle(.red)
    .font(.system(size: 24))

// 多色图标 (iOS 15+)
Image(systemName: "person.fill.badge.plus")
    .symbolRenderingMode(.multicolor)

// 可变颜色图标
Image(systemName: "star")
    .foregroundStyle(.yellow)

3.3 按钮交互:Button

Button 组件介绍

Button 用于创建可点击的按钮,支持多种样式和交互方式。

基本用法

// 基本按钮
Button("点击我") {
    print("按钮被点击了")
}

// 带图标的按钮
Button {
    print("按钮被点击了")
} label: {
    HStack {
        Image(systemName: "star.fill")
        Text("喜欢")
    }
}

// 带角色的按钮
Button("删除", role: .destructive) {
    print("删除操作")
}

按钮样式

// 边框按钮
Button("边框按钮") {
    // 操作
}
.buttonStyle(.bordered)

// 突出显示的按钮
Button("突出按钮") {
    // 操作
}
.buttonStyle(.borderedProminent)
.tint(.blue)  // 按钮颜色

// 胶囊按钮
Button("胶囊按钮") {
    // 操作
}
.buttonStyle(.borderedProminent)
.tint(.green)
.cornerRadius(20)

// 文本按钮
Button("文本按钮") {
    // 操作
}
.buttonStyle(.plain)

禁用状态

@State private var isEnabled = false

Button("禁用按钮") {
    // 操作
}
.disabled(!isEnabled)
.opacity(isEnabled ? 1.0 : 0.5)

3.4 输入控件:TextField、SecureField、TextEditor

TextField 文本输入框

@State private var text = ""

TextField("请输入文本", text: $text)
    .textFieldStyle(.roundedBorder)  // 边框样式
    .padding()                      // 内边距
    .keyboardType(.default)         // 键盘类型
    .autocapitalization(.sentences) // 自动大写
    .autocorrectionDisabled(true)   // 禁用自动纠正

// 带提示的 TextField
TextField(
    "请输入用户名",
    text: $text,
    prompt: Text("用户名不能为空")
        .foregroundStyle(.secondary)
)
.textFieldStyle(.roundedBorder)

SecureField 安全输入框

@State private var password = ""

SecureField("请输入密码", text: $password)
    .textFieldStyle(.roundedBorder)
    .padding()

// 带可见性切换的密码输入
@State private var showPassword = false

ZStack(alignment: .trailing) {
    if showPassword {
        TextField("请输入密码", text: $password)
    } else {
        SecureField("请输入密码", text: $password)
    }
    Button(action: {
        showPassword.toggle()
    }) {
        Image(systemName: showPassword ? "eye.slash.fill" : "eye.fill")
            .foregroundStyle(.secondary)
            .padding(.trailing, 8)
    }
}
.textFieldStyle(.roundedBorder)
.padding()

TextEditor 多行文本编辑器

@State private var message = ""

TextEditor(text: $message)
    .frame(height: 150)           // 设置高度
    .border(Color.gray.opacity(0.3), width: 1)  // 边框
    .cornerRadius(8)              // 圆角
    .padding()
    .foregroundStyle(.primary)    // 文本颜色

// 带占位符的 TextEditor
ZStack(alignment: .topLeading) {
    TextEditor(text: $message)
        .frame(height: 150)
        .padding(8)
    
    if message.isEmpty {
        Text("请输入消息...")
            .foregroundStyle(.secondary)
            .padding(10)
            .allowsHitTesting(false)  // 允许点击穿透
    }
}
.border(Color.gray.opacity(0.3), width: 1)
.cornerRadius(8)
.padding()

3.5 开关与选择:Toggle、Picker、Slider、Stepper

Toggle 开关

@State private var isEnabled = false

Toggle("启用功能", isOn: $isEnabled)
    .toggleStyle(.switch)  // 开关样式
    .padding()

// 带图标的 Toggle
Toggle(isOn: $isEnabled) {
    HStack {
        Image(systemName: "bell.fill")
        Text("接收通知")
    }
}
.toggleStyle(.switch)
.padding()

Picker 选择器

@State private var selectedOption = "选项1"
let options = ["选项1", "选项2", "选项3"]

// 分段控件样式
Picker("选择", selection: $selectedOption) {
    ForEach(options, id: \.self) {
        Text($0)
    }
}
.pickerStyle(.segmented)
.padding()

// 菜单样式
Picker("选择", selection: $selectedOption) {
    ForEach(options, id: \.self) {
        Text($0)
    }
}
.pickerStyle(.menu)
.padding()

// 轮盘样式(iOS 14+)
@State private var selectedColor = Color.red
let colors: [Color] = [.red, .green, .blue, .yellow]

Picker("颜色", selection: $selectedColor) {
    ForEach(colors, id: \.self) {
        ColorPickerView(color: $0)
    }
}
.pickerStyle(.wheel)
.frame(height: 200)
.padding()

// 辅助视图
struct ColorPickerView: View {
    let color: Color
    var body: some View {
        HStack {
            Rectangle()
                .fill(color)
                .frame(width: 20, height: 20)
                .cornerRadius(4)
            Text(String(describing: color))
        }
    }
}

Slider 滑块

@State private var value = 0.5

Slider(value: $value, in: 0...1)
    .padding()
    .tint(.blue)  // 滑块颜色

// 带标签的滑块
Slider(
    value: $value,
    in: 0...1,
    label: { Text("亮度") },
    minimumValueLabel: { Text("暗") },
    maximumValueLabel: { Text("亮") }
)
.padding()

// 整数滑块
@State private var intValue = 5

Slider(value: Binding(
    get: { Double(intValue) },
    set: { intValue = Int($0) }
), in: 0...10, step: 1)
.padding()
Text("值:\(intValue)")

Stepper 步进器

@State private var count = 0

Stepper("数量:\(count)", value: $count)
    .padding()

// 带范围的步进器
Stepper(
    "数量:\(count)",
    value: $count,
    in: 0...10,
    step: 2
)
.padding()

// 带标签的步进器
Stepper {
    Text("数量:\(count)")
} onIncrement: {
    count += 1
    print("增加到:\(count)")
} onDecrement: {
    count -= 1
    print("减少到:\(count)")
}
.padding()

3.6 进度指示:ProgressView

不确定进度

// 基本进度指示器
ProgressView()

// 带标签的进度指示器
ProgressView("加载中...")

// 带样式的进度指示器
ProgressView("处理中...")
    .progressViewStyle(.circular)
    .tint(.blue)
    .padding()

确定进度

@State private var progress = 0.0

ProgressView("下载进度", value: progress, total: 1.0)
    .padding()

// 带百分比的进度条
ProgressView(
    value: progress,
    total: 1.0,
    label: { Text("下载进度") },
    currentValueLabel: { Text("\(Int(progress * 100))%") }
)
.padding()

// 水平进度条样式
ProgressView(value: progress, total: 1.0)
    .progressViewStyle(.linear)
    .tint(.green)
    .frame(height: 10)
    .padding()

实战:创建一个用户设置页面

需求分析

创建一个包含以下元素的用户设置页面:

  1. 个人信息区域
  2. 通知设置(开关)
  3. 主题选择(选择器)
  4. 字体大小(滑块)
  5. 清除缓存按钮
  6. 退出登录按钮

代码实现

import SwiftUI

struct SettingsView: View {
    // 状态变量
    @State private var notificationsEnabled = true
    @State private var selectedTheme = "浅色"
    @State private var fontSize = 16.0
    @State private var cacheSize = "128 MB"
    
    // 主题选项
    let themes = ["浅色", "深色", "跟随系统"]
    
    var body: some View {
        NavigationStack {
            List {
                // 个人信息区域
                Section {
                    HStack {
                        Image(systemName: "person.circle.fill")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: 60, height: 60)
                            .foregroundStyle(.blue)
                        
                        VStack(alignment: .leading, spacing: 4) {
                            Text("张三")
                                .font(.headline)
                            Text("zhangsan@example.com")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                        }
                        Spacer()
                        Image(systemName: "chevron.right")
                            .foregroundStyle(.secondary)
                    }
                    .padding(.vertical, 8)
                }
                
                // 通知设置
                Section("通知设置") {
                    Toggle("接收推送通知", isOn: $notificationsEnabled)
                    Toggle("声音提醒", isOn: $notificationsEnabled)
                    Toggle("振动提醒", isOn: $notificationsEnabled)
                }
                
                // 外观设置
                Section("外观设置") {
                    Picker("主题", selection: $selectedTheme) {
                        ForEach(themes, id: \.self) {
                            Text($0)
                        }
                    }
                    .pickerStyle(.menu)
                    
                    VStack(alignment: .leading, spacing: 8) {
                        Text("字体大小:\(Int(fontSize))")
                        Slider(value: $fontSize, in: 12...24, step: 1)
                            .tint(.blue)
                    }
                }
                
                // 存储设置
                Section("存储设置") {
                    HStack {
                        Text("缓存大小")
                        Spacer()
                        Text(cacheSize)
                            .foregroundStyle(.secondary)
                    }
                    Button("清除缓存") {
                        // 清除缓存逻辑
                        print("清除缓存")
                    }
                    .foregroundStyle(.blue)
                }
                
                // 账户设置
                Section {
                    Button("关于我们") {
                        // 关于我们逻辑
                    }
                    Button("隐私政策") {
                        // 隐私政策逻辑
                    }
                    Button("退出登录", role: .destructive) {
                        // 退出登录逻辑
                    }
                }
            }
            .navigationTitle("设置")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

#Preview {
    SettingsView()
}

代码解析

  • List 和 Section:使用列表和分组组织设置项
  • NavigationStack:提供导航功能
  • Toggle:用于开关设置
  • Picker:用于主题选择
  • Slider:用于调整字体大小
  • Button:用于操作按钮
  • HStack 和 VStack:用于布局
  • @State:用于管理视图状态

小结

本章介绍了 SwiftUI 中的基础视图组件,包括:

  • Text:文本显示,支持富文本、本地化和格式化
  • Image:图片显示,支持系统图标、资源文件和网络图片
  • Button:按钮交互,支持多种样式和角色
  • 输入控件:TextFieldSecureFieldTextEditor
  • 选择控件:TogglePickerSliderStepper
  • 进度指示:ProgressView
  • 一个完整的用户设置页面实战

通过本章的学习,你已经掌握了 SwiftUI 中最常用的基础组件,能够创建各种常见的用户界面元素。


参考资料


本内容为《SwiftUI 基础教程》第三章,欢迎关注后续更新。

昨天以前首页

第2章:声明式 UI 基础

作者 90后晨仔
2026年4月15日 22:22

2.1 声明式 vs 命令式 UI 对比

命令式 UI(UIKit)

命令式编程是一种传统的编程范式,开发者需要明确告诉计算机“如何”完成任务。在 UIKit 中,你需要:

  1. 创建视图对象
  2. 配置视图属性
  3. 添加视图到视图层级
  4. 设置布局约束
  5. 手动更新视图状态

示例代码(UIKit)

import UIKit

class ProfileViewController: UIViewController {
    private let nameLabel = UILabel()
    private let avatarImageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 创建视图
        view.backgroundColor = .white
        
        // 2. 配置 nameLabel
        nameLabel.text = "张三"
        nameLabel.font = UIFont.systemFont(ofSize: 18, weight: .medium)
        nameLabel.textColor = .black
        nameLabel.textAlignment = .center
        
        // 3. 配置 avatarImageView
        avatarImageView.image = UIImage(named: "avatar")
        avatarImageView.contentMode = .scaleAspectFill
        avatarImageView.layer.cornerRadius = 40
        avatarImageView.clipsToBounds = true
        
        // 4. 添加到视图层级
        view.addSubview(avatarImageView)
        view.addSubview(nameLabel)
        
        // 5. 设置约束
        avatarImageView.translatesAutoresizingMaskIntoConstraints = false
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            avatarImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
            avatarImageView.widthAnchor.constraint(equalToConstant: 80),
            avatarImageView.heightAnchor.constraint(equalToConstant: 80),
            
            nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 20),
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
    
    // 6. 更新数据时需要手动刷新 UI
    func updateProfile(name: String, avatar: String) {
        nameLabel.text = name
        avatarImageView.image = UIImage(named: avatar)
    }
}

声明式 UI(SwiftUI)

声明式编程是一种现代的编程范式,开发者只需要描述“是什么”,而不需要关心“如何”实现。在 SwiftUI 中,你只需要:

  1. 描述界面的结构
  2. 绑定状态
  3. 系统自动处理更新

示例代码(SwiftUI)

import SwiftUI

struct ProfileView: View {
    let name: String
    let avatar: String
    
    var body: some View {
        VStack(spacing: 20) {
            // 1. 头像
            Image(avatar)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 80, height: 80)
                .clipShape(Circle())
            
            // 2. 姓名
            Text(name)
                .font(.system(size: 18, weight: .medium))
                .foregroundStyle(.primary)
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(.systemBackground))
    }
}

// 使用示例
ProfileView(name: "张三", avatar: "avatar")

核心差异

维度 命令式 UI(UIKit) 声明式 UI(SwiftUI)
代码量 多(需要手动管理所有细节) 少(只描述结果)
状态同步 手动更新 UI 自动同步状态
可读性 较低(逻辑分散) 高(逻辑集中)
维护性 较低(容易遗漏更新) 高(状态驱动自动更新)
错误率 较高(手动操作容易出错) 较低(框架保证一致性)
开发效率 低(重复代码多) 高(简洁明了)

2.2 View 协议与 body 计算属性

View 协议

在 SwiftUI 中,所有的视图都必须遵循 View 协议:

public protocol View {
    associatedtype Body : View
    @ViewBuilder var body: Self.Body { get }
}

核心概念

  • associatedtype Body:关联类型,表示视图的内容类型
  • body:计算属性,返回视图的内容
  • @ViewBuilder:属性包装器,允许使用声明式语法组合多个视图

body 计算属性

body 是 SwiftUI 视图的核心,它是一个计算属性,每次状态变化时都会重新计算。

重要特性

  1. 计算属性:不是存储属性,每次访问都会重新计算
  2. 轻量级:应该保持简洁,避免复杂计算
  3. 返回类型:必须返回一个遵循 View 协议的类型
  4. 自动合成:@ViewBuilder 允许使用简洁的语法组合多个视图

示例

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, SwiftUI!")
            Button("Tap Me") {}
        }
    }
}

理解 some View

some View 是一个不透明类型(Opaque Type),它表示:

  • 这个函数返回一个遵循 View 协议的类型
  • 但具体是什么类型,不需要暴露给调用者
  • 编译器可以进行更多的优化

2.3 结构体视图与值类型

视图是结构体

在 SwiftUI 中,视图是使用结构体(struct)实现的,这与 UIKit 中的类(class)不同。

结构体的优势

  1. 值类型:传递时会复制,避免引用计数问题
  2. 轻量级:分配在栈内存上,创建和销毁成本低
  3. 不可变:默认不可变,状态通过 @State 等包装器管理
  4. 线程安全:值类型天生线程安全

结构体的生命周期

SwiftUI 视图的生命周期与结构体的实例化无关:

  • 结构体可能被频繁创建和销毁
  • 但底层的真实视图对象由 SwiftUI 管理
  • 视图的身份(Identity)由其在视图树中的位置决定

示例

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

count 改变时:

  1. SwiftUI 检测到状态变化
  2. 重新创建 CounterView 结构体实例
  3. 调用 body 计算属性生成新的视图树
  4. 与旧视图树对比,只更新变化的部分

2.4 修饰符(Modifier)的使用

什么是修饰符?

修饰符是 SwiftUI 中用于修改视图属性和行为的方法。它们通常以点语法链式调用。

修饰符的工作原理

修饰符不是直接修改原视图,而是返回一个新的视图,这个新视图包含了原视图和应用的修改。

示例

Text("Hello")
    .font(.largeTitle)        // 返回一个新的 Text 视图,字体为 largeTitle
    .foregroundStyle(.blue)    // 返回一个新的视图,文本颜色为蓝色
    .padding()                // 返回一个新的视图,带有内边距

常用修饰符

布局修饰符

  • padding():添加内边距
  • frame():设置视图大小和对齐方式
  • background():设置背景
  • foregroundStyle():设置前景样式(颜色、渐变等)
  • clipShape():裁剪视图形状
  • overlay():在视图上叠加内容

排版修饰符

  • font():设置字体
  • bold():加粗文本
  • italic():斜体文本
  • multilineTextAlignment():多行文本对齐
  • lineLimit():限制文本行数

交互修饰符

  • onTapGesture():添加点击手势
  • disabled():禁用视图
  • accessibility():添加无障碍支持

动画修饰符

  • animation():添加动画
  • transition():添加转场动画

修饰符的顺序

修饰符的顺序很重要,因为每个修饰符都会作用于前一个修饰符返回的视图。

示例

// 先设置背景,再添加内边距
Text("Hello")
    .background(Color.blue)
    .padding()

// 先添加内边距,再设置背景
Text("Hello")
    .padding()
    .background(Color.blue)

这两种写法会产生不同的效果,第一种背景只覆盖文本区域,第二种背景会覆盖整个内边距区域。

实战:创建一个信息卡片

需求分析

创建一个包含以下元素的信息卡片:

  1. 标题
  2. 副标题
  3. 描述文本
  4. 图标
  5. 卡片样式(圆角、阴影)

代码实现

import SwiftUI

struct InfoCardView: View {
    // 卡片数据
    let title: String
    let subtitle: String
    let description: String
    let iconName: String
    let iconColor: Color
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // 图标和标题区域
            HStack(alignment: .center, spacing: 12) {
                // 图标
                Circle()
                    .fill(iconColor.opacity(0.1))
                    .frame(width: 48, height: 48)
                    .overlay {
                        Image(systemName: iconName)
                            .resizable()
                            .scaledToFit()
                            .frame(width: 24, height: 24)
                            .foregroundStyle(iconColor)
                    }
                
                // 标题和副标题
                VStack(alignment: .leading, spacing: 4) {
                    Text(title)
                        .font(.headline)
                        .fontWeight(.semibold)
                    Text(subtitle)
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
            }
            
            // 描述文本
            Text(description)
                .font(.body)
                .foregroundStyle(.primary)
                .lineLimit(nil) // 不限制行数
        }
        .padding(16) // 卡片内边距
        .background(
            RoundedRectangle(cornerRadius: 12)
                .fill(Color(.systemBackground))
                .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
        )
        .padding(.horizontal, 16) // 卡片外边距
    }
}

// 使用示例
struct ContentView: View {
    var body: some View {
        VStack(spacing: 16) {
            InfoCardView(
                title: "SwiftUI 简介",
                subtitle: "现代 UI 框架",
                description: "SwiftUI 是一个声明式 UI 框架,允许开发者使用 Swift 语言创建跨 Apple 平台的用户界面。它提供了简洁、直观的语法,使 UI 开发变得更加高效。",
                iconName: "star.fill",
                iconColor: .yellow
            )
            
            InfoCardView(
                title: "声明式编程",
                subtitle: "现代编程范式",
                description: "声明式编程让开发者只需要描述界面的样子,而不需要关心如何实现。系统会自动处理视图的创建、更新和销毁。",
                iconName: "code",
                iconColor: .blue
            )
            
            InfoCardView(
                title: "跨平台",
                subtitle: "一次编写,多处运行",
                description: "SwiftUI 支持 iOS、iPadOS、macOS、watchOS 和 tvOS,让你的代码可以在所有 Apple 平台上运行。",
                iconName: "globe",
                iconColor: .green
            )
        }
        .padding(.vertical, 16)
        .background(Color(.systemGroupedBackground))
    }
}

#Preview {
    ContentView()
}

代码解析

  1. 结构体参数:通过参数传递卡片数据,使视图可复用
  2. VStack 和 HStack:使用栈布局组织视图
  3. Circle:创建圆形背景
  4. overlay:在圆形上叠加图标
  5. RoundedRectangle:创建圆角矩形背景
  6. shadow:添加阴影效果
  7. spacing:设置栈视图的间距
  8. alignment:设置栈视图的对齐方式

小结

本章介绍了声明式 UI 的基础概念,包括:

  • 声明式 vs 命令式 UI 的对比
  • View 协议与 body 计算属性
  • 结构体视图与值类型的特性
  • 修饰符的使用方法和顺序
  • 一个信息卡片的实战实现

通过本章的学习,你已经了解了 SwiftUI 的基本工作原理和核心概念,为后续的学习打下了坚实的基础。

参考资料

第1章:SwiftUI 与开发环境简介

作者 90后晨仔
2026年4月15日 21:55

距离上一次学习SwiftUI已经过去几年的时间了,好多知识点都些忘记了,最近刚好有有一些时间就好好的在从零回顾一下吧。


1.1 什么是 SwiftUI?

官方定义

根据 Apple 官方文档,SwiftUI 是一个现代的声明式 UI 框架,它允许开发者使用 Swift 语言创建跨 Apple 平台的用户界面。

SwiftUI 的核心优势

  1. 声明式语法:描述界面“是什么”而不是“怎么做”
  2. 跨平台:一次编写,在 iOS、iPadOS、macOS、watchOS 和 tvOS 上运行
  3. 实时预览:在 Xcode 中实时查看界面效果
  4. 与 Swift 语言深度集成:充分利用 Swift 的类型安全和现代特性
  5. 自动适配:自动处理不同尺寸设备的布局

与 UIKit 的对比

特性 SwiftUI UIKit
编程范式 声明式 命令式
代码风格 简洁、直观 冗长、命令式
布局系统 自动布局,基于栈 Auto Layout,需要手动设置约束
状态管理 自动状态同步 手动更新 UI
跨平台 支持所有 Apple 平台 主要针对 iOS/tvOS
开发效率 相对较低

1.2 Xcode 开发环境配置

系统要求

  • macOS:最新版本(推荐 macOS Sonoma 或更高)
  • Xcode:最新版本(推荐 Xcode 15 或更高)
  • Swift:Swift 5.7 或更高
  • iOS:iOS 15.0 或更高(如果需要支持旧版本,最低可到 iOS 13.0)

安装 Xcode

  1. 打开 App Store
  2. 搜索 “Xcode”
  3. 点击 “获取” 进行安装
  4. 安装完成后,打开 Xcode 并同意许可协议

安装额外组件

首次打开 Xcode 时,会提示安装额外的组件,包括:

  • 命令行工具
  • 模拟器运行时
  • 其他必要的开发工具

1.3 创建你的第一个 SwiftUI 项目

步骤 1:打开 Xcode

步骤 2:创建新项目

  1. 点击 “Create a new Xcode project”
  2. 选择 “iOS” 标签页
  3. 选择 “App” 模板
  4. 点击 “Next”

步骤 3:配置项目信息

  1. Product Name:输入项目名称,例如 “SwiftUIHelloWorld”
  2. Team:选择你的开发团队(如果没有,可以选择 “None”)
  3. Organization Identifier:输入你的组织标识符,例如 “com.yourname”
  4. Interface:选择 “SwiftUI”
  5. Language:选择 “Swift”
  6. Life Cycle:选择 “SwiftUI App”
  7. 取消勾选 “Use Core Data”(暂时不需要)
  8. 点击 “Next”

步骤 4:选择保存位置

选择一个合适的文件夹保存项目,然后点击 “Create”

步骤 5:项目结构介绍

创建完成后,你会看到以下文件结构:

  • SwiftUIHelloWorldApp.swift:应用程序入口
  • ContentView.swift:主视图
  • Assets.xcassets:资源文件
  • Info.plist:应用配置

1.4 认识 Xcode 预览功能

预览面板

Xcode 右侧的预览面板是 SwiftUI 最强大的特性之一,它允许你实时查看界面效果。

使用预览

  1. 打开 ContentView.swift 文件
  2. 确保右侧的预览面板可见(如果不可见,点击 Xcode 顶部的 “Editor” → “Canvas”)
  3. 你会看到 ContentView 的实时预览
  4. 修改代码,预览会自动更新

预览配置

你可以在预览代码中添加多个预览,例如:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
                .previewDisplayName("默认预览")
            
            ContentView()
                .previewDisplayName("暗黑模式")
                .preferredColorScheme(.dark)
            
            ContentView()
                .previewDisplayName("iPhone 15 Pro")
                .previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro"))
        }
    }
}

预览快捷键

  • Option + Command + P:刷新预览
  • Command + K:清除构建

1.5 SwiftUI 项目结构解析

应用程序入口

SwiftUIHelloWorldApp.swift 是应用的入口点,它定义了应用的结构:

import SwiftUI

@main
struct SwiftUIHelloWorldApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

主视图

ContentView.swift 是应用的主视图:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

关键概念解析

  1. @main:标记应用程序的入口点
  2. App 协议:定义应用的结构
  3. Scene:表示应用的一个场景
  4. WindowGroup:创建应用的窗口
  5. View 协议:所有 SwiftUI 视图必须遵循的协议
  6. body:计算属性,返回视图的内容
  7. #Preview:Xcode 15+ 的新语法,用于创建预览

资源管理

  • Assets.xcassets:管理应用的图片、颜色等资源
  • Info.plist:应用的配置信息,如应用名称、版本号等

实战:创建一个简单的欢迎页面

需求分析

创建一个包含以下元素的欢迎页面:

  1. 应用图标
  2. 应用名称
  3. 欢迎标语
  4. 开始按钮

代码实现

import SwiftUI

struct WelcomeView: View {
    // 状态变量,用于控制是否显示欢迎页面
    @State private var isWelcomeShown = true
    
    var body: some View {
        if isWelcomeShown {
            VStack(spacing: 20) {
                // 应用图标
                Image(systemName: "star.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 100, height: 100)
                    .foregroundStyle(.yellow)
                
                // 应用名称
                Text("欢迎使用 SwiftUI")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                
                // 欢迎标语
                Text("一个现代、简洁的 UI 框架")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
                
                // 开始按钮
                Button("开始探索") {
                    // 点击按钮后隐藏欢迎页面
                    isWelcomeShown = false
                }
                .buttonStyle(.borderedProminent)
                .tint(.blue)
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(
                LinearGradient(
                    colors: [.blue.opacity(0.1), .purple.opacity(0.1)],
                    startPoint: .top,
                    endPoint: .bottom
                )
            )
        } else {
            // 主内容页面
            VStack {
                Text("探索 SwiftUI 的世界!")
                    .font(.title)
                Text("这里是你的应用主界面")
                    .foregroundStyle(.secondary)
            }
            .padding()
        }
    }
}

#Preview {
    WelcomeView()
}

代码解析

  1. @State:用于创建视图的本地状态
  2. VStack:垂直堆叠视图
  3. Image:显示图片,使用系统图标
  4. Text:显示文本
  5. Button:创建按钮,带有点击动作
  6. LinearGradient:创建线性渐变背景
  7. 条件渲染:使用 if-else 控制显示不同的内容

小结

本章介绍了 SwiftUI 的基本概念和开发环境搭建,包括:

  • SwiftUI 的核心优势和与 UIKit 的对比
  • Xcode 的安装和配置
  • 创建第一个 SwiftUI 项目的步骤
  • Xcode 预览功能的使用
  • SwiftUI 项目的基本结构
  • 一个简单欢迎页面的实现

通过本章的学习,你已经了解了 SwiftUI 的基本概念和开发环境,为后续的学习打下了基础。

参考资料

深入剖析 SDAnimatedImageView:如何优雅地在 iOS 中实现高性能动态图渲染

作者 90后晨仔
2026年4月2日 06:28

在日常的 iOS 开发中,动态图(GIF、APNG、WebP)的展示几乎无处不在。然而,很多开发者在使用系统原生的 UIImageView 加载动态图时,往往会遭遇内存暴涨(OOM)或滑动卡顿的窘境。

作为 iOS 圈内最权威的图片处理框架,SDWebImage 为我们提供了一个非常好的解决方案——SDAnimatedImageView

本文将从系统痛点出发,结合 SDWebImage 最新源码,深度拆解 SDAnimatedImageView 的底层架构、核心属性机制,并分享在复杂业务场景下的避坑指南。

文中所涉及源码均基于 SDWebImage 5.x 版本,示例代码采用 Objective-C,Swift 开发者可参照类似逻辑使用。


一、系统原生方案的“三宗罪”

在了解 SDAnimatedImageView 之前,我们必须先明白系统原生方案到底差在哪里。

1. 内存爆炸

系统的 UIImage 在解析 GIF 时,采用“全量解码”策略。
一张体积仅为 2MB 的 GIF,如果包含 50 帧,系统会将其每一帧都解码成庞大的位图对象驻留在内存中。
解码后的位图大小 = 图片宽 × 高 × 4 字节(RGBA)。
假设宽高为 1000×1000,一帧就占约 4MB,50 帧就是 200MB,极易触发 OOM 崩溃。

2. 主线程阻塞

图片的解码过程默认在主线程同步进行,会导致明显的掉帧和卡顿。

3. 控制力极弱

系统几乎没有提供控制 GIF 播放进度、暂停、快进的 API。

SDAnimatedImageView 的诞生,正是为了彻底颠覆这种粗放的渲染模式。


二、核心架构:按需解码与帧缓冲池

SDAnimatedImageView 继承自 UIImageView,但它在内部重构了整个动态图渲染管线。其核心思想是:按需解码,以可控的内存开销换取极致的播放流畅度

1. 零内存的原始数据存储

它配合 SDAnimatedImage 使用。SDAnimatedImage 在初始化时,只保存动态图的原始文件数据(NSData),绝不提前解码任何一帧。此时,无论 GIF 有多少帧,内存占用几乎等于文件本身的大小。

// 从网络或本地获取 NSData
NSData *gifData = [NSData dataWithContentsOfFile:path];
// 创建 SDAnimatedImage,此时仅保留原始数据,不解码
SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:gifData];

2. 智能帧缓冲池

当动画开始播放时,它不会一次性解码所有帧,而是维护一个滑动窗口式的缓冲池。在渲染当前帧的同时,后台异步线程会提前解码接下来的几帧放入内存;当某一帧不再处于缓冲窗口内时,其占用的内存会被立即释放。

3. VSync 级别的精准驱动机制

抛弃了传统的 NSTimer(容易受 RunLoop 阻塞影响导致掉帧),SDAnimatedImageView 底层采用了基于 VSync 信号的 CADisplayLink。它与屏幕刷新率完美同步,根据每一帧设定的 duration 精准计算渲染时机,保证动画如丝般顺滑,且在 App 退到后台时自动暂停,不浪费 CPU 资源。


三、源码级 API 解析(核心属性深挖)

很多开发者只把 SDAnimatedImageView 当作普通的 UIImageView 来用,这其实暴殄天物。以下几个核心属性,体现了框架设计的极致细节。

1. 性能调优:maxBufferSizeprefetchNumberOfFrames

@property (nonatomic, assign) NSUInteger maxBufferSize;
@property (nonatomic, assign) NSUInteger prefetchNumberOfFrames;
  • maxBufferSize:最大缓冲区大小(字节)。
    ⚠️ 重要纠正:很多人以为默认值 0 代表“不限制缓冲”,这是错误的!
    根据官方源码注释,0 代表 Auto(自动调整),框架会根据当前设备的内存压力动态计算缓冲上限。
    如果你需要极致的性能,可以设为 NSUIntegerMax(全缓冲,最高性能);如果内存极度吃紧,设为 1(代表无缓冲,最低内存)。

  • prefetchNumberOfFrames:预解码帧数,默认为 3~5 帧。
    增大它可以提高流畅度(尤其在高帧率动图中),但会增加内存;减小则会降低内存占用,但可能在复杂 GIF 时掉帧。
    这个值需要根据业务场景权衡。

2. 运行循环策略:runLoopMode

@property (nonatomic, strong) NSRunLoopMode runLoopMode;

⚠️ 源码纠正:普遍认为它的默认模式是 NSRunLoopCommonModes,但这并不完全准确。
官方源码的默认逻辑其实更智能:

// SDAnimatedImageView.m 中的 commonInit 片段
if ([[NSProcessInfo processInfo] processorCount] > 1) {
    _runLoopMode = NSRunLoopCommonModes;
} else {
    _runLoopMode = NSDefaultRunLoopMode;
}
  • 在多核设备上,默认为 NSRunLoopCommonModes,确保在 UIScrollView 滑动时,GIF 依然能流畅播放(因为滑动时 RunLoop 切换到了 UITrackingRunLoopMode)。
  • 在单核设备(老旧设备)上,默认降级为 NSDefaultRunLoopMode。目的是在滑动时主动暂停 GIF 播放,以节省宝贵的 CPU 资源用来保证列表滑动的流畅度。

3. 进阶播放控制(易被忽略的宝藏属性)

@property (nonatomic, assign) float playbackRate;                       // 播放速率,默认 1.0
@property (nonatomic, assign) BOOL clearBufferWhenStopped;              // 停止时是否清空缓冲池
@property (nonatomic, assign) BOOL shouldIncrementalLoad;               // 是否支持渐进式加载
  • playbackRate:支持 0.5 慢放、2.0 快进。这在实现类似“表情包编辑器”时非常有用。
  • clearBufferWhenStopped:停止动画时是否清空帧缓存(默认 NO)。
    实战意义极大:在复杂的 Feed 流中,当 Cell 滑出屏幕停止播放时,开启此属性可以立即释放掉该 GIF 占用的解码内存,大幅降低峰值内存。
  • shouldIncrementalLoad:是否支持渐进式加载(默认 YES)。
    配合网络下载,即使 GIF 只下载了 30%,它也能立刻播放已下载完成的那部分帧,带来“秒开”的体验。

四、实战:如何正确使用

1. 结合网络加载(最常用)

得益于 SDWebImage 的封装,日常开发中你甚至不需要手动创建 SDAnimatedImage,框架在下载完毕后会自动识别格式并适配。

步骤

  1. 在 Xib/Storyboard 中,将 UIImageViewCustom Class 改为 SDAnimatedImageView
    或纯代码创建:
    SDAnimatedImageView *imageView = [[SDAnimatedImageView alloc] init];
    
  2. 直接使用 sd_setImage 方法:
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"https://example.com/demo.gif"]
                  placeholderImage:[UIImage imageNamed:@"placeholder"]];

原理:SDWebImage 在下载完成后,会根据图片数据判断是否为动图(如检查 GIF 头部 GIF89a),如果是,会自动创建 SDAnimatedImage 实例并赋值给 animatedImage 属性,从而触发按需解码机制。

2. 本地动态图加载

如果是加载 Bundle 或沙盒中的本地数据,必须手动包装为 SDAnimatedImage 才能触发低内存机制:

// 从 Bundle 中获取 GIF 数据
NSString *path = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"gif"];
NSData *gifData = [NSData dataWithContentsOfFile:path];

// 关键步骤:转换为 SDAnimatedImage,保留原始 NSData
SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:gifData];

// 赋值给 SDAnimatedImageView
self.imageView.animatedImage = animatedImage;  // 自动开始播放(若 autoPlayAnimatedImage 为 YES)

3. 手动控制播放

如果不想自动播放,可以设置 autoPlayAnimatedImage = NO,然后手动调用:

self.imageView.autoPlayAnimatedImage = NO;
self.imageView.animatedImage = animatedImage;
// 在合适的时机手动开始
[self.imageView startAnimating];

也可以获取当前播放状态:

NSUInteger currentFrame = self.imageView.currentFrameIndex;
NSUInteger currentLoop = self.imageView.currentLoopCount;

五、生产环境“避坑指南”

在将 SDAnimatedImageView 推向线上后,我们踩过几个深坑,这里分享给大家。

坑 1:XIB/Storyboard 忘记改 Class

这是排名第一的线上低级错误。视觉上看不出区别,GIF 也能播放,但内存监控会报警。
只要没有把 Custom Class 改为 SDAnimatedImageView,它底层就会退化为原生的全量解码模式
对策:在创建 ImageView 时,务必确认类型。

坑 2:缓存降级导致的“静态图”Bug

场景:首页用 SDAnimatedImageView 加载并缓存了一个 GIF。进入详情页,由于某些原因使用了原生的 UIImageView 加载同一个 URL。
现象:详情页的 GIF 变成了一张静态图。
原因:SDWebImage 的磁盘缓存中,为了保留 SDAnimatedImage 的特性,存储的是经过优化的特殊格式数据。普通的 UIImageView 从缓存读取后,由于不具备解码动态图的能力,只能显示第一帧。
对策:在项目架构层面,统一动态图加载组件,严禁混用原生 UIImageViewSDAnimatedImageView 加载同一个动态图 URL。

坑 3:WebP 动图不支持

SDAnimatedImageView 默认支持 GIF 和 APNG,但不支持 WebP 动图。如果你需要播放 WebP,必须引入独立的解码器。
正确集成方式(在 Podfile 中):

pod 'SDWebImageWebPCoder'

然后在 App 启动时注册:

#import <SDWebImageWebPCoder/SDImageWebPCoder.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [SDImageCodersManager.sharedManager addCoder:SDImageWebPCoder.sharedCoder];
    return YES;
}

坑 4:长列表内存优化组合拳

在包含大量 GIF 的朋友圈或微博 Feed 流中,建议在 UITableViewCellprepareForReuse 中配合以下设置:

- (void)prepareForReuse {
    [super prepareForReuse];
    
    // 取消正在进行的图片加载
    [self.gifImageView sd_cancelCurrentImageLoad];
    
    // 停止播放并清空缓冲,极大缓解长列表内存压力
    self.gifImageView.clearBufferWhenStopped = YES;
    [self.gifImageView stopAnimating];
}

为什么这样做?

  • sd_cancelCurrentImageLoad 避免复用 Cell 时旧图片加载回调错乱。
  • clearBufferWhenStopped = YES 确保 Cell 离开屏幕后立即释放解码内存。
  • stopAnimating 停止 CADisplayLink 回调,节约 CPU。

坑 5:动画不播放的排查思路

如果 GIF 设置了但不播放,可以按以下顺序检查:

  1. 确认 animatedImage 属性不为 nil(如果是网络加载,检查 sd_setImage 的回调中是否成功)。
  2. 确认 autoPlayAnimatedImage 是否为 YES,或手动调用了 startAnimating
  3. 确认 runLoopMode 是否在当前 RunLoop 模式下被允许(常见于滑动时,若设置为了 NSDefaultRunLoopMode 则滑动时会暂停)。
  4. 确认图片数据是否完整(可尝试用 SDAnimatedImageimages 属性查看帧数)。

六、总结

SDAnimatedImageView 绝不仅仅是一个“能播 GIF 的 ImageView”。它通过 按需解码动态帧缓冲VSync 驱动 以及 设备自适应策略,在内存与性能之间找到了最优解。

理解并善用它的进阶属性(如 maxBufferSize 的 Auto 机制、clearBufferWhenStopped 等),不仅能让你的 App 告别动态图引发的 OOM 崩溃,更能体现出一名 iOS 开发者对底层渲染机制的深刻理解。在动态图渲染这一块,SDAnimatedImageView 依然是当前业界当之无愧的标杆。


互动时间:你在项目中遇到过哪些动态图相关的“奇葩”问题?欢迎在评论区留言,我们一起探讨最佳实践!

❌
❌