第二章:SwiftUI 视图基础

SwiftUI 是 Apple 于 WWDC 2019 推出的声明式 UI 框架,使用 Swift 语言描述界面「应该是什么样」,而非「怎么做」。本章覆盖 View 协议、修饰符系统、视图生命周期、预览功能和组件复用等核心概念。


2.1 View 协议与声明式 UI

View 协议

SwiftUI 中所有 UI 元素都遵循 View 协议,body 属性描述视图内容:

swift 复制代码
import SwiftUI

// 最简单的 View
struct GreetingView: View {
    let name: String       // 外部传入的数据
    
    var body: some View {
        Text("你好,\(name)!")
            .font(.largeTitle)
            .foregroundStyle(.blue)
    }
}

// 在父视图中使用
struct ContentView: View {
    var body: some View {
        GreetingView(name: "开发者")
    }
}

// 预览(Xcode 右侧 Canvas 实时显示)
#Preview {
    GreetingView(name: "预览用户")
}

声明式 vs 命令式

命令式编程(UIKit 风格) 关注「怎么做」------你需要手动创建 UI 控件、设置属性、添加约束。每次状态变化,都要手写更新逻辑,代码量大且容易出错。

声明式编程(SwiftUI 风格) 关注「是什么」------你只需描述当前状态下的 UI 形态,SwiftUI 负责自动计算出 DOM 差异并高效更新。状态驱动 UI,UI 是状态的映射(UI = f(State))。

核心优势: 状态变化时无需手动更新 UI,SwiftUI 自动重新执行 body,这大幅减少了 bug 的产生。

swift 复制代码
// UIKit(命令式):描述"怎么做"
let label = UILabel()
label.text = "你好"
label.font = .systemFont(ofSize: 24)
label.textColor = .blue
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true

// SwiftUI(声明式):描述"是什么"
Text("你好")
    .font(.title)
    .foregroundStyle(.blue)
    .frame(maxWidth: .infinity, alignment: .center)

2.2 视图修饰符(ViewModifier)

修饰符(Modifier)是 SwiftUI 最核心的概念之一。每个修饰符都会返回一个新的包装后的 View,而非在原 View 上就地修改。这意味着:

  • 修饰符链形成一个视图树,每层都是前一层的包装
  • 顺序至关重要padding().background(.yellow)background(.yellow).padding() 渲染结果完全不同
  • 修饰符是类型安全的,编译器可以在编译期发现错误
修饰符顺序 表现
.padding().background(.yellow) 背景色覆盖包含 padding 的区域(推荐)
.background(.yellow).padding() 背景色只覆盖文字本身,padding 在背景外
swift 复制代码
struct ModifierDemo: View {
    var body: some View {
        VStack(spacing: 20) {
            // 基础修饰符链
            Text("SwiftUI 修饰符")
                .font(.title2)
                .fontWeight(.bold)
                .foregroundStyle(.white)
                .padding(.horizontal, 20)
                .padding(.vertical, 12)
                .background(
                    LinearGradient(
                        colors: [.blue, .purple],
                        startPoint: .leading,
                        endPoint: .trailing
                    )
                )
                .clipShape(Capsule())
                .shadow(color: .blue.opacity(0.4), radius: 8, x: 0, y: 4)
            
            // 修饰符顺序很重要!
            // 先 padding 再 background ------ 背景包含 padding
            Text("先 padding").padding().background(.yellow)
            
            // 先 background 再 padding ------ padding 在背景外
            Text("先 background").background(.yellow).padding()
        }
    }
}

自定义 ViewModifier

当多个视图需要相同的样式组合时,直接复制修饰符链会导致代码重复难以维护。通过实现 ViewModifier 协议,可以将修饰符组合封装为可复用的模块,再通过 extension View 提供链式调用语法。

最佳实践: 将 App 内通用的卡片样式、按钮样式、加载遮罩等统一封装为 ViewModifier,确保全局 UI 一致性。改动时只需修改一处。

swift 复制代码
// 定义可复用的修饰符组合
struct CardStyle: ViewModifier {
    var backgroundColor: Color = Color(.systemBackground)
    var cornerRadius: CGFloat = 16
    
    func body(content: Content) -> some View {
        content
            .padding(16)
            .background(backgroundColor)
            .clipShape(RoundedRectangle(cornerRadius: cornerRadius))
            .shadow(color: .black.opacity(0.08), radius: 8, y: 4)
    }
}

