第二章: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/else、switch 和 ForEach 实现条件与列表渲染。这些都可以在 @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. 可复用性 :AvatarView、BadgeView 等可以在任意位置使用
  3. 可维护性:修改某个子组件不影响其他部分
  4. 性能优化:SwiftUI 只会重新渲染状态产生变化的子树

@ViewBuilder 是实现「内容插槽」的关键。标记了 @ViewBuilder 的闭包参数可以接受多个视图、if/else、ForEach 等语句,就像在 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 小时前
类型擦除之后,Metal 滤镜组合为什么不能只执行一个 Filter?
ios·swift
传奇开心果编程6 小时前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
传奇开心果编程7 小时前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程13 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
tink15 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程16 小时前
【现代声明式UI学与练】第9课 性能优化——渲染优化、列表优化、内存优化、启动优化
学习·flutter·react native·ui·性能优化·swiftui·android jetpack
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程17 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架18 小时前
iOS隐私合规扫描工具
ios·审核