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 拆分为职责单一的小组件,再组合成复杂界面,有以下优点:
- 可测试性 :小组件逻辑简单,独立
#Preview验证更容易 - 可复用性 :
AvatarView、BadgeView等可以在任意位置使用 - 可维护性:修改某个子组件不影响其他部分
- 性能优化: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弹窗 |