// 通过 extension 添加便捷方法
extension View {
    func cardStyle(
        backgroundColor: Color = Color(.systemBackground),
        cornerRadius: CGFloat = 16
    ) -> some View {
        modifier(CardStyle(backgroundColor: backgroundColor,
                           cornerRadius: cornerRadius))
    }
    
    // 加载遮罩
    func loadingOverlay(isLoading: Bool) -> some View {
        overlay {
            if isLoading {
                ZStack {
                    Color.black.opacity(0.3)
                    ProgressView()
                        .tint(.white)
                }
                .clipShape(RoundedRectangle(cornerRadius: 16))
            }
        }
    }
    
    // 对齐简便方法
    func leading() -> some View {
        frame(maxWidth: .infinity, alignment: .leading)
    }
}

// 使用
struct ArticleCard: View {
    let title: String
    let isLoading: Bool
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(title)
                .font(.headline)
                .leading()
            Text("点击查看详情")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .cardStyle()
        .loadingOverlay(isLoading: isLoading)
    }
}

2.3 视图生命周期

SwiftUI 视图是值类型(struct) ,没有像 UIViewController 那样显式的 viewDidLoad / viewWillAppear 等钩子。SwiftUI 通过修饰符的方式来观察视图的出现和消失:

修饰符 触发时机 是否自动取消 推荐场景
.onAppear 视图每次出现在屏幕上 同步状态更新、日志上报
.onDisappear 视图每次从屏幕消失 暂停计时器、保存草稿
.task {} 视图出现后立即异步执行 是(自动取消) 网络请求、异步数据加载
.task(id:) id 值变化时重新执行 搜索、依赖 id 的请求
.onChange(of:) 监听指定值变化 响应状态变化的副作用

重要: 优先使用 .task {} 代替 .onAppear 来执行异步操作,因为它会在视图消失时自动取消正在进行的 Task,避免内存泄漏和界面不一致。

swift 复制代码
struct LifecycleView: View {
    @State private var data: [String] = []
    @State private var searchQuery = ""
    
    var body: some View {
        List(data, id: \.self) { Text($0) }
        
        // ① onAppear:视图出现时(每次)
        .onAppear {
            print("视图出现")
        }
        
        // ② onDisappear:视图消失时(每次)
        .onDisappear {
            print("视图消失")
        }
        
        // ③ task:视图出现时执行异步任务,消失时自动取消(推荐)
        .task {
            await loadInitialData()
        }
        
        // ④ task(id:):id 变化时重新执行(适合搜索等响应式场景)
        .task(id: searchQuery) {
            await performSearch(query: searchQuery)
        }
        
        // ⑤ onChange:监听值变化
        .onChange(of: searchQuery) { oldValue, newValue in
            print("搜索词从 '\(oldValue)' 变为 '\(newValue)'")
        }
    }
    
    func loadInitialData() async {
        try? await Task.sleep(for: .seconds(1))
        data = ["Swift", "SwiftUI", "Xcode"]
    }
    
    func performSearch(query: String) async {
        guard !query.isEmpty else { data = []; return }
        // 搜索逻辑...
    }
}

生命周期顺序

复制代码
App 启动:
iOSDemosApp.init() → WindowGroup → ContentView.body

视图进入:
body 执行 → onAppear → task 启动

视图退出:
onDisappear → task 自动取消

注意:SwiftUI 结构体不像 UIViewController 有明显的生命周期,
      body 可能随时被重新调用(状态变化时),
      应避免在 body 中执行副作用操作。

2.4 条件渲染与列表渲染

SwiftUI 通过 if/elseswitchForEach 实现条件与列表渲染。这些都可以在 @ViewBuilder 上下文中直接使用(不需要特殊处理):

  • if/else 根据条件决定是否将视图加入视图树。条件为假时,视图被完全销毁 (会触发 onDisappear),而非仅仅隐藏。若想保留视图仅改变可见性,应使用 .opacity(0).hidden()
  • ForEach 不是 Swift 的 for 循环,而是一个特殊的 SwiftUI 视图,支持增删动画。id 参数用于唯一标识每个元素,SwiftUI 依此做 diff 优化。
  • id 的重要性: ForEach 必须提供稳定的 id。若使用索引(\.offset),在数组中间插入元素时可能产生错误的动画或 bug;推荐使用元素的唯一属性(UUID 等)。
swift 复制代码
struct ConditionalRenderingDemo: View {
    @State private var isLoggedIn = false
    @State private var selectedTab = 0
    
    var body: some View {
        VStack(spacing: 20) {
            // 1. if/else 条件渲染
            if isLoggedIn {
                Label("已登录", systemImage: "checkmark.circle.fill")
                    .foregroundStyle(.green)
            } else {
                Button("登录") { isLoggedIn = true }
            }
            
            // 2. 三元运算符(小范围条件)
            Image(systemName: isLoggedIn ? "lock.open" : "lock")
                .foregroundStyle(isLoggedIn ? .green : .red)
            
            // 3. ForEach 列表渲染
            let fruits = ["苹果", "香蕉", "橙子", "葡萄"]
            ForEach(fruits, id: \.self) { fruit in
                Text(fruit)
                    .padding(.horizontal, 12)
                    .padding(.vertical, 6)
                    .background(.blue.opacity(0.1))
                    .cornerRadius(8)
            }
            
            // 4. ForEach 枚举索引
            ForEach(Array(fruits.enumerated()), id: \.offset) { index, fruit in
                HStack {
                    Text("\(index + 1).")
                        .foregroundStyle(.secondary)
                    Text(fruit)
                }
            }
        }
        .padding()
    }
}

2.5 组件复用与组合

SwiftUI 鼓励组合优于继承的设计理念。将 UI 拆分为职责单一的小组件,再组合成复杂界面,有以下优点:

  1. 可测试性 :小组件逻辑简单,独立 #Preview 验证更容易
  2. 可复用性AvatarViewBadgeView 等可以在任意位置使用
  3. 可维护性:修改某个子组件不影响其他部分
  4. 性能优化:SwiftUI 只会重新渲染状态产生变化的子树

@ViewBuilder 是实现「内容插槽」的关键。标记了 @ViewBuilder 的闭包参数可以接受多个视图、if/elseForEach 等语句,就像在 body 里写代码一样自然。

设计原则: 一个组件如果超过 100 行,就应该考虑是否可以进一步拆分。保持每个组件只关注一件事。

swift 复制代码
// ① 基础组件:职责单一
struct AvatarView: View {
    let name: String
    let size: CGFloat
    var systemImage: String = "person.fill"
    
    var body: some View {
        ZStack {
            Circle()
                .fill(Color(hue: hashColor(name), saturation: 0.6, brightness: 0.8))
                .frame(width: size, height: size)
            
            Text(String(name.prefix(1)).uppercased())
                .font(.system(size: size * 0.4, weight: .bold))
                .foregroundStyle(.white)
        }
    }
    
    private func hashColor(_ string: String) -> Double {
        let hash = string.unicodeScalars.reduce(0) { $0 + Int($1.value) }
        return Double(hash % 360) / 360.0
    }
}

// ② 复合组件:组合多个基础组件
struct UserRowView: View {
    let user: UserInfo
    let onTap: () -> Void
    
    var body: some View {
        Button(action: onTap) {
            HStack(spacing: 12) {
                AvatarView(name: user.name, size: 48)
                
                VStack(alignment: .leading, spacing: 4) {
                    Text(user.name)
                        .font(.headline)
                        .foregroundStyle(.primary)
                    Text(user.email)
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
                
                Spacer()
                
                Image(systemName: "chevron.right")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            .padding(.vertical, 8)
        }
        .buttonStyle(.plain)
    }
}

// ③ ViewBuilder - 灵活的内容插槽
struct SectionCard<Content: View>: View {
    let title: String
    let icon: String
    @ViewBuilder let content: () -> Content
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // 卡片标题
            Label(title, systemImage: icon)
                .font(.headline)
            
            Divider()
            
            // 插槽内容(调用者决定)
            content()
        }
        .cardStyle()
    }
}

// 使用 @ViewBuilder 插槽
SectionCard(title: "个人信息", icon: "person.circle") {
    UserRowView(user: currentUser) { }
    Divider()
    UserRowView(user: anotherUser) { }
}

2.6 环境值与主题

@Environment 提供了一种依赖注入机制,允许父视图向整个子视图树传递值,而无需逐层手动传参。SwiftUI 内置了大量环境值:

键名 类型 用途
colorScheme .dark / .light 深色/浅色模式
dynamicTypeSize DynamicTypeSize 用户字体大小偏好
locale Locale 语言地区
horizontalSizeClass UserInterfaceSizeClass 紧凑/常规布局
dismiss DismissAction 关闭当前视图
openURL OpenURLAction 打开 URL

自定义环境值 需要三步:① 定义 EnvironmentKey 并设置默认值;② 扩展 EnvironmentValues 添加计算属性;③ 用 .environment(\.yourKey, value) 注入。

注意: 环境值是沿视图树向下传递的,子视图可以读取祖先注入的环境值,但无法直接影响兄弟或父视图的环境。

swift 复制代码
// 读取系统环境
struct EnvironmentDemo: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.locale) var locale
    @Environment(\.horizontalSizeClass) var sizeClass
    @Environment(\.dynamicTypeSize) var typeSize
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack(spacing: 16) {
            Text("界面模式:\(colorScheme == .dark ? "深色" : "浅色")")
            Text("语言地区:\(locale.identifier)")
            Text("设备类型:\(sizeClass == .compact ? "iPhone" : "iPad/Mac")")
            Text("字体大小:\(typeSize.description)")
            
            Button("关闭当前页") { dismiss() }
        }
    }
}

// 自定义环境值
struct AppThemeKey: EnvironmentKey {
    static let defaultValue = AppTheme.default
}

extension EnvironmentValues {
    var appTheme: AppTheme {
        get { self[AppThemeKey.self] }
        set { self[AppThemeKey.self] = newValue }
    }
}

// 注入自定义环境值
ContentView()
    .environment(\.appTheme, AppTheme.blue)

2.7 预览(Preview)

swift 复制代码
// 单个预览
#Preview {
    GreetingView(name: "张三")
        .padding()
}

// 多设备预览
#Preview("iPhone SE") {
    ContentView()
}

#Preview("iPad") {
    ContentView()
}

// 深色模式预览
#Preview("Dark Mode") {
    GreetingView(name: "深色用户")
        .preferredColorScheme(.dark)
}

// 不同语言预览
#Preview("英文") {
    GreetingView(name: "Alice")
        .environment(\.locale, Locale(identifier: "en"))
}

// 预览中注入模拟数据
#Preview("已登录状态") {
    let viewModel = AppViewModel()
    viewModel.isLoggedIn = true
    viewModel.currentUser = User.mock
    
    return ProfileView()
        .environment(viewModel)
}

章节总结

知识点 核心要点 重要程度
View 协议 body 描述 UI,some View 不透明类型 ⭐⭐⭐⭐⭐
修饰符系统 链式调用,顺序影响结果 ⭐⭐⭐⭐⭐
自定义 ViewModifier 复用修饰符组合,extension View ⭐⭐⭐⭐
生命周期 onAppear/onDisappear/task/.task(id:) ⭐⭐⭐⭐⭐
条件与列表渲染 if/else/ForEach ⭐⭐⭐⭐⭐
组件复用 拆分子视图、@ViewBuilder 插槽 ⭐⭐⭐⭐⭐
环境值 @Environment 读取系统和自定义值 ⭐⭐⭐⭐
预览 #Preview 多维度调试 ⭐⭐⭐⭐

Demo 说明

文件 演示内容
ViewBasicsDemo.swift View 协议、修饰符顺序可视化
ViewModifierDemo.swift 自定义 ViewModifier + 扩展
LifecycleDemo.swift 生命周期钩子演示
CompositionDemo.swift 组件复用、@ViewBuilder 插槽

📎 扩展内容补充

来源:第二章_UI组件与布局.md
本章概述:掌握 SwiftUI 的核心 View 组件、布局系统(VStack/HStack/ZStack/Grid)、动画、手势、表单、列表、弹窗等 UI 构建能力。


2.1 SwiftUI 基础 View 组件

概念讲解

SwiftUI 提供丰富的内置组件,与 Flutter 的 Widget 体系对应:

Flutter Widget SwiftUI View 用途
Text Text 文本展示
Image Image 图片展示
ElevatedButton Button 按钮
Icon Image(systemName:) SF Symbols
Container Rectangle/RoundedRectangle 形状容器
Divider Divider 分割线
swift 复制代码
import SwiftUI

struct BasicViewsDemo: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // Text - 文本
                Text("你好,SwiftUI!")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundStyle(
                        LinearGradient(colors: [.blue, .purple],
                                      startPoint: .leading,
                                      endPoint: .trailing)
                    )
                
                // Label - 图标+文字(SF Symbols)
                Label("收藏夹", systemImage: "star.fill")
                    .font(.headline)
                    .foregroundStyle(.orange)
                
                // Image - 系统图标
                Image(systemName: "heart.fill")
                    .font(.system(size: 48))
                    .foregroundStyle(.red)
                    .symbolEffect(.bounce)  // iOS 17 动效
                
                // Button - 多种样式
                Button("主要按钮") { print("点击") }
                    .buttonStyle(.borderedProminent)
                
                Button("边框按钮") { print("点击") }
                    .buttonStyle(.bordered)
                
                // Toggle
                Toggle("深色模式", isOn: .constant(true))
                    .toggleStyle(.switch)
                
                // Slider / Stepper / ProgressView
                Slider(value: .constant(0.6))
                    .tint(.blue)
                
                ProgressView(value: 0.7)
                    .progressViewStyle(.linear)
                    .tint(.green)
            }
            .padding()
        }
    }
}

项目中的应用Label 在列表项中做图标+文字,ProgressView 在网络加载时做进度提示,Toggle 用于设置页开关。


2.2 布局系统

概念讲解

SwiftUI 的布局基于 「父视图提供尺寸建议,子视图决定自身大小」 的协商机制,与 Flutter 的约束系统类似。

VStack / HStack / ZStack
swift 复制代码
struct LayoutDemo: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            // HStack - 水平排列
            HStack {
                Circle()
                    .fill(.blue)
                    .frame(width: 50, height: 50)
                
                VStack(alignment: .leading) {
                    Text("张三").font(.headline)
                    Text("iOS 开发工程师").foregroundStyle(.secondary)
                }
                
                Spacer()  // 类似 Flutter 的 Expanded + 空 Container
                
                Image(systemName: "chevron.right")
                    .foregroundStyle(.secondary)
            }
            .padding()
            .background(.regularMaterial)  // 磨砂玻璃效果
            .cornerRadius(12)
            
            // ZStack - 叠加布局(类似 Flutter 的 Stack)
            ZStack(alignment: .bottomTrailing) {
                RoundedRectangle(cornerRadius: 16)
                    .fill(
                        LinearGradient(colors: [.blue, .purple],
                                      startPoint: .topLeading,
                                      endPoint: .bottomTrailing)
                    )
                    .frame(height: 120)
                
                Text("渐变卡片")
                    .font(.title2)
                    .bold()
                    .foregroundStyle(.white)
                    .frame(maxWidth: .infinity, maxHeight: .infinity, 
                           alignment: .center)
                
                // 角标
                Text("NEW")
                    .font(.caption)
                    .bold()
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .background(.orange)
                    .foregroundStyle(.white)
                    .cornerRadius(8)
                    .padding(12)
            }
        }
        .padding()
    }
}
LazyVGrid / LazyHGrid - 网格布局
swift 复制代码
struct GridLayoutDemo: View {
    // 自适应列宽
    let columns = [GridItem(.adaptive(minimum: 100))]
    
    // 固定3列
    let fixedColumns = Array(repeating: GridItem(.flexible()), count: 3)
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 16) {
                ForEach(0..<20) { index in
                    RoundedRectangle(cornerRadius: 12)
                        .fill(Color.randomSystemColor(seed: index))
                        .frame(height: 100)
                        .overlay {
                            Text("\(index)")
                                .foregroundStyle(.white)
                                .font(.title2.bold())
                        }
                }
            }
            .padding()
        }
    }
}
GeometryReader - 动态布局
swift 复制代码
struct AdaptiveView: View {
    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {
                RoundedRectangle(cornerRadius: 8)
                    .fill(.blue)
                    .frame(width: geometry.size.width * 0.6)  // 占60%宽度
                
                RoundedRectangle(cornerRadius: 8)
                    .fill(.orange)
                    .frame(width: geometry.size.width * 0.4)  // 占40%宽度
            }
        }
        .frame(height: 80)
        .padding()
    }
}

2.3 组件复用与 ViewModifier

概念讲解

SwiftUI 通过 ViewModifier 实现可复用的视图修饰,类似 Flutter 的装饰器模式。

swift 复制代码
// 自定义 ViewModifier(类似 Flutter 的 DecoratedBox/Padding 组合)
struct CardModifier: ViewModifier {
    var backgroundColor: Color = .white
    var cornerRadius: CGFloat = 16
    
    func body(content: Content) -> some View {
        content
            .padding(16)
            .background(backgroundColor)
            .cornerRadius(cornerRadius)
            .shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 4)
    }
}

// 扩展 View 提供简便调用
extension View {
    func cardStyle(
        backgroundColor: Color = .white,
        cornerRadius: CGFloat = 16
    ) -> some View {
        modifier(CardModifier(backgroundColor: backgroundColor,
                              cornerRadius: cornerRadius))
    }
    
    // 常用:加载状态遮罩
    func loadingOverlay(isLoading: Bool) -> some View {
        overlay {
            if isLoading {
                ProgressView()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(.ultraThinMaterial)
            }
        }
    }
}

// 使用
struct ProductCard: View {
    let product: Product
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            AsyncImage(url: product.imageURL) { image in
                image.resizable().aspectRatio(contentMode: .fill)
            } placeholder: {
                Rectangle().fill(.gray.opacity(0.1))
            }
            .frame(height: 180)
            .clipped()
            
            Text(product.name).font(.headline)
            Text("¥\(product.price, specifier: "%.2f")")
                .foregroundStyle(.red)
                .fontWeight(.bold)
        }
        .cardStyle()
    }
}

项目中的应用cardStyle() 全局统一卡片样式,避免每个视图重复写 padding+背景+圆角+阴影。


2.4 动画系统

概念讲解

SwiftUI 动画分为隐式动画(withAnimation)显式动画(animation modifier)

隐式动画
swift 复制代码
struct AnimationDemo: View {
    @State private var isExpanded = false
    @State private var rotation = 0.0
    @State private var scale = 1.0
    
    var body: some View {
        VStack(spacing: 30) {
            // 1. 弹簧动画
            RoundedRectangle(cornerRadius: 16)
                .fill(.blue.gradient)
                .frame(
                    width: isExpanded ? 280 : 120,
                    height: isExpanded ? 160 : 80
                )
                .animation(.spring(duration: 0.5, bounce: 0.4), value: isExpanded)
                .onTapGesture { isExpanded.toggle() }
            
            // 2. 旋转 + 缩放
            Image(systemName: "star.fill")
                .font(.system(size: 48))
                .foregroundStyle(.orange)
                .rotationEffect(.degrees(rotation))
                .scaleEffect(scale)
                .onTapGesture {
                    withAnimation(.easeInOut(duration: 0.6)) {
                        rotation += 360
                        scale = scale == 1 ? 1.5 : 1
                    }
                }
            
            // 3. matchedGeometryEffect(类似 Flutter 的 Hero)
            // 见 HeroTransitionDemo
        }
        .padding()
    }
}
matchedGeometryEffect(Hero 动画)
swift 复制代码
struct HeroDemo: View {
    @Namespace private var heroNamespace
    @State private var isExpanded = false
    
    var body: some View {
        if isExpanded {
            // 展开状态(大图)
            VStack {
                Image(systemName: "photo.fill")
                    .matchedGeometryEffect(id: "hero", in: heroNamespace)
                    .frame(width: 300, height: 200)
                Text("点击收起")
            }
            .onTapGesture {
                withAnimation(.spring()) { isExpanded = false }
            }
        } else {
            // 收起状态(缩略图)
            Image(systemName: "photo.fill")
                .matchedGeometryEffect(id: "hero", in: heroNamespace)
                .frame(width: 80, height: 80)
                .onTapGesture {
                    withAnimation(.spring()) { isExpanded = true }
                }
        }
    }
}

2.5 手势识别

概念讲解

swift 复制代码
struct GestureDemo: View {
    @State private var offset: CGSize = .zero
    @State private var scale: CGFloat = 1.0
    @State private var angle: Angle = .zero
    
    var body: some View {
        VStack(spacing: 30) {
            // 1. 拖拽手势(类比 Flutter 的 Draggable)
            RoundedRectangle(cornerRadius: 16)
                .fill(.blue.gradient)
                .frame(width: 120, height: 80)
                .offset(offset)
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            offset = value.translation
                        }
                        .onEnded { _ in
                            withAnimation(.spring()) {
                                offset = .zero  // 回弹
                            }
                        }
                )
            
            // 2. 缩放手势
            Circle()
                .fill(.orange.gradient)
                .frame(width: 100, height: 100)
                .scaleEffect(scale)
                .gesture(
                    MagnifyGesture()
                        .onChanged { value in
                            scale = value.magnification
                        }
                        .onEnded { _ in
                            withAnimation { scale = 1.0 }
                        }
                )
            
            // 3. 旋转手势
            Image(systemName: "arrow.triangle.2.circlepath")
                .font(.system(size: 60))
                .rotationEffect(angle)
                .gesture(
                    RotateGesture()
                        .onChanged { value in
                            angle = value.rotation
                        }
                )
            
            // 4. 组合手势(同时支持缩放+旋转)
            Text("多手势")
                .font(.title)
                .scaleEffect(scale)
                .rotationEffect(angle)
                .gesture(
                    SimultaneousGesture(
                        MagnifyGesture().onChanged { scale = $0.magnification },
                        RotateGesture().onChanged { angle = $0.rotation }
                    )
                )
        }
    }
}

2.6 表单与输入处理

概念讲解

swift 复制代码
struct FormDemo: View {
    @State private var username = ""
    @State private var password = ""
    @State private var email = ""
    @State private var age = 18
    @State private var agreeTerms = false
    @State private var selectedGender = "男"
    
    // 表单校验
    var isValid: Bool {
        !username.isEmpty && !password.isEmpty && password.count >= 6
            && email.contains("@") && agreeTerms
    }
    
    var body: some View {
        Form {
            Section("账号信息") {
                TextField("用户名", text: $username)
                    .textContentType(.username)
                    .autocorrectionDisabled()
                
                SecureField("密码(至少6位)", text: $password)
                    .textContentType(.password)
                
                if !password.isEmpty && password.count < 6 {
                    Label("密码至少需要6位", systemImage: "exclamationmark.circle")
                        .foregroundStyle(.red)
                        .font(.caption)
                }
                
                TextField("邮箱", text: $email)
                    .textContentType(.emailAddress)
                    .keyboardType(.emailAddress)
                    .textInputAutocapitalization(.never)
            }
            
            Section("个人信息") {
                Stepper("年龄:\(age)", value: $age, in: 1...120)
                
                Picker("性别", selection: $selectedGender) {
                    Text("男").tag("男")
                    Text("女").tag("女")
                    Text("其他").tag("其他")
                }
            }
            
            Section {
                Toggle("同意用户协议", isOn: $agreeTerms)
            }
            
            Section {
                Button("注册") {
                    // 提交表单
                }
                .frame(maxWidth: .infinity, alignment: .center)
                .disabled(!isValid)
            }
        }
        .navigationTitle("注册")
    }
}

项目中的应用Form 在设置页面、注册登录页被大量使用,textContentType 让系统自动填充。


2.7 列表与滚动

概念讲解

swift 复制代码
// List - 高性能列表(类比 Flutter 的 ListView.builder)
struct ListDemo: View {
    @State private var items: [TaskItem] = TaskItem.samples
    @State private var isLoading = false
    
    var body: some View {
        List {
            ForEach(items) { item in
                TaskRowView(item: item)
                    .swipeActions(edge: .trailing) {
                        // 左滑删除
                        Button(role: .destructive) {
                            items.removeAll { $0.id == item.id }
                        } label: {
                            Label("删除", systemImage: "trash")
                        }
                        
                        // 左滑归档
                        Button {
                            // 归档操作
                        } label: {
                            Label("归档", systemImage: "archivebox")
                        }
                        .tint(.blue)
                    }
                    .swipeActions(edge: .leading) {
                        // 右滑标记
                        Button {
                            // 标记操作
                        } label: {
                            Label("标记", systemImage: "flag")
                        }
                        .tint(.orange)
                    }
            }
            .onMove { source, destination in
                items.move(fromOffsets: source, toOffset: destination)
            }
            .onDelete { indexSet in
                items.remove(atOffsets: indexSet)
            }
            
            // 下拉加载更多
            if isLoading {
                ProgressView().frame(maxWidth: .infinity)
            }
        }
        .listStyle(.insetGrouped)
        .refreshable {  // 下拉刷新
            await refreshData()
        }
        .toolbar {
            EditButton()  // 编辑模式(排序/删除)
        }
    }
    
    func refreshData() async {
        isLoading = true
        try? await Task.sleep(for: .seconds(1.5))
        isLoading = false
    }
}

struct TaskRowView: View {
    let item: TaskItem
    
    var body: some View {
        HStack {
            Image(systemName: item.isDone ? "checkmark.circle.fill" : "circle")
                .foregroundStyle(item.isDone ? .green : .secondary)
            
            VStack(alignment: .leading, spacing: 2) {
                Text(item.title)
                    .strikethrough(item.isDone)
                Text(item.subtitle).font(.caption).foregroundStyle(.secondary)
            }
            
            Spacer()
            Text(item.priority.rawValue)
                .font(.caption)
                .padding(.horizontal, 8)
                .padding(.vertical, 3)
                .background(item.priority.color.opacity(0.2))
                .foregroundStyle(item.priority.color)
                .cornerRadius(8)
        }
    }
}

2.8 弹窗系统

概念讲解

swift 复制代码
struct AlertSheetDemo: View {
    @State private var showAlert = false
    @State private var showSheet = false
    @State private var showConfirmDialog = false
    @State private var showContextMenu = false
    
    var body: some View {
        VStack(spacing: 20) {
            // 1. Alert 弹窗
            Button("显示 Alert") { showAlert = true }
            .alert("删除确认", isPresented: $showAlert) {
                Button("取消", role: .cancel) {}
                Button("删除", role: .destructive) {
                    print("执行删除")
                }
            } message: {
                Text("此操作不可撤销,确定要删除吗?")
            }
            
            // 2. Sheet 底部弹出
            Button("显示 Sheet") { showSheet = true }
            .sheet(isPresented: $showSheet) {
                // 弹出内容
                NavigationStack {
                    Text("Sheet 内容")
                        .navigationTitle("详情")
                        .toolbar {
                            Button("完成") { showSheet = false }
                        }
                }
                .presentationDetents([.medium, .large])  // 半屏/全屏
                .presentationDragIndicator(.visible)
            }
            
            // 3. ConfirmationDialog(类似 ActionSheet)
            Button("显示选项") { showConfirmDialog = true }
            .confirmationDialog("选择操作", isPresented: $showConfirmDialog,
                               titleVisibility: .visible) {
                Button("拍照") { print("拍照") }
                Button("从相册选择") { print("相册") }
                Button("删除", role: .destructive) { print("删除") }
                Button("取消", role: .cancel) {}
            }
            
            // 4. Context Menu(长按菜单)
            Image(systemName: "photo.fill")
                .font(.system(size: 60))
                .contextMenu {
                    Button { } label: {
                        Label("分享", systemImage: "square.and.arrow.up")
                    }
                    Button { } label: {
                        Label("收藏", systemImage: "star")
                    }
                    Button(role: .destructive) { } label: {
                        Label("删除", systemImage: "trash")
                    }
                }
        }
        .padding()
    }
}

章节总结

知识点 核心组件/API 重要程度
基础 View Text/Image/Button/Label ⭐⭐⭐⭐⭐
布局 VStack/HStack/ZStack/LazyGrid ⭐⭐⭐⭐⭐
ViewModifier modifier()/自定义修饰符 ⭐⭐⭐⭐
动画 withAnimation/matchedGeometryEffect ⭐⭐⭐⭐
手势 DragGesture/MagnifyGesture/RotateGesture ⭐⭐⭐⭐
表单 Form/TextField/Picker/Toggle ⭐⭐⭐⭐⭐
列表 List/ForEach/swipeActions/refreshable ⭐⭐⭐⭐⭐
弹窗 alert/sheet/confirmationDialog/contextMenu ⭐⭐⭐⭐

Demo 说明

本章对应 Demo 位于 iOS_demos/Chapter02/

Demo 文件 演示内容
BasicViewsDemo.swift 基础 View 组件展示
LayoutDemo.swift VStack/HStack/ZStack/Grid 布局
ViewModifierDemo.swift 自定义 ViewModifier 复用
AnimationDemo.swift 弹簧动画/Hero动画
GestureDemo.swift 拖拽/缩放/旋转手势
FormDemo.swift 完整注册表单
ListScrollDemo.swift 下拉刷新/左滑操作/分页列表
AlertSheetDemo.swift Alert/Sheet/ContextMenu弹窗
相关推荐
空中海4 小时前
第七章:iOS网络与数据持久化
网络·ios
空中海4 小时前
第六章:iOS导航与路由系统
macos·ios·cocoa
空中海5 小时前
第九章:iOS系统框架与能力
macos·ios·cocoa
择势6 小时前
MVVM 本质解构 + RxSwift 与 Combine 深度对决与选型指南
swiftui·swift·rxswift
空中海6 小时前
第三章:布局与组件系统
ios
空中海6 小时前
第八章:iOS并发编程
macos·ios·cocoa
空中海8 小时前
第五章:i OS状态与数据流管理
ios
花间相见10 小时前
【大模型微调与部署01】—— ms-swift-3.12入门:安装、快速上手
开发语言·ios·swift
空中海10 小时前
第一章:Swift 语言核心
ios·cocoa·swift