普通视图

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

“闪电”完成2026人形机器人半马

2026年4月19日 09:20
今天,由北京市人民政府、中央广播电视总台等联合主办的2026人形机器人半程马拉松鸣枪开跑,参赛队伍超百支。自主导航与遥控赛队同台竞技,统一排名,两组别成绩分别按1.0与1.2的加权系数进行核算。 来自北京荣耀的“闪电”以48分19秒的净成绩率先冲线完赛。该机器人也是首个出发的选手。(央视新闻)

宇树称打破人类1500米世界纪录

2026年4月19日 09:20
4月19日,宇树科技发布消息称,16号北京人形机器人马拉松排位赛,宇树H1(2023年改版)自主跑完1.9公里多弯道赛程,用时4分13秒,打破人类1500米世界纪录(按比例计算)。

燃油成本飙升冲击航空业 多家航司上调费用并削减航线

2026年4月19日 09:12
当地时间4月18日获悉,因霍尔木兹海峡局势推高航空燃油价格,多家航空公司正通过上调行李托运费及削减航线来应对成本压力。美国航空业组织“美国航空协会”首席执行官克里斯·苏努努表示,燃油成本约占航空公司运营成本的25%至30%。当前全球油价上涨正持续挤压行业利润空间,预计全年行业亏损可能至少达250亿美元。(央视新闻)

C3安全大会成立多项AI安全生态联盟,华为、阿里云、ABB等参与

2026年4月19日 09:08
4月18日,在第八届C3安全大会上,多家领军企业联合发起多个产业联盟。华为与亚信安全共同成立“鸿蒙安全实验室”,旨在构建面向鸿蒙生态的全栈式终端安全防护体系。此外,“智能体原生安全产业共同体”“Physical AI联盟”以及“天枢卫星互联网与空间计算安全联合实验室”也同步启动,参与方包括阿里云、ABB机器人、德勤、电子科技大学等。各方将围绕智能体安全、物理AI规模化落地及卫星互联网安全等方向开展技术攻关与标准引领。(界面新闻)

《swiftUI进阶 第9章SwiftUI 状态管理完全指南》

作者 90后晨仔
2026年4月19日 00:49

概述

状态管理是 SwiftUI 应用的核心。本章将系统介绍从 iOS 13 到 iOS 17+ 的所有状态管理技术,包括传统的 ObservableObject 系列和现代的 @Observable 宏,帮助你根据项目需求选择最合适的方案。


第一部分:基础状态管理(iOS 13+)

1. @State:本地视图状态

@State 用于管理视图内部的简单状态,当值改变时自动刷新 UI。

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

要点

  • 标记为 private,仅当前视图使用
  • 适合 IntStringBool 等简单类型
  • 当状态变化时,SwiftUI 重新计算 body

2. @Binding:父子视图双向绑定

@Binding 创建对现有状态的引用,允许子视图修改父视图的状态。

struct ParentView: View {
    @State private var isOn = false
    
    var body: some View {
        ToggleView(isOn: $isOn)  // 传递绑定
    }
}

struct ToggleView: View {
    @Binding var isOn: Bool
    
    var body: some View {
        Toggle("开关", isOn: $isOn)
    }
}

第二部分:传统响应式状态管理(iOS 13+)

3. ObservableObject 协议与 @Published

ObservableObject 用于创建可观察的类,@Published 标记需要通知视图的属性。

import Combine

class UserViewModel: ObservableObject {
    @Published var name = "张三"
    @Published var age = 25
    
    func updateName(_ newName: String) {
        name = newName
    }
}

4. @StateObject vs @ObservedObject

特性 @StateObject @ObservedObject
生命周期 视图创建时初始化一次 随视图重建而重建
所有权 拥有对象 仅观察外部对象
适用场景 视图的主要数据源 从父视图传入的对象
struct ContentView: View {
    @StateObject private var viewModel = UserViewModel()  // 拥有
    
    var body: some View {
        ChildView(viewModel: viewModel)  // 传递
    }
}

struct ChildView: View {
    @ObservedObject var viewModel: UserViewModel  // 观察
    
    var body: some View {
        Text(viewModel.name)
    }
}

5. @EnvironmentObject:全局共享状态

通过环境在任意层级共享对象,避免逐层传递。

class AppState: ObservableObject {
    @Published var isLoggedIn = false
}

@main
struct MyApp: App {
    @StateObject private var appState = AppState()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appState)
        }
    }
}

struct ProfileView: View {
    @EnvironmentObject var appState: AppState
    
    var body: some View {
        Text(appState.isLoggedIn ? "已登录" : "未登录")
    }
}

6. @Environment:系统环境值

访问系统提供的环境值,如颜色方案、尺寸类等。

struct ThemeAwareView: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    
    var body: some View {
        Text("当前模式: \(colorScheme == .dark ? "深色" : "浅色")")
    }
}

7. @AppStorage:持久化存储

使用 UserDefaults 自动持久化简单数据。

struct SettingsView: View {
    @AppStorage("username") var username = ""
    @AppStorage("isDarkMode") var isDarkMode = false
    
    var body: some View {
        TextField("用户名", text: $username)
        Toggle("深色模式", isOn: $isDarkMode)
    }
}

8. @SceneStorage:场景持久化

在场景(如多窗口)中保持状态,窗口关闭后自动清除。

struct DocumentView: View {
    @SceneStorage("scrollPosition") var scrollPosition: Double = 0
    
    var body: some View {
        ScrollView {
            // 内容
        }
    }
}

第三部分:现代状态管理(iOS 17+)

9. @Observable 宏

iOS 17 引入 @Observable 宏,简化了可观察对象的创建,无需 ObservableObject@Published

import SwiftUI

@Observable
class UserModel {
    var name = "张三"
    var age = 25
    var email = "zhangsan@example.com"
}

struct ContentView: View {
    @State private var userModel = UserModel()
    
    var body: some View {
        VStack {
            Text("姓名: \(userModel.name)")
            TextField("修改姓名", text: $userModel.name)  // 直接使用 $ 绑定
        }
    }
}

优势

  • 语法更简洁,无需协议和属性包装器
  • 所有属性默认可观察
  • 性能更优(直接访问)

10. @Bindable 双向绑定

当需要将 @Observable 对象的属性传递给需要绑定的子视图时,使用 @Bindable

@Observable
class Settings {
    var isDarkMode = false
}

struct ParentView: View {
    @State private var settings = Settings()
    
    var body: some View {
        ChildView(settings: settings)  // 直接传递
    }
}

struct ChildView: View {
    @Bindable var settings: Settings  // 添加 @Bindable
    
    var body: some View {
        Toggle("深色模式", isOn: $settings.isDarkMode)  // 可绑定
    }
}

11. 使用 @Environment 与 @Observable 结合

现代方式也可以将可观察对象放入环境。

@Observable
class AppState {
    var isLoggedIn = false
    var userName = ""
}

@main
struct MyApp: App {
    @State private var appState = AppState()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(appState)  // 注入环境
        }
    }
}

struct ProfileView: View {
    @Environment(AppState.self) private var appState  // 读取环境
    
    var body: some View {
        Text(appState.isLoggedIn ? "欢迎 \(appState.userName)" : "未登录")
    }
}

第四部分:最佳实践与迁移指南

选择合适的状态管理工具

场景 推荐方式(iOS 13-16) 推荐方式(iOS 17+)
单个视图内部状态 @State @State
父子视图共享 @Binding @Binding
复杂业务逻辑 @StateObject + ObservableObject @State + @Observable
全局共享状态 @EnvironmentObject @Environment + @Observable
持久化简单数据 @AppStorage @AppStorage
场景临时状态 @SceneStorage @SceneStorage

从 ObservableObject 迁移到 @Observable

迁移步骤

  1. class SomeModel: ObservableObject 改为 @Observable class SomeModel
  2. 移除所有 @Published 包装器
  3. @StateObject 改为 @State(如果对象是视图拥有的)
  4. @ObservedObject 改为 @Bindable(如果需要双向绑定)
  5. @EnvironmentObject 改为 @Environment(SomeModel.self)

迁移示例

// 旧方式
class OldViewModel: ObservableObject {
    @Published var text = ""
}
struct OldView: View {
    @StateObject private var vm = OldViewModel()
    var body: some View { TextField("", text: $vm.text) }
}

// 新方式
@Observable
class NewViewModel {
    var text = ""
}
struct NewView: View {
    @State private var vm = NewViewModel()
    var body: some View { TextField("", text: $vm.text) }
}

第五部分:实战:完整的待办事项应用(双版本对比)

传统方式(ObservableObject)

import SwiftUI
import Combine

class TodoViewModel: ObservableObject {
    @Published var todos: [Todo] = []
    @Published var newTitle = ""
    
    struct Todo: Identifiable {
        let id = UUID()
        var title: String
        var isCompleted = false
    }
    
    func addTodo() {
        guard !newTitle.isEmpty else { return }
        todos.append(Todo(title: newTitle))
        newTitle = ""
    }
    
    func toggle(_ todo: Todo) {
        if let idx = todos.firstIndex(where: { $0.id == todo.id }) {
            todos[idx].isCompleted.toggle()
        }
    }
}

struct TodoListView: View {
    @StateObject private var viewModel = TodoViewModel()
    
    var body: some View {
        NavigationStack {
            VStack {
                HStack {
                    TextField("新待办", text: $viewModel.newTitle)
                        .textFieldStyle(.roundedBorder)
                    Button("添加") { viewModel.addTodo() }
                }
                .padding()
                List {
                    ForEach(viewModel.todos) { todo in
                        HStack {
                            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                                .onTapGesture { viewModel.toggle(todo) }
                            Text(todo.title)
                        }
                    }
                }
            }
            .navigationTitle("待办事项")
        }
    }
}

现代方式(@Observable)

import SwiftUI

@Observable
class TodoViewModel {
    var todos: [Todo] = []
    var newTitle = ""
    
    struct Todo: Identifiable {
        let id = UUID()
        var title: String
        var isCompleted = false
    }
    
    func addTodo() {
        guard !newTitle.isEmpty else { return }
        todos.append(Todo(title: newTitle))
        newTitle = ""
    }
    
    func toggle(_ todo: Todo) {
        if let idx = todos.firstIndex(where: { $0.id == todo.id }) {
            todos[idx].isCompleted.toggle()
        }
    }
}

struct TodoListView: View {
    @State private var viewModel = TodoViewModel()
    
    var body: some View {
        NavigationStack {
            VStack {
                HStack {
                    TextField("新待办", text: $viewModel.newTitle)
                        .textFieldStyle(.roundedBorder)
                    Button("添加") { viewModel.addTodo() }
                }
                .padding()
                List {
                    ForEach(viewModel.todos) { todo in
                        HStack {
                            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                                .onTapGesture { viewModel.toggle(todo) }
                            Text(todo.title)
                        }
                    }
                }
            }
            .navigationTitle("待办事项")
        }
    }
}

总结

SwiftUI 提供了从基础到高级的完整状态管理方案:

  • 基础层@State@Binding – 适用于简单、局部的状态
  • 传统响应式层ObservableObject@Published@StateObject@ObservedObject@EnvironmentObject – 适用于 iOS 13-16 的复杂状态管理
  • 持久化层@AppStorage@SceneStorage – 适用于数据持久化
  • 现代层(iOS 17+)@Observable@Bindable – 更简洁、更高效,推荐新项目使用

选择建议:

  • 新项目且最低支持 iOS 17:优先使用 @Observable + @Environment
  • 需要兼容 iOS 16 及以下:继续使用 ObservableObject 系列
  • 两者可以在同一项目中共存,逐步迁移

掌握这些工具,你将能够构建出响应迅速、结构清晰的 SwiftUI 应用。


参考资料


本内容为《SwiftUI 进阶》第9章,涵盖从基础到现代的全部状态管理技术。欢迎关注后续更新。

《 SwiftUI 进阶第8章:表单与设置界面》

作者 90后晨仔
2026年4月19日 00:49

8.1 Form 组件

核心概念

Form 是 SwiftUI 中用于创建表单界面的专用组件,它提供了:

  • 自动的分组和分隔线
  • 自适应的布局
  • 与系统设置一致的外观
  • 支持多种表单控件

基本使用

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    Text("个人信息")
                }
                
                Section {
                    Text("姓名: 张三")
                    Text("年龄: 25")
                    Text("邮箱: zhangsan@example.com")
                }
            }
            .navigationTitle("个人资料")
        }
    }
}

动态表单

import SwiftUI

struct ContentView: View {
    @State private var name = "张三"
    @State private var age = 25
    @State private var email = "zhangsan@example.com"
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    Text("个人信息")
                }
                
                Section {
                    TextField("姓名", text: $name)
                    Stepper("年龄: \(age)", value: $age, in: 1...100)
                    TextField("邮箱", text: $email)
                }
            }
            .navigationTitle("编辑资料")
        }
    }
}

8.2 常见表单控件组合

基础控件

控件类型 用途 示例代码
TextField 文本输入 TextField("输入", text: $text)
SecureField 密码输入 SecureField("密码", text: $password)
Toggle 开关 Toggle("启用", isOn: $isEnabled)
Picker 选择器 Picker("选择", selection: $selection) { ... }
Stepper 步进器 Stepper("数量: \(count)", value: $count)
Slider 滑块 Slider(value: $value, in: 0...100)
DatePicker 日期选择 DatePicker("日期", selection: $date)

组合使用

import SwiftUI

struct ContentView: View {
    @State private var notifications = true
    @State private var sound = true
    @State private var theme = "浅色"
    @State private var brightness = 0.5
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    Toggle("通知", isOn: $notifications)
                    Toggle("声音", isOn: $sound)
                }
                
                Section {
                    Picker("主题", selection: $theme) {
                        Text("浅色").tag("浅色")
                        Text("深色").tag("深色")
                        Text("跟随系统").tag("跟随系统")
                    }
                }
                
                Section {
                    Text("亮度: \(Int(brightness * 100))%")
                    Slider(value: $brightness, in: 0...1)
                }
            }
            .navigationTitle("设置")
        }
    }
}

8.3 表单验证

基本验证

import SwiftUI

struct ContentView: View {
    @State private var email = ""
    @State private var password = ""
    @State private var showError = false
    @State private var errorMessage = ""
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    TextField("邮箱", text: $email)
                        .keyboardType(.emailAddress)
                    SecureField("密码", text: $password)
                }
                
                Section {
                    Button("登录") {
                        if !validateForm() {
                            showError = true
                        }
                    }
                }
            }
            .navigationTitle("登录")
            .alert("错误", isPresented: $showError) {
                Button("确定") {}
            } message: {
                Text(errorMessage)
            }
        }
    }
    
    func validateForm() -> Bool {
        if email.isEmpty {
            errorMessage = "请输入邮箱"
            return false
        }
        if !email.contains("@") {
            errorMessage = "请输入有效的邮箱"
            return false
        }
        if password.count < 6 {
            errorMessage = "密码至少需要6个字符"
            return false
        }
        return true
    }
}

实时验证

import SwiftUI

struct ContentView: View {
    @State private var email = ""
    @State private var password = ""
    
    var emailIsValid: Bool {
        !email.isEmpty && email.contains("@")
    }
    
    var passwordIsValid: Bool {
        password.count >= 6
    }
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    TextField("邮箱", text: $email)
                        .keyboardType(.emailAddress)
                        .foregroundColor(emailIsValid ? .primary : .red)
                    
                    SecureField("密码", text: $password)
                        .foregroundColor(passwordIsValid ? .primary : .red)
                    
                    if !emailIsValid && !email.isEmpty {
                        Text("请输入有效的邮箱")
                            .foregroundColor(.red)
                            .font(.caption)
                    }
                    
                    if !passwordIsValid && !password.isEmpty {
                        Text("密码至少需要6个字符")
                            .foregroundColor(.red)
                            .font(.caption)
                    }
                }
                
                Section {
                    Button("登录") {
                        // 登录逻辑
                    }
                    .disabled(!emailIsValid || !passwordIsValid)
                }
            }
            .navigationTitle("登录")
        }
    }
}

8.4 实战:用户设置页面

完整示例

import SwiftUI

struct ContentView: View {
    @State private var notifications = true
    @State private var sound = true
    @State private var haptic = true
    @State private var darkMode = false
    @State private var language = "简体中文"
    @State private var autoLock = 5 // 分钟
    
    var body: some View {
        NavigationStack {
            List {
                Section("通知设置") {
                    Toggle("推送通知", isOn: $notifications)
                    Toggle("声音", isOn: $sound)
                    Toggle("震动", isOn: $haptic)
                }
                
                Section("外观设置") {
                    Toggle("深色模式", isOn: $darkMode)
                }
                
                Section("语言设置") {
                    Picker("语言", selection: $language) {
                        Text("简体中文").tag("简体中文")
                        Text("English").tag("English")
                    }
                }
                
                Section("安全设置") {
                    Picker("自动锁定", selection: $autoLock) {
                        Text("30秒").tag(0)
                        Text("1分钟").tag(1)
                        Text("5分钟").tag(5)
                        Text("10分钟").tag(10)
                        Text("永不").tag(-1)
                    }
                }
                
                Section("关于") {
                    HStack {
                        Text("版本")
                        Spacer()
                        Text("1.0.0")
                            .foregroundColor(.gray)
                    }
                    
                    Button("检查更新") {
                        // 检查更新逻辑
                    }
                    
                    Button("隐私政策") {
                        // 打开隐私政策
                    }
                }
            }
            .navigationTitle("设置")
        }
    }
}

分组样式

Form {
    // 表单内容
}
.formStyle(.grouped) // 分组样式

最佳实践

  1. 分组逻辑:按照功能将表单控件分组
  2. 标签清晰:为每个控件提供明确的标签
  3. 验证反馈:及时提供验证错误反馈
  4. 默认值:为控件设置合理的默认值
  5. 布局合理:使用合适的控件类型和布局

性能优化

  1. 避免复杂计算:不要在 body 中进行复杂计算
  2. 使用 @State 优化:合理使用 @State 管理表单状态
  3. 延迟加载:对于复杂表单,考虑使用延迟加载

与 iOS 专家博客对比

根据 SwiftUI by Example 的建议:

  • 使用 Section 组织表单内容
  • 为表单控件提供合适的键盘类型
  • 利用 Form 的自动布局特性
  • 结合 NavigationStack 构建设置页面层次

高级技巧

自定义表单样式

struct CustomFormStyle: FormStyle {
    func makeBody(configuration: Configuration) -> some View {
        VStack(spacing: 0) {
            ForEach(configuration.content) {
                $0
                    .padding()
                    .background(Color.white)
                    .border(Color.gray.opacity(0.2), edges: .bottom)
            }
        }
        .background(Color.gray.opacity(0.1))
    }
}

// 使用
Form {
    // 表单内容
}
.formStyle(CustomFormStyle())

表单数据持久化

import SwiftUI

struct ContentView: View {
    @AppStorage("notifications") private var notifications = true
    @AppStorage("darkMode") private var darkMode = false
    @AppStorage("language") private var language = "简体中文"
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    Toggle("通知", isOn: $notifications)
                    Toggle("深色模式", isOn: $darkMode)
                    Picker("语言", selection: $language) {
                        Text("简体中文").tag("简体中文")
                        Text("English").tag("English")
                    }
                }
            }
            .navigationTitle("设置")
        }
    }
}

总结

表单与设置界面是应用中常见的组成部分,SwiftUI 提供了强大的 Form 组件来简化开发:

  • Form:创建结构化的表单布局
  • 多种内置控件:满足各种输入需求
  • 实时验证:提供良好的用户反馈
  • 与系统风格一致:确保视觉一致性

通过合理组织表单内容、提供清晰的验证反馈、使用适当的控件类型,可以创建出既美观又实用的设置界面。


参考资料


本内容为《SwiftUI 进阶》第八章,欢迎关注后续更新。

《SwiftUI 进阶第7章:导航系统》

作者 90后晨仔
2026年4月19日 00:35

7.1 NavigationStack 基础导航

核心概念

NavigationStack 是 SwiftUI 中用于构建导航层次结构的核心组件,它替代了旧版的 NavigationView(在 iOS 16+ 中已被废弃)。

基本使用

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("前往详情页", destination: DetailView())
            }
            .navigationTitle("主页面")
        }
    }
}

struct DetailView: View {
    var body: some View {
        Text("详情页内容")
            .navigationTitle("详情页")
    }
}

程序化导航

NavigationStack 支持使用路径进行程序化导航:

import SwiftUI

struct ContentView: View {
    @State private var path: [Int] = []
    
    var body: some View {
        NavigationStack(path: $path) {
            List(1..<10) { number in
                NavigationLink(value: number) {
                    Text("项目 \(number)")
                }
            }
            .navigationTitle("主页面")
            .navigationDestination(for: Int.self) {
                DetailView(number: $0, path: $path)
            }
        }
    }
}

struct DetailView: View {
    let number: Int
    @Binding var path: [Int]
    
    var body: some View {
        VStack {
            Text("详情页 \(number)")
            Button("前往下一页") {
                path.append(number + 10)
            }
            Button("返回首页") {
                path.removeAll()
            }
        }
        .navigationTitle("详情页 \(number)")
    }
}

与官方文档对比

根据苹果官方文档,NavigationStack 提供了更灵活的导航控制,包括:

  • 路径管理:可以通过绑定的数组控制导航状态
  • 类型安全:使用 navigationDestination(for:) 提供类型安全的导航目标
  • 向后兼容:在 iOS 16+ 中推荐使用

7.2 NavigationLink 页面跳转

核心概念

NavigationLink 是用于创建导航链接的组件,它可以:

  • 直接指定目标视图
  • 使用值传递方式(配合 navigationDestination
  • 控制激活状态

直接目标方式

NavigationLink("前往详情页", destination: DetailView())

值传递方式

NavigationLink(value: item) {
    Text(item.name)
}

条件导航

NavigationLink(
    "登录", 
    destination: LoginView(),
    isActive: $isLoggedIn
)

7.3 navigationTitle 与 navigationBarTitleDisplayMode

navigationTitle

设置导航栏标题:

.navigationTitle("页面标题")

navigationBarTitleDisplayMode

控制标题显示模式:

模式 描述
.automatic 自动(默认)
.inline 内联模式(小字体)
.large 大标题模式
.navigationBarTitleDisplayMode(.large)

7.4 Sheet 模态视图

核心概念

Sheet 用于显示模态视图,通常用于:

  • 表单填写
  • 详情展示
  • 辅助操作

基本使用

import SwiftUI

struct ContentView: View {
    @State private var isSheetPresented = false
    
    var body: some View {
        Button("显示 Sheet") {
            isSheetPresented = true
        }
        .sheet(isPresented: $isSheetPresented) {
            SheetView(isPresented: $isSheetPresented)
        }
    }
}

struct SheetView: View {
    @Binding var isPresented: Bool
    
    var body: some View {
        VStack {
            Text("这是一个 Sheet 视图")
            Button("关闭") {
                isPresented = false
            }
        }
        .padding()
    }
}

带值的 Sheet

.sheet(item: $selectedItem) {
    DetailView(item: $0)
}

7.5 TabView 标签页导航

核心概念

TabView 用于创建底部标签栏导航,是构建多标签应用的基础。

基本使用

import SwiftUI

struct ContentView: View {
    @State private var selectedTab = 0
    
    var body: some View {
        TabView(selection: $selectedTab) {
            HomeView()
                .tabItem {
                    Label("首页", systemImage: "house")
                }
                .tag(0)
            
            ProfileView()
                .tabItem {
                    Label("个人", systemImage: "person")
                }
                .tag(1)
        }
    }
}

struct HomeView: View {
    var body: some View {
        Text("首页")
    }
}

struct ProfileView: View {
    var body: some View {
        Text("个人中心")
    }
}

自定义样式

TabView {
    // 标签内容
}
.tabViewStyle(.automatic) // 自动样式

最佳实践

  1. 导航层次:保持导航层次清晰,避免过深的导航栈
  2. 标题设置:为每个页面设置合适的标题和显示模式
  3. 模态视图:合理使用 Sheet 展示临时内容
  4. 标签栏:控制标签数量(建议 3-5 个)
  5. 状态管理:使用 @State@Observable 管理导航状态

性能优化

  1. 延迟加载:使用 LazyView 包装目标视图
  2. 导航栈管理:及时清理不需要的导航路径
  3. 避免过度动画:减少导航过程中的复杂动画

建议:

  • 优先使用 NavigationStack 而非 NavigationView
  • 使用值类型传递而非对象引用
  • 结合 @ObservableObservableObject 管理复杂导航状态

实战:多页面应用

import SwiftUI

struct ContentView: View {
    @State private var path: [String] = []
    
    var body: some View {
        NavigationStack(path: $path) {
            TabView {
                HomeView(path: $path)
                    .tabItem {
                        Label("首页", systemImage: "house")
                    }
                
                SettingsView()
                    .tabItem {
                        Label("设置", systemImage: "gear")
                    }
            }
        }
    }
}

struct HomeView: View {
    @Binding var path: [String]
    
    var body: some View {
        List {
            NavigationLink(value: "detail") {
                Text("详情页")
            }
            NavigationLink(value: "profile") {
                Text("个人资料")
            }
        }
        .navigationTitle("首页")
        .navigationDestination(for: String.self) {
            switch $0 {
            case "detail":
                DetailView()
            case "profile":
                ProfileView()
            default:
                Text("未知页面")
            }
        }
    }
}

struct DetailView: View {
    var body: some View {
        Text("详情页内容")
            .navigationTitle("详情")
    }
}

struct ProfileView: View {
    var body: some View {
        Text("个人资料")
            .navigationTitle("个人")
    }
}

struct SettingsView: View {
    var body: some View {
        Text("设置页面")
            .navigationTitle("设置")
    }
}

总结

导航系统是构建 iOS 应用的核心部分,SwiftUI 提供了现代化的导航组件:

  • NavigationStack:构建导航层次结构
  • NavigationLink:创建导航链接
  • Sheet:显示模态视图
  • TabView:实现标签页导航

掌握这些组件的使用,将帮助你构建结构清晰、用户体验良好的多页面应用。


参考资料


本内容为《SwiftUI 进阶》第七章,欢迎关注后续更新。

《SwiftUI 进阶第5章:数据处理与网络请求》

作者 90后晨仔
2026年4月19日 00:11

学习目标

  • 掌握 SwiftUI 中的数据处理基本方法
  • 了解如何进行网络请求
  • 学习如何处理网络请求的加载状态和错误
  • 掌握数据过滤和排序的方法
  • 了解如何使用 JSONDecoder 解析 JSON 数据

核心概念

数据模型

在 SwiftUI 中,数据模型通常使用结构体来定义,并且需要符合 Identifiable 协议以便在列表中使用。

示例代码:

struct Post: Identifiable, Decodable {
    let id: Int
    let title: String
    let body: String
    let userId: Int
}

本地数据处理

本地数据处理包括数据的添加、删除、修改和查询等操作。

示例代码:

@State private var localData: [String] = ["本地数据1", "本地数据2", "本地数据3"]
@State private var newData = ""

HStack {
    TextField("输入新数据", text: $newData)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .padding()
    
    Button("添加") {
        if !newData.isEmpty {
            localData.append(newData)
            newData = ""
        }
    }
}

List(localData, id: \.self) { item in
    Text(item)
}

网络请求

在 SwiftUI 中,网络请求通常使用 URLSession 来实现,并且需要在后台线程中执行,然后在主线程中更新 UI。

示例代码:

@State private var posts: [Post] = []
@State private var isLoading = false
@State private var errorMessage: String? = nil

func fetchPosts() {
    isLoading = true
    errorMessage = nil
    
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
        self.isLoading = false
        self.errorMessage = "无效的URL"
        return
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        DispatchQueue.main.async {
            self.isLoading = false
            
            if let error = error {
                self.errorMessage = error.localizedDescription
                return
            }
            
            guard let data = data else {
                self.errorMessage = "无数据返回"
                return
            }
            
            do {
                let decodedPosts = try JSONDecoder().decode([Post].self, from: data)
                self.posts = decodedPosts
            } catch {
                self.errorMessage = "解析数据失败"
            }
        }
    }.resume()
}

数据状态管理

在网络请求过程中,需要管理不同的状态:加载中、加载成功和加载失败。

示例代码:

if isLoading {
    ProgressView("加载中...")
        .padding()
} else if let errorMessage = errorMessage {
    Text("错误: \(errorMessage)")
        .foregroundColor(.red)
        .padding()
} else {
    List(posts) { post in
        VStack(alignment: .leading) {
            Text(post.title)
                .font(.headline)
            Text(post.body)
                .font(.body)
                .foregroundColor(.gray)
        }
    }
}

数据过滤

使用 filter 方法可以对数据进行过滤,只显示符合条件的数据。

示例代码:

List(localData.filter { $0.contains("1") }, id: \.self) {
    Text($0)
}

数据排序

使用 sorted 方法可以对数据进行排序,按照指定的规则排列数据。

示例代码:

List(localData.sorted(), id: \.self) {
    Text($0)
}

实践示例:完整数据处理与网络请求演示

以下是一个完整的数据处理与网络请求演示示例:

import SwiftUI

// 数据模型
struct Post: Identifiable, Decodable {
    let id: Int
    let title: String
    let body: String
    let userId: Int
}

struct DataProcessingAndNetworkingDemo: View {
    // 状态管理
    @State private var posts: [Post] = []
    @State private var isLoading = false
    @State private var errorMessage: String? = nil
    @State private var localData: [String] = ["本地数据1", "本地数据2", "本地数据3"]
    @State private var newData = ""
    @State private var filterKeyword = ""
    @State private var sortAscending = true
    
    var body: some View {
        ScrollView {
            VStack(spacing: 25) {
                // 标题
                Text("数据处理与网络请求")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.blue)
                
                // 1. 本地数据处理
                VStack(alignment: .leading, spacing: 12) {
                    Text("1. 本地数据处理")
                        .font(.headline)
                    
                    HStack {
                        TextField("输入新数据", text: $newData)
                            .textFieldStyle(.roundedBorder)
                        Button("添加") {
                            if !newData.isEmpty {
                                localData.append(newData)
                                newData = ""
                            }
                        }
                        .buttonStyle(.borderedProminent)
                    }
                    
                    List(localData, id: \.self) { item in
                        Text(item)
                    }
                    .frame(height: 150)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 2. 网络请求
                VStack(alignment: .leading, spacing: 12) {
                    Text("2. 网络请求")
                        .font(.headline)
                    
                    Button("获取网络数据") {
                        fetchPosts()
                    }
                    .buttonStyle(.borderedProminent)
                    
                    if isLoading {
                        ProgressView("加载中...")
                            .padding()
                    } else if let errorMessage = errorMessage {
                        Text("错误: \(errorMessage)")
                            .foregroundColor(.red)
                            .padding()
                    } else if !posts.isEmpty {
                        List(posts) { post in
                            VStack(alignment: .leading) {
                                Text(post.title)
                                    .font(.headline)
                                Text(post.body)
                                    .font(.body)
                                    .foregroundColor(.gray)
                            }
                        }
                        .frame(height: 250)
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 3. 数据过滤
                VStack(alignment: .leading, spacing: 12) {
                    Text("3. 数据过滤")
                        .font(.headline)
                    
                    TextField("输入过滤关键词", text: $filterKeyword)
                        .textFieldStyle(.roundedBorder)
                    
                    List(localData.filter { 
                        filterKeyword.isEmpty ? true : $0.contains(filterKeyword) 
                    }, id: \.self) { item in
                        Text(item)
                    }
                    .frame(height: 120)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 4. 数据排序
                VStack(alignment: .leading, spacing: 12) {
                    Text("4. 数据排序")
                        .font(.headline)
                    
                    Toggle("升序排列", isOn: $sortAscending)
                    
                    List(sortAscending ? localData.sorted() : localData.sorted(by: >), id: \.self) { item in
                        Text(item)
                    }
                    .frame(height: 120)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
            }
            .padding()
        }
    }
    
    // 网络请求方法
    func fetchPosts() {
        isLoading = true
        errorMessage = nil
        
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
            self.isLoading = false
            self.errorMessage = "无效的URL"
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            DispatchQueue.main.async {
                self.isLoading = false
                
                if let error = error {
                    self.errorMessage = error.localizedDescription
                    return
                }
                
                guard let data = data else {
                    self.errorMessage = "无数据返回"
                    return
                }
                
                do {
                    let decodedPosts = try JSONDecoder().decode([Post].self, from: data)
                    self.posts = decodedPosts
                } catch {
                    self.errorMessage = "解析数据失败"
                }
            }
        }.resume()
    }
}

#Preview {
    DataProcessingAndNetworkingDemo()
}

常见问题与解决方案

1. 网络请求在主线程执行

问题:网络请求在主线程执行,导致 UI 卡顿。

解决方案:使用 DispatchQueue.global().async 将网络请求放在后台线程执行,然后在主线程中更新 UI。实际上 URLSession.dataTask 的回调默认就在后台线程,只需确保 UI 更新在 DispatchQueue.main.async 中。

URLSession.shared.dataTask(with: url) { data, response, error in
    DispatchQueue.main.async {
        // 更新 UI
    }
}.resume()

2. 数据解析失败

问题:JSON 数据解析失败。

解决方案

  • 确保数据模型与 JSON 数据结构完全匹配(字段名、类型)
  • 使用 CodingKeys 处理字段名不一致的情况
  • 使用 try?do-catch 捕获错误
struct Post: Decodable {
    let id: Int
    let title: String
    
    enum CodingKeys: String, CodingKey {
        case id
        case title = "post_title"  // 如果 JSON 字段名不同
    }
}

3. 加载状态未正确显示

问题:网络请求过程中没有显示加载状态。

解决方案:使用 @State 变量管理加载状态,并在请求开始前设置为 true,完成后设置为 false

4. 错误处理不完善

问题:网络请求失败时没有显示错误信息。

解决方案:捕获并处理网络请求中的错误,将错误信息显示给用户。

if let errorMessage = errorMessage {
    Text("错误: \(errorMessage)")
        .foregroundColor(.red)
}

总结

本章介绍了 SwiftUI 中的数据处理与网络请求,包括:

  • 数据模型的定义:使用 IdentifiableDecodable 协议
  • 本地数据处理:增删改查、列表展示
  • 网络请求的实现:使用 URLSession 和异步回调
  • 数据状态管理:加载中、成功、失败三种状态
  • 数据过滤:使用 filter 方法按条件筛选
  • 数据排序:使用 sorted 方法自定义排序规则

通过这些技术,可以实现数据的获取、处理和展示,为应用提供丰富的数据源。在实际开发中,数据处理与网络请求是应用的核心功能之一,掌握这些技术对于开发高质量的 SwiftUI 应用至关重要。


参考资料


本内容为《SwiftUI 进阶》第五章,欢迎关注后续更新。

每日一题-下标对中的最大距离🟡

2026年4月19日 00:00

给你两个 非递增 的整数数组 nums1nums2 ,数组下标均 从 0 开始 计数。

下标对 (i, j)0 <= i < nums1.length0 <= j < nums2.length 。如果该下标对同时满足 i <= jnums1[i] <= nums2[j] ,则称之为 有效 下标对,该下标对的 距离j - i

返回所有 有效 下标对 (i, j) 中的 最大距离 。如果不存在有效下标对,返回 0

一个数组 arr ,如果每个 1 <= i < arr.length 均有 arr[i-1] >= arr[i] 成立,那么该数组是一个 非递增 数组。

 

示例 1:

输入:nums1 = [55,30,5,4,2], nums2 = [100,20,10,10,5]
输出:2
解释:有效下标对是 (0,0), (2,2), (2,3), (2,4), (3,3), (3,4) 和 (4,4) 。
最大距离是 2 ,对应下标对 (2,4) 。

示例 2:

输入:nums1 = [2,2,2], nums2 = [10,10,1]
输出:1
解释:有效下标对是 (0,0), (0,1) 和 (1,1) 。
最大距离是 1 ,对应下标对 (0,1) 。

示例 3:

输入:nums1 = [30,29,19,5], nums2 = [25,25,25,25,25]
输出:2
解释:有效下标对是 (2,2), (2,3), (2,4), (3,3) 和 (3,4) 。
最大距离是 2 ,对应下标对 (2,4) 。

 

提示:

  • 1 <= nums1.length <= 105
  • 1 <= nums2.length <= 105
  • 1 <= nums1[i], nums2[j] <= 105
  • nums1nums2 都是 非递增 数组

《SwiftUI 进阶第4章:响应式布局》

作者 90后晨仔
2026年4月18日 23:19

Snip20260418_7.png

学习目标

  • 掌握 SwiftUI 中的响应式布局概念
  • 了解如何根据屏幕尺寸调整布局
  • 学习使用环境变量获取设备信息
  • 掌握动态网格布局的实现方法
  • 了解几何读取器和安全区域的使用

核心概念

响应式布局基础

在 SwiftUI 中,响应式布局是通过环境变量、条件布局和自适应组件来实现的,它可以根据不同的屏幕尺寸和设备类型自动调整布局。


环境变量

尺寸类

SwiftUI 提供了尺寸类来描述设备的屏幕尺寸,主要有两种尺寸类:

  • horizontalSizeClass - 水平尺寸类,分为 .compact(紧凑)和 .regular(常规)
  • verticalSizeClass - 垂直尺寸类,同样分为 .compact.regular

示例代码:

@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass

Text("水平尺寸类: \(horizontalSizeClass == .compact ? "紧凑" : "常规")")
Text("垂直尺寸类: \(verticalSizeClass == .compact ? "紧凑" : "常规")")

自适应布局

根据尺寸类调整布局是响应式设计的核心。

示例代码:

// 根据水平尺寸类调整布局
if horizontalSizeClass == .compact {
    // 紧凑模式 - 垂直布局
    VStack(spacing: 10) {
        Color.red
            .frame(height: 100)
            .cornerRadius(10)
        Color.green
            .frame(height: 100)
            .cornerRadius(10)
        Color.blue
            .frame(height: 100)
            .cornerRadius(10)
    }
} else {
    // 常规模式 - 水平布局
    HStack(spacing: 10) {
        Color.red
            .frame(height: 100)
            .cornerRadius(10)
        Color.green
            .frame(height: 100)
            .cornerRadius(10)
        Color.blue
            .frame(height: 100)
            .cornerRadius(10)
    }
}

动态网格布局

使用 LazyVGridGridItem 可以创建动态网格布局,根据屏幕尺寸自动调整列数。

示例代码:

// 根据水平尺寸类调整网格列数
let columns = horizontalSizeClass == .compact ? [
    GridItem(.flexible()),
    GridItem(.flexible())
] : [
    GridItem(.flexible()),
    GridItem(.flexible()),
    GridItem(.flexible()),
    GridItem(.flexible())
]

LazyVGrid(columns: columns, spacing: 10) {
    ForEach(1..<9) { index in
        Color(hue: Double(index)/10, saturation: 0.8, brightness: 0.8)
            .frame(height: 100)
            .cornerRadius(10)
            .overlay(
                Text("\(index)")
                    .foregroundColor(.white)
                    .font(.headline)
            )
    }
}

几何读取器

GeometryReader 可以获取父视图的尺寸和位置信息,用于创建更加灵活的布局。

示例代码:

GeometryReader { geometry in
    VStack {
        Text("屏幕宽度: \(geometry.size.width, specifier: "%.0f")")
        Text("屏幕高度: \(geometry.size.height, specifier: "%.0f")")
        
        Rectangle()
            .fill(.purple)
            .frame(width: geometry.size.width * 0.8, height: 100)
            .cornerRadius(10)
    }
}
.frame(height: 200)

安全区域

安全区域是指屏幕上不会被系统 UI(如状态栏、导航栏、底部安全区域)遮挡的区域。

示例代码:

Color.blue
    .frame(height: 100)
    .ignoresSafeArea(edges: .top)
    .cornerRadius(10)

自适应文本

使用 .multilineTextAlignment() 可以创建自适应文本,根据屏幕宽度自动换行。

示例代码:

Text("这是一段自适应文本,会根据屏幕宽度自动换行")
    .font(.body)
    .multilineTextAlignment(.center)
    .padding()
    .background(.gray.opacity(0.1))
    .cornerRadius(10)

条件内容

根据尺寸类显示不同的内容,实现设备特定的布局。

示例代码:

if horizontalSizeClass == .compact {
    Text("当前是手机模式,显示手机专用内容")
        .font(.body)
        .padding()
        .background(.green)
        .foregroundColor(.white)
        .cornerRadius(10)
} else {
    Text("当前是平板模式,显示平板专用内容")
        .font(.body)
        .padding()
        .background(.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
}

动态间距

根据屏幕尺寸调整组件之间的间距。

示例代码:

// 根据水平尺寸类调整间距
let spacing = horizontalSizeClass == .compact ? 10.0 : 20.0

VStack(spacing: spacing) {
    Color.red
        .frame(height: 50)
        .cornerRadius(10)
    Color.green
        .frame(height: 50)
        .cornerRadius(10)
    Color.blue
        .frame(height: 50)
        .cornerRadius(10)
}

实践示例:完整响应式布局演示

以下是一个完整的响应式布局演示示例,包含了各种响应式设计技术:

import SwiftUI

struct ResponsiveLayoutDemo: View {
    // 环境变量 - 用于获取屏幕尺寸
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    @Environment(\.verticalSizeClass) private var verticalSizeClass
    
    var body: some View {
        ScrollView {
            VStack(spacing: 25) {
                // 标题
                Text("响应式布局")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.blue)
                
                // 1. 屏幕尺寸信息
                VStack {
                    Text("1. 屏幕尺寸信息")
                        .font(.headline)
                    HStack {
                        Text("水平尺寸类:")
                        Text(horizontalSizeClass == .compact ? "紧凑 (Compact)" : "常规 (Regular)")
                            .foregroundColor(.blue)
                    }
                    HStack {
                        Text("垂直尺寸类:")
                        Text(verticalSizeClass == .compact ? "紧凑 (Compact)" : "常规 (Regular)")
                            .foregroundColor(.blue)
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 2. 自适应布局(垂直/水平切换)
                VStack {
                    Text("2. 自适应布局")
                        .font(.headline)
                    
                    if horizontalSizeClass == .compact {
                        VStack(spacing: 10) {
                            Color.red.frame(height: 60).cornerRadius(8)
                            Color.green.frame(height: 60).cornerRadius(8)
                            Color.blue.frame(height: 60).cornerRadius(8)
                        }
                    } else {
                        HStack(spacing: 10) {
                            Color.red.frame(height: 80).cornerRadius(8)
                            Color.green.frame(height: 80).cornerRadius(8)
                            Color.blue.frame(height: 80).cornerRadius(8)
                        }
                    }
                    Text("\(horizontalSizeClass == .compact ? "垂直堆叠" : "水平排列")")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 3. 动态网格布局
                VStack {
                    Text("3. 动态网格布局")
                        .font(.headline)
                    
                    let columns = horizontalSizeClass == .compact ? [
                        GridItem(.flexible()),
                        GridItem(.flexible())
                    ] : [
                        GridItem(.flexible()),
                        GridItem(.flexible()),
                        GridItem(.flexible()),
                        GridItem(.flexible())
                    ]
                    
                    LazyVGrid(columns: columns, spacing: 10) {
                        ForEach(1..<9) { index in
                            Color(hue: Double(index)/10, saturation: 0.7, brightness: 0.9)
                                .frame(height: 80)
                                .cornerRadius(8)
                                .overlay(
                                    Text("\(index)")
                                        .foregroundColor(.white)
                                        .font(.headline)
                                )
                        }
                    }
                    Text("\(horizontalSizeClass == .compact ? "2列网格" : "4列网格")")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 4. 几何读取器
                VStack {
                    Text("4. 几何读取器")
                        .font(.headline)
                    
                    GeometryReader { geometry in
                        VStack {
                            Text("可用宽度: \(geometry.size.width, specifier: "%.0f")")
                                .font(.caption)
                            Rectangle()
                                .fill(.purple)
                                .frame(width: geometry.size.width * 0.7, height: 40)
                                .cornerRadius(8)
                                .overlay(
                                    Text("70% 宽度")
                                        .font(.caption)
                                        .foregroundColor(.white)
                                )
                        }
                        .frame(maxWidth: .infinity)
                    }
                    .frame(height: 100)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 5. 安全区域示例
                VStack {
                    Text("5. 安全区域")
                        .font(.headline)
                    
                    Color.blue
                        .frame(height: 60)
                        .cornerRadius(8)
                        .overlay(
                            Text("默认在安全区域内")
                                .foregroundColor(.white)
                        )
                    
                    Color.orange
                        .frame(height: 60)
                        .cornerRadius(8)
                        .ignoresSafeArea(edges: .horizontal)
                        .overlay(
                            Text("忽略水平安全区域")
                                .foregroundColor(.white)
                        )
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 6. 自适应文本
                VStack {
                    Text("6. 自适应文本")
                        .font(.headline)
                    
                    Text("这是一段自适应文本,会根据屏幕宽度自动换行。当屏幕较窄时,文字会折行显示;屏幕较宽时,可以在一行内完整显示。")
                        .font(.body)
                        .multilineTextAlignment(.center)
                        .padding()
                        .background(Color.gray.opacity(0.2))
                        .cornerRadius(8)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 7. 条件内容(设备专用)
                VStack {
                    Text("7. 条件内容")
                        .font(.headline)
                    
                    if horizontalSizeClass == .compact {
                        Text("📱 手机模式:显示紧凑型布局")
                            .font(.body)
                            .padding()
                            .frame(maxWidth: .infinity)
                            .background(.green)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    } else {
                        Text("🖥️ 平板模式:显示扩展型布局")
                            .font(.body)
                            .padding()
                            .frame(maxWidth: .infinity)
                            .background(.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 8. 动态间距
                VStack {
                    Text("8. 动态间距")
                        .font(.headline)
                    
                    let dynamicSpacing = horizontalSizeClass == .compact ? 8.0 : 20.0
                    
                    VStack(spacing: dynamicSpacing) {
                        Color.red.frame(height: 40).cornerRadius(6)
                        Color.green.frame(height: 40).cornerRadius(6)
                        Color.blue.frame(height: 40).cornerRadius(6)
                    }
                    Text("当前间距: \(dynamicSpacing, specifier: "%.0f") pt")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
            }
            .padding()
        }
    }
}

#Preview {
    ResponsiveLayoutDemo()
}

常见问题与解决方案

1. 布局在不同设备上显示不一致

问题:布局在手机上显示正常,但在平板上显示异常。

解决方案:使用尺寸类和条件布局,为不同尺寸的设备提供不同的布局方案。

// 根据水平尺寸类选择不同的布局结构
if horizontalSizeClass == .compact {
    // 手机布局:垂直堆叠
    VStack { ... }
} else {
    // 平板布局:水平排列或更复杂的网格
    HStack { ... }
}

2. 内容被安全区域遮挡

问题:内容被状态栏或导航栏遮挡。

解决方案:使用 .ignoresSafeArea() 修饰符或确保内容在安全区域内。

// 方法一:忽略安全区域(适用于背景视图)
Color.blue.ignoresSafeArea()

// 方法二:使用 safeAreaInset 添加自定义内容
List {
    // 内容
}
.safeAreaInset(edge: .bottom) {
    Button("底部按钮") { }
        .padding()
}

3. 网格布局在小屏幕上显示拥挤

问题:网格布局在小屏幕上列数过多,导致内容拥挤。

解决方案:根据屏幕尺寸动态调整网格列数。

let columns = horizontalSizeClass == .compact ? [
    GridItem(.flexible()),
    GridItem(.flexible())
] : [
    GridItem(.flexible()),
    GridItem(.flexible()),
    GridItem(.flexible())
]

4. GeometryReader 导致布局异常

问题:使用 GeometryReader 后,子视图大小不符合预期。

解决方案:注意 GeometryReader 会占据父视图提供的全部空间,可以在内部使用 frame(height:) 限制高度。

GeometryReader { geometry in
    // 内容
}
.frame(height: 200)  // 固定高度

总结

本章介绍了 SwiftUI 中的响应式布局技术,包括:

  • 环境变量:使用 @Environment 获取设备尺寸信息(horizontalSizeClassverticalSizeClass
  • 自适应布局:根据尺寸类调整布局结构(VStackHStack
  • 动态网格布局:使用 LazyVGridGridItem 创建响应式网格
  • 几何读取器:通过 GeometryReader 获取父视图尺寸,实现精确布局
  • 安全区域:处理状态栏、导航栏等系统 UI 遮挡问题
  • 自适应文本:使用 .multilineTextAlignment() 实现文本自动换行
  • 条件内容:为不同设备类型显示不同的 UI 组件
  • 动态间距:根据屏幕尺寸调整组件之间的间距

通过这些技术,可以创建在不同设备上都能良好显示的布局,提升用户体验。在实际开发中,响应式布局是确保应用在各种设备上都能正常显示的重要手段。


参考资料


本内容为《SwiftUI 进阶》第四章,欢迎关注后续更新。

《SwiftUI 进阶学习第3章:手势与交互》

作者 90后晨仔
2026年4月18日 23:06

手势基础

在 SwiftUI 中,手势是通过各种手势类型和修饰符来实现的,它们可以附加到任何视图上,以响应用户的交互。


常用手势类型

1. 点击手势

点击手势通过 onTapGesture 修饰符实现,用于检测用户的点击操作。

示例代码:

@State private var isTapped = false

Rectangle()
    .fill(.blue)
    .frame(width: 200, height: 100)
    .cornerRadius(10)
    .onTapGesture {
        isTapped.toggle()
    }

点击次数:

可以通过 count 参数指定点击次数,例如双击:

Rectangle()
    .onTapGesture(count: 2) {
        tapCount += 1
    }

2. 长按手势

长按手势通过 onLongPressGesture 修饰符实现,用于检测用户的长按操作。

示例代码:

@State private var isLongPressed = false

Rectangle()
    .fill(.green)
    .frame(width: 200, height: 100)
    .cornerRadius(10)
    .onLongPressGesture {
        isLongPressed.toggle()
    }

3. 拖拽手势

拖拽手势通过 DragGesture 实现,用于检测用户的拖拽操作。

示例代码:

@State private var dragOffset = CGSize.zero

Circle()
    .fill(.red)
    .frame(width: 50, height: 50)
    .offset(dragOffset)
    .gesture(
        DragGesture()
            .onChanged { value in
                dragOffset = value.translation
            }
            .onEnded { value in
                // 可以在这里添加结束拖动的逻辑
            }
    )

4. 缩放手势

缩放手势通过 MagnificationGesture 实现,用于检测用户的缩放操作。

示例代码:

@State private var scale = 1.0

Rectangle()
    .fill(.purple)
    .frame(width: 200, height: 200)
    .cornerRadius(10)
    .scaleEffect(scale)
    .gesture(
        MagnificationGesture()
            .onChanged { value in
                scale = value
            }
    )

5. 旋转手势

旋转手势通过 RotationGesture 实现,用于检测用户的旋转操作。

示例代码:

@State private var rotation = 0.0

Rectangle()
    .fill(.orange)
    .frame(width: 200, height: 200)
    .cornerRadius(10)
    .rotationEffect(.degrees(rotation))
    .gesture(
        RotationGesture()
            .onChanged { value in
                rotation = value.degrees
            }
    )

组合手势

组合手势是将多种手势效果结合在一起,可以使用 .simultaneousGesture() 修饰符。

示例代码:

@State private var offset = CGSize.zero
@State private var scale = 1.0
@State private var rotation = 0.0

Rectangle()
    .fill(.blue)
    .frame(width: 200, height: 200)
    .cornerRadius(10)
    .offset(offset)
    .scaleEffect(scale)
    .rotationEffect(.degrees(rotation))
    .gesture(
        DragGesture()
            .onChanged { value in
                offset = value.translation
            }
    )
    .simultaneousGesture(
        MagnificationGesture()
            .onChanged { value in
                scale = value
            }
    )
    .simultaneousGesture(
        RotationGesture()
            .onChanged { value in
                rotation = value.degrees
            }
    )

手势状态管理

手势通常与状态管理结合使用,以追踪手势的状态和数据。

示例代码:

@State private var isDragging = false

Rectangle()
    .fill(.purple)
    .frame(width: 100, height: 100)
    .cornerRadius(10)
    .gesture(
        DragGesture()
            .onChanged { _ in
                isDragging = true
            }
            .onEnded { _ in
                isDragging = false
            }
    )

实践示例:完整手势演示

以下是一个完整的手势演示示例,包含了各种手势类型和组合:

import SwiftUI

struct GestureAndInteractionDemo: View {
    // 状态管理
    @State private var isTapped = false
    @State private var isLongPressed = false
    @State private var offset = CGSize.zero
    @State private var scale = 1.0
    @State private var rotation = 0.0
    @State private var dragOffset = CGSize.zero
    @State private var isDragging = false
    @State private var tapCount = 0
    
    var body: some View {
        ScrollView {
            VStack(spacing: 25) {
                // 标题
                Text("手势与交互")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.blue)
                
                // 1. 点击手势
                VStack {
                    Text("1. 点击手势")
                        .font(.headline)
                    Rectangle()
                        .fill(isTapped ? Color.green : Color.blue)
                        .frame(width: 200, height: 80)
                        .cornerRadius(10)
                        .onTapGesture {
                            withAnimation {
                                isTapped.toggle()
                            }
                        }
                    Text("状态: \(isTapped ? "已点击" : "未点击")")
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 2. 长按手势
                VStack {
                    Text("2. 长按手势")
                        .font(.headline)
                    Rectangle()
                        .fill(isLongPressed ? Color.red : Color.green)
                        .frame(width: 200, height: 80)
                        .cornerRadius(10)
                        .onLongPressGesture {
                            withAnimation {
                                isLongPressed.toggle()
                            }
                        }
                    Text("状态: \(isLongPressed ? "长按中" : "未长按")")
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 3. 拖拽手势
                VStack {
                    Text("3. 拖拽手势")
                        .font(.headline)
                    Circle()
                        .fill(.red)
                        .frame(width: 60, height: 60)
                        .offset(dragOffset)
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    dragOffset = value.translation
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        dragOffset = .zero
                                    }
                                }
                        )
                    Text("拖拽小球后自动归位")
                        .font(.caption)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 4. 缩放手势
                VStack {
                    Text("4. 缩放手势")
                        .font(.headline)
                    Rectangle()
                        .fill(.purple)
                        .frame(width: 120, height: 120)
                        .cornerRadius(10)
                        .scaleEffect(scale)
                        .gesture(
                            MagnificationGesture()
                                .onChanged { value in
                                    scale = value
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        scale = 1.0
                                    }
                                }
                        )
                    Text("双指缩放,松手复位")
                        .font(.caption)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 5. 旋转手势
                VStack {
                    Text("5. 旋转手势")
                        .font(.headline)
                    Rectangle()
                        .fill(.orange)
                        .frame(width: 120, height: 120)
                        .cornerRadius(10)
                        .rotationEffect(.degrees(rotation))
                        .gesture(
                            RotationGesture()
                                .onChanged { value in
                                    rotation = value.degrees
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        rotation = 0
                                    }
                                }
                        )
                    Text("双指旋转,松手复位")
                        .font(.caption)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 6. 组合手势
                VStack {
                    Text("6. 组合手势")
                        .font(.headline)
                    Rectangle()
                        .fill(.blue)
                        .frame(width: 120, height: 120)
                        .cornerRadius(10)
                        .offset(offset)
                        .scaleEffect(scale)
                        .rotationEffect(.degrees(rotation))
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    offset = value.translation
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        offset = .zero
                                    }
                                }
                        )
                        .simultaneousGesture(
                            MagnificationGesture()
                                .onChanged { value in
                                    scale = value
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        scale = 1.0
                                    }
                                }
                        )
                        .simultaneousGesture(
                            RotationGesture()
                                .onChanged { value in
                                    rotation = value.degrees
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        rotation = 0
                                    }
                                }
                        )
                    Text("支持拖动、缩放、旋转")
                        .font(.caption)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 7. 双击计数
                VStack {
                    Text("7. 双击计数")
                        .font(.headline)
                    Rectangle()
                        .fill(.teal)
                        .frame(width: 200, height: 80)
                        .cornerRadius(10)
                        .onTapGesture(count: 2) {
                            tapCount += 1
                        }
                    Text("双击次数: \(tapCount)")
                    Button("重置") {
                        tapCount = 0
                    }
                    .buttonStyle(.bordered)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
            }
            .padding()
        }
    }
}

#Preview {
    GestureAndInteractionDemo()
}

常见问题与解决方案

1. 手势不响应

问题:视图添加了手势,但没有响应。

解决方案

  • 确保视图有足够的大小(例如,不要将手势添加到 frame(width: 0, height: 0) 的视图上)
  • 检查视图是否被其他视图遮挡(使用 .contentShape(Rectangle()) 扩大可点击区域)
  • 确保没有其他手势冲突
// 扩大点击区域
Rectangle()
    .fill(.clear)
    .contentShape(Rectangle())  // 使透明区域也能响应手势
    .onTapGesture { }

2. 组合手势冲突

问题:多个手势同时应用时出现冲突。

解决方案

  • 使用 .simultaneousGesture() 修饰符来允许同时处理多个手势
  • 使用 .highPriorityGesture() 让某个手势优先
  • 使用 .gesture()including 参数控制手势识别行为
// 高优先级手势(会阻断其他手势)
view.highPriorityGesture(
    TapGesture().onEnded { }
)

// 同时识别多个手势
view.simultaneousGesture(dragGesture)
    .simultaneousGesture(rotationGesture)

3. 手势状态管理

问题:手势结束后状态没有正确更新。

解决方案

  • .onEnded 回调中正确更新状态
  • 对于需要持久化的状态,使用 @State@StateObject
DragGesture()
    .onChanged { value in
        // 实时更新
        offset = value.translation
    }
    .onEnded { value in
        // 结束时的处理
        withAnimation {
            offset = .zero
        }
    }

总结

本章介绍了 SwiftUI 中的手势与交互,包括:

  • 基本手势类型:点击、长按、拖拽、缩放、旋转
  • 手势的状态管理和数据处理
  • 组合手势的实现方法(.simultaneousGesture
  • 手势的高级应用技巧(优先级控制、自定义识别)

通过这些手势,可以使应用界面更加交互友好,提升用户体验。在实际开发中,合理使用手势可以为应用增添交互性,使界面操作更加直观自然。


参考资料


本内容为《SwiftUI 进阶》第三章,欢迎关注后续更新。

《SwiftUI 进阶学习第2章:动画与过渡》

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

学习目标

  • 掌握 SwiftUI 中的基本动画实现
  • 了解不同类型的动画效果
  • 学习如何创建组合动画
  • 掌握过渡效果的使用方法
  • 了解不同动画曲线的特点

核心概念

动画基础

在 SwiftUI 中,动画是通过 withAnimation 函数来实现的,它可以将状态变化包装在动画中,使 UI 变化更加平滑自然。

withAnimation {
    // 状态变化
}

动画类型

1. 淡入淡出动画

淡入淡出动画通过改变视图的不透明度来实现,可以使用 .transition(.opacity) 修饰符。

struct FadeAnimationDemo: View {
    @State private var isVisible = false
    
    var body: some View {
        VStack {
            Button("显示/隐藏") {
                withAnimation {
                    isVisible.toggle()
                }
            }
            
            if isVisible {
                Text("Hello, Animation!")
                    .transition(.opacity)
            }
        }
    }
}

2. 缩放动画

缩放动画通过改变视图的缩放比例来实现,可以使用 .scaleEffect() 修饰符。

struct ScaleAnimationDemo: View {
    @State private var scale = 1.0
    
    var body: some View {
        VStack {
            Button("缩放") {
                withAnimation(.spring()) {
                    scale = scale == 1.0 ? 1.5 : 1.0
                }
            }
            
            Circle()
                .fill(.red)
                .frame(width: 100, height: 100)
                .scaleEffect(scale)
        }
    }
}

3. 旋转动画

旋转动画通过改变视图的旋转角度来实现,可以使用 .rotationEffect() 修饰符。

struct RotationAnimationDemo: View {
    @State private var rotation = 0.0
    
    var body: some View {
        VStack {
            Button("旋转") {
                withAnimation(.easeInOut(duration: 1.0)) {
                    rotation += 360
                }
            }
            
            Rectangle()
                .fill(.yellow)
                .frame(width: 100, height: 100)
                .rotationEffect(.degrees(rotation))
        }
    }
}

4. 位移动画

位移动画通过改变视图的位置来实现,可以使用 .offset() 修饰符。

struct OffsetAnimationDemo: View {
    @State private var offset = CGSize.zero
    
    var body: some View {
        VStack {
            Button("移动") {
                withAnimation(.interactiveSpring()) {
                    offset = offset == .zero ? CGSize(width: 100, height: 50) : .zero
                }
            }
            
            Rectangle()
                .fill(.blue)
                .frame(width: 100, height: 100)
                .offset(offset)
        }
    }
}

5. 颜色动画

颜色动画通过改变视图的颜色来实现,可以直接动画化颜色属性。

struct ColorAnimationDemo: View {
    @State private var color = Color.blue
    
    var body: some View {
        VStack {
            Button("变色") {
                withAnimation(.easeInOut(duration: 1.0)) {
                    color = color == .blue ? .red : .blue
                }
            }
            
            Rectangle()
                .fill(color)
                .frame(width: 200, height: 100)
                .cornerRadius(10)
        }
    }
}

6. 组合动画

组合动画是将多种动画效果结合在一起,可以同时应用多个动画修饰符。

struct CombinedAnimationDemo: View {
    @State private var scale = 1.0
    @State private var rotation = 0.0
    @State private var opacity = 1.0
    
    var body: some View {
        VStack {
            Button("组合动画") {
                withAnimation(.easeInOut(duration: 1.0)) {
                    scale = scale == 1.0 ? 1.2 : 1.0
                    rotation = rotation == 0 ? 45 : 0
                    opacity = opacity == 1.0 ? 0.5 : 1.0
                }
            }
            
            Rectangle()
                .fill(.green)
                .frame(width: 100, height: 100)
                .scaleEffect(scale)
                .rotationEffect(.degrees(rotation))
                .opacity(opacity)
        }
    }
}

过渡效果

过渡效果是在视图出现或消失时应用的动画,可以使用 .transition() 修饰符。

struct TransitionDemo: View {
    @State private var isVisible = false
    
    var body: some View {
        VStack {
            Button("切换视图") {
                withAnimation {
                    isVisible.toggle()
                }
            }
            
            if isVisible {
                Text("滑入视图")
                    .transition(.slide)
            }
        }
    }
}

SwiftUI 提供了多种内置过渡效果:

过渡效果 描述
.opacity 淡入淡出
.slide 从边缘滑入/滑出
.scale 缩放出现/消失
.move(edge:) 从指定方向移动
.asymmetric(insertion:removal:) 不对称过渡(出现和消失用不同效果)

动画曲线

动画曲线定义了动画的速度变化,可以使用不同的动画曲线来实现不同的视觉效果。

常用动画曲线

曲线 描述
.linear 线性动画,速度保持不变
.easeIn 缓入动画,开始慢,逐渐加快
.easeOut 缓出动画,开始快,逐渐减慢
.easeInOut 缓入缓出动画,开始慢,中间快,结束慢
.spring() 弹簧动画,有弹性效果
.interactiveSpring() 交互式弹簧动画,响应更灵敏

示例代码

// 线性动画
withAnimation(.linear(duration: 1.0)) {
    // 动画代码
}

// 弹簧动画
withAnimation(.spring(response: 0.6, dampingFraction: 0.8, blendDuration: 0)) {
    // 动画代码
}

// 可重复动画
withAnimation(.easeInOut(duration: 1.0).repeatCount(3, autoreverses: true)) {
    // 动画代码
}

实践示例:完整动画演示

以下是一个完整的动画演示示例,包含了各种动画类型和过渡效果:

import SwiftUI

struct AnimationAndTransitionDemo: View {
    // 状态管理
    @State private var isVisible = false
    @State private var scale = 1.0
    @State private var rotation = 0.0
    @State private var opacity = 1.0
    @State private var offset = CGSize.zero
    @State private var color = Color.blue
    @State private var selectedCurve = "linear"
    
    let curveOptions = ["linear", "easeIn", "easeOut", "easeInOut", "spring"]
    
    var body: some View {
        ScrollView {
            VStack(spacing: 25) {
                // 标题
                Text("动画与过渡")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.blue)
                
                // 动画曲线选择
                VStack {
                    Text("动画曲线选择")
                        .font(.headline)
                    Picker("曲线", selection: $selectedCurve) {
                        ForEach(curveOptions, id: \.self) { option in
                            Text(option).tag(option)
                        }
                    }
                    .pickerStyle(.segmented)
                }
                
                // 淡入淡出动画
                VStack {
                    Text("1. 淡入淡出")
                        .font(.headline)
                    Button("显示/隐藏") {
                        withAnimation(getAnimation()) {
                            isVisible.toggle()
                        }
                    }
                    if isVisible {
                        Text("Hello, Animation!")
                            .padding()
                            .background(Color.orange)
                            .cornerRadius(8)
                            .transition(.opacity)
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 缩放动画
                VStack {
                    Text("2. 缩放动画")
                        .font(.headline)
                    Button("缩放") {
                        withAnimation(getAnimation()) {
                            scale = scale == 1.0 ? 1.5 : 1.0
                        }
                    }
                    Circle()
                        .fill(.red)
                        .frame(width: 80, height: 80)
                        .scaleEffect(scale)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 旋转动画
                VStack {
                    Text("3. 旋转动画")
                        .font(.headline)
                    Button("旋转") {
                        withAnimation(getAnimation()) {
                            rotation += 360
                        }
                    }
                    Rectangle()
                        .fill(.yellow)
                        .frame(width: 80, height: 80)
                        .rotationEffect(.degrees(rotation))
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 位移动画
                VStack {
                    Text("4. 位移动画")
                        .font(.headline)
                    Button("移动") {
                        withAnimation(getAnimation()) {
                            offset = offset == .zero ? CGSize(width: 100, height: 50) : .zero
                        }
                    }
                    Rectangle()
                        .fill(.blue)
                        .frame(width: 80, height: 80)
                        .offset(offset)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 颜色动画
                VStack {
                    Text("5. 颜色动画")
                        .font(.headline)
                    Button("变色") {
                        withAnimation(getAnimation()) {
                            color = color == .blue ? .red : .blue
                        }
                    }
                    Rectangle()
                        .fill(color)
                        .frame(width: 150, height: 80)
                        .cornerRadius(10)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 组合动画
                VStack {
                    Text("6. 组合动画")
                        .font(.headline)
                    Button("组合动画") {
                        withAnimation(getAnimation()) {
                            scale = scale == 1.0 ? 1.2 : 1.0
                            rotation = rotation == 0 ? 45 : 0
                            opacity = opacity == 1.0 ? 0.5 : 1.0
                        }
                    }
                    Rectangle()
                        .fill(.green)
                        .frame(width: 80, height: 80)
                        .scaleEffect(scale)
                        .rotationEffect(.degrees(rotation))
                        .opacity(opacity)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 过渡效果
                VStack {
                    Text("7. 过渡效果(Slide)")
                        .font(.headline)
                    Button("切换视图") {
                        withAnimation(getAnimation()) {
                            isVisible.toggle()
                        }
                    }
                    if isVisible {
                        Text("滑入视图")
                            .padding()
                            .background(Color.purple)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                            .transition(.slide)
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
            }
            .padding()
        }
    }
    
    // 根据选择的曲线返回对应的动画
    private func getAnimation() -> Animation {
        switch selectedCurve {
        case "linear":
            return .linear(duration: 0.8)
        case "easeIn":
            return .easeIn(duration: 0.8)
        case "easeOut":
            return .easeOut(duration: 0.8)
        case "easeInOut":
            return .easeInOut(duration: 0.8)
        case "spring":
            return .spring(response: 0.6, dampingFraction: 0.7)
        default:
            return .easeInOut(duration: 0.8)
        }
    }
}

#Preview {
    AnimationAndTransitionDemo()
}

常见问题与解决方案

1. 动画不生效

问题:状态变化了,但没有动画效果。

解决方案:确保状态变化被包裹在 withAnimation 函数中。

// 错误 ❌
isVisible.toggle()

// 正确 ✅
withAnimation {
    isVisible.toggle()
}

2. 动画效果不符合预期

问题:动画效果不够流畅或不符合预期。

解决方案:尝试使用不同的动画曲线,如 .spring().easeInOut(),并调整动画时长。

// 使用弹簧动画获得更自然的弹性效果
withAnimation(.spring(response: 0.5, dampingFraction: 0.6)) {
    // 状态变化
}

3. 过渡效果不显示

问题:视图出现或消失时没有过渡效果。

解决方案:确保为视图添加了 .transition() 修饰符,并且状态变化在 withAnimation 中。

if isVisible {
    Text("Hello")
        .transition(.slide)  // 必须添加 transition
}

4. 动画卡顿或掉帧

问题:动画执行时界面卡顿。

解决方案

  • 避免在动画中同时改变过多属性
  • 对于复杂视图,考虑使用 .drawingGroup() 优化渲染
  • 确保动画中不执行耗时操作

总结

本章介绍了 SwiftUI 中的动画与过渡效果,包括:

  • 基本动画类型:淡入淡出、缩放、旋转、位移、颜色动画
  • 组合动画:同时应用多种动画效果
  • 过渡效果:视图出现/消失时的动画(.transition
  • 动画曲线:线性、缓入、缓出、弹簧等不同速度曲线
  • 实践示例:完整的动画演示应用

通过这些动画效果,可以使应用界面更加生动有趣,提升用户体验。在实际开发中,合理使用动画可以为应用增添活力,使界面交互更加自然流畅。


参考资料


本内容为《SwiftUI 高级教程》第二章,欢迎关注后续更新。

《SwiftUI 进阶学习第1章:高级视图组件》

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

Snip20260418_5.png

概述

本章介绍 SwiftUI 中的高级视图组件,包括日期选择器、时间选择器、分段控件、滑块、步进器、活动指示器、进度视图和列表分组等。这些组件可以帮助您构建更加丰富和交互性更强的用户界面。

学习目标

  • 掌握各种高级视图组件的使用方法
  • 了解如何配置和自定义这些组件
  • 能够在实际项目中应用这些组件

核心组件

1. 日期选择器 (DatePicker)

功能说明

  • 可以只显示日期部分
  • 可以同时显示日期和时间
  • 支持多种显示样式

代码示例

DatePicker(
    "选择日期",
    selection: $selectedDate,
    displayedComponents: .date
)
.padding()
.background(.gray.opacity(0.1))
.cornerRadius(10)

2. 时间选择器 (DatePicker)

功能说明

  • 可以只显示小时和分钟
  • 支持24小时和12小时制

代码示例

DatePicker(
    "选择时间",
    selection: $selectedTime,
    displayedComponents: .hourAndMinute
)
.padding()
.background(.blue.opacity(0.1))
.cornerRadius(10)

3. 分段控件 (Picker)

功能说明

  • 使用 .segmented 样式
  • 支持多个选项
  • 可以绑定到状态变量

代码示例

Picker("选择选项", selection: .constant(0)) {
    Text("选项1").tag(0)
    Text("选项2").tag(1)
    Text("选项3").tag(2)
}
.pickerStyle(.segmented)
.padding()
.background(.green.opacity(0.1))
.cornerRadius(10)

4. 滑块 (Slider)

功能说明

  • 可以设置最小值和最大值
  • 支持步长
  • 可以显示当前值

代码示例

HStack {
    Text("音量: \(Int(progress * 100))%")
    Slider(value: $progress, in: 0...1)
}
.padding()
.background(.yellow.opacity(0.1))
.cornerRadius(10)

5. 步进器 (Stepper)

功能说明

  • 可以设置最小值、最大值和步长
  • 可以显示当前值
  • 支持自定义标签

代码示例

Stepper(
    "数量: \(Int(progress * 10))",
    value: $progress,
    in: 0...1,
    step: 0.1
)
.padding()
.background(.purple.opacity(0.1))
.cornerRadius(10)

6. 活动指示器 (ProgressView)

功能说明

  • 可以显示文本
  • 可以设置样式
  • 适合在异步操作时使用

代码示例

if isPlaying {
    ProgressView("加载中...")
        .padding()
}

7. 进度视图 (ProgressView)

功能说明

  • 可以设置当前值和总值
  • 支持动画效果
  • 适合显示下载、上传等进度

代码示例

ProgressView(value: progress)
    .padding()

Button("更新进度") {
    withAnimation {
        progress = progress < 1.0 ? progress + 0.1 : 0.0
    }
}

8. 列表分组 (List)

功能说明

  • 支持多个分组
  • 可以添加分组标题
  • 适合显示分类数据

代码示例

List {
    Section(header: Text("水果")) {
        Text("苹果")
        Text("香蕉")
        Text("橙子")
    }
    
    Section(header: Text("蔬菜")) {
        Text("西红柿")
        Text("黄瓜")
        Text("土豆")
    }
}
.frame(height: 200)

综合示例

以下是一个完整的高级视图组件演示:

struct AdvancedViewsDemo: View {
    // 状态管理
    @State private var selectedDate = Date()
    @State private var selectedTime = Date()
    @State private var isPlaying = false
    @State private var progress = 0.0
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // 标题
                Text("高级视图组件")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.blue)
                
                // 日期选择器
                VStack {
                    Text("1. 日期选择器")
                        .font(.headline)
                        .fontWeight(.semibold)
                    
                    DatePicker(
                        "选择日期",
                        selection: $selectedDate,
                        displayedComponents: .date
                    )
                    .padding()
                    .background(.gray.opacity(0.1))
                    .cornerRadius(10)
                }
                
                // 其他组件...
            }
            .padding()
        }
    }
}

最佳实践

  1. 响应式设计:确保组件在不同屏幕尺寸上都能正常显示
  2. 用户体验:为组件添加适当的标签和提示
  3. 性能优化:对于复杂列表,考虑使用 ListForEach 的性能优化技巧
  4. 可访问性:确保组件支持 VoiceOver 等辅助功能
  5. 自定义样式:根据应用的设计风格自定义组件的外观

总结

本章介绍了 SwiftUI 中的各种高级视图组件,包括日期选择器、时间选择器、分段控件、滑块、步进器、活动指示器、进度视图和列表分组。这些组件是构建现代 iOS 应用界面的重要工具,掌握它们的使用方法对于开发高质量的 SwiftUI 应用至关重要。

通过本章的学习,您应该能够:

  • 熟练使用各种高级视图组件
  • 根据实际需求配置和自定义组件
  • 构建具有良好用户体验的界面
  • 应用最佳实践来提高应用质量

参考资料


本内容为《SwiftUI 进阶学习》第一章,欢迎关注后续更新。

下标对中的最大距离

2021年5月9日 13:39

方法一:双指针

提示 $1$

考虑遍历下标对中的某一个下标,并寻找此时所有有效坐标对中距离最大的另一个下标。暴力遍历另一个下标显然不满足时间复杂度要求,此时是否存在一些可以优化寻找过程的性质?

思路与算法

不失一般性,我们遍历 $\textit{nums}_2$ 中的下标 $j$,同时寻找此时符合要求的 $\textit{nums}_1$ 中最小的下标 $i$。

假设下标 $j$ 对应的最小下标为 $i$,当 $j$ 变为 $j + 1$ 时,由于 $\textit{nums}_2$ 非递增,即 $\textit{nums}_2[j] \ge \textit{nums}_2[j+1]$,那么 $\textit{nums}_1$ 中可取元素的上界不会增加。同时由于 $\textit{nums}_1$ 也非递增,因此 $j + 1$ 对应的最小下标 $i'$ 一定满足 $i' \ge i$。

那么我们就可以在遍历 $j$ 的同时维护对应的 $i$,并用 $\textit{res}$ 来维护下标对 $(i, j)$ 的最大距离。我们将 $\textit{res}$ 初值置为 $0$,这样即使存在 $\textit{nums}_1[i] \le \textit{nums}_2[j]$ 但 $i > j$ 这种不符合要求的情况,由于此时距离为负因而不会对结果产生影响(不存在时也返回 $0$)。

另外,在维护最大距离的时候要注意下标 $i$ 的合法性,即 $i < n_1$,其中 $n_1$ 为 $\textit{nums}_1$ 的长度。

代码

###C++

class Solution {
public:
    int maxDistance(vector<int>& nums1, vector<int>& nums2) {
        int n1 = nums1.size();
        int n2 = nums2.size();
        int i = 0;
        int res = 0;
        for (int j = 0; j < n2; ++j){
            while (i < n1 && nums1[i] > nums2[j]){
                ++i;
            }
            if (i < n1){
                res = max(res, j - i);
            }
        }
        return res;
    }
};

###Python

class Solution:
    def maxDistance(self, nums1: List[int], nums2: List[int]) -> int:
        n1, n2 = len(nums1), len(nums2)
        i = 0
        res = 0
        for j in range(n2):
            while i < n1 and nums1[i] > nums2[j]:
                i += 1
            if i < n1:
                res = max(res, j - i)
        return res

###Java

class Solution {
    public int maxDistance(int[] nums1, int[] nums2) {
        int n1 = nums1.length;
        int n2 = nums2.length;
        int i = 0;
        int res = 0;
        
        for (int j = 0; j < n2; j++) {
            while (i < n1 && nums1[i] > nums2[j]) {
                i++;
            }
            if (i < n1) {
                res = Math.max(res, j - i);
            }
        }
        
        return res;
    }
}

###C#

public class Solution {
    public int MaxDistance(int[] nums1, int[] nums2) {
        int n1 = nums1.Length;
        int n2 = nums2.Length;
        int i = 0;
        int res = 0;
        
        for (int j = 0; j < n2; j++) {
            while (i < n1 && nums1[i] > nums2[j]) {
                i++;
            }
            if (i < n1) {
                res = Math.Max(res, j - i);
            }
        }
        
        return res;
    }
}

###Go

func maxDistance(nums1 []int, nums2 []int) int {
    n1 := len(nums1)
    n2 := len(nums2)
    i := 0
    res := 0
    
    for j := 0; j < n2; j++ {
        for i < n1 && nums1[i] > nums2[j] {
            i++
        }
        if i < n1 {
            if j-i > res {
                res = j - i
            }
        }
    }
    
    return res
}

###C

int maxDistance(int* nums1, int nums1Size, int* nums2, int nums2Size) {
    int i = 0;
    int res = 0;
    
    for (int j = 0; j < nums2Size; j++) {
        while (i < nums1Size && nums1[i] > nums2[j]) {
            i++;
        }
        if (i < nums1Size) {
            if (j - i > res) {
                res = j - i;
            }
        }
    }
    
    return res;
}

###JavaScript

var maxDistance = function(nums1, nums2) {
    const n1 = nums1.length;
    const n2 = nums2.length;
    let i = 0;
    let res = 0;
    
    for (let j = 0; j < n2; j++) {
        while (i < n1 && nums1[i] > nums2[j]) {
            i++;
        }
        if (i < n1) {
            res = Math.max(res, j - i);
        }
    }
    
    return res;
};

###TypeScript

function maxDistance(nums1: number[], nums2: number[]): number {
    const n1 = nums1.length;
    const n2 = nums2.length;
    let i = 0;
    let res = 0;
    
    for (let j = 0; j < n2; j++) {
        while (i < n1 && nums1[i] > nums2[j]) {
            i++;
        }
        if (i < n1) {
            res = Math.max(res, j - i);
        }
    }
    
    return res;
};

###Rust

impl Solution {
    pub fn max_distance(nums1: Vec<i32>, nums2: Vec<i32>) -> i32 {
        let n1 = nums1.len();
        let n2 = nums2.len();
        let mut i = 0;
        let mut res = 0;
        
        for j in 0..n2 {
            while i < n1 && nums1[i] > nums2[j] {
                i += 1;
            }
            if i < n1 {
                res = res.max((j as i32) - (i as i32));
            }
        }
        
        res
    }
}

复杂度分析

  • 时间复杂度:$O(n_1 + n_2)$,其中 $n_1, n_2$ 分别为 $\textit{nums}_1$ 与 $\textit{nums}_2$ 的长度。在双指针寻找最大值的过程中,我们最多会遍历两个数组各一次。

  • 空间复杂度:$O(1)$,我们使用了常数个变量进行遍历。

java 经典双指针

作者 SweetTea
2021年5月9日 12:30

双指针p1、p2指向两数组的首元素,从左向右遍历。
因为i <= j 且 nums1[i] <= nums2[j]才有效,所以nums1[p1] > nums2[p2]无效,并且p1要始终保持<=p2,
所以如果p1 == p2的时候,两个指针都向后移动一格,否则p2不动p1向后移动

class Solution {
   public int maxDistance(int[] nums1, int[] nums2) {
        int p1 = 0;
        int p2 = 0;
        int res = 0;
        while (p1 < nums1.length && p2 <nums2.length){
            if(nums1[p1] > nums2[p2]){  //无效
                if(p1 == p2){
                    p1++;
                    p2++;
                }else p1++;
            }else {     //有效
                res =Math.max(res,p2-p1);
                p2++;
            }
        }
        return res;
    }
}

双指针(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2021年5月9日 12:09

枚举 $j$,随着 $j$ 的变大,$\textit{nums}_2[j]$ 变小,满足要求的最小的 $i$ 随之变大。

所以可以用双指针做:如果 $j$ 变大后,发现 $\textit{nums}_1[i] > \textit{nums}_2[j]$,那么增大 $i$,直到 $i=n$ 或者 $\textit{nums}_1 \le \textit{nums}_2[j]$ 为止。然后用 $j-i$ 更新答案的最大值(答案初始为 $0$)。

无需担心 $j-i < 0$,这不会影响答案。

class Solution:
    def maxDistance(self, nums1: List[int], nums2: List[int]) -> int:
        ans = i = 0
        for j, y in enumerate(nums2):
            while i < len(nums1) and nums1[i] > y:
                i += 1
            if i == len(nums1):
                break
            ans = max(ans, j - i)
        return ans
class Solution {
    public int maxDistance(int[] nums1, int[] nums2) {
        int ans = 0;
        int i = 0;
        for (int j = 0; j < nums2.length; j++) {
            while (i < nums1.length && nums1[i] > nums2[j]) {
                i++;
            }
            if (i == nums1.length) {
                break;
            }
            ans = Math.max(ans, j - i);
        }
        return ans;
    }
}
class Solution {
public:
    int maxDistance(vector<int>& nums1, vector<int>& nums2) {
        int ans = 0;
        int i = 0;
        for (int j = 0; j < nums2.size(); j++) {
            while (i < nums1.size() && nums1[i] > nums2[j]) {
                i++;
            }
            if (i == nums1.size()) {
                break;
            }
            ans = max(ans, j - i);
        }
        return ans;
    }
};
int maxDistance(int* nums1, int nums1Size, int* nums2, int nums2Size) {
    int ans = 0;
    int i = 0;
    for (int j = 0; j < nums2Size; j++) {
        int y = nums2[j];
        while (i < nums1Size && nums1[i] > y) {
            i++;
        }
        if (i == nums1Size) {
            break;
        }
        ans = MAX(ans, j - i);
    }
    return ans;
}
func maxDistance(nums1, nums2 []int) (ans int) {
i := 0
for j, y := range nums2 {
for i < len(nums1) && nums1[i] > y {
i++
}
if i == len(nums1) {
break
}
ans = max(ans, j-i)
}
return
}
var maxDistance = function(nums1, nums2) {
    let ans = 0;
    let i = 0;
    for (let j = 0; j < nums2.length; j++) {
        while (i < nums1.length && nums1[i] > nums2[j]) {
            i++;
        }
        if (i === nums1.length) {
            break;
        }
        ans = Math.max(ans, j - i);
    }
    return ans;
};
impl Solution {
    pub fn max_distance(nums1: Vec<i32>, nums2: Vec<i32>) -> i32 {
        let mut ans = 0;
        let mut i = 0;
        for (j, y) in nums2.into_iter().enumerate() {
            while i < nums1.len() && nums1[i] > y {
                i += 1;
            }
            if i == nums1.len() {
                break;
            }
            ans = ans.max(j as i32 - i as i32);
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+m)$,其中 $n$ 是 $\textit{nums}_1$ 的长度,$m$ 是 $\textit{nums}_2$ 的长度。虽然我们写了个二重循环,但 $i$ 最多自增 $n$ 次,所以二重循环的循环次数至多为 $n+m$。
  • 空间复杂度:$\mathcal{O}(1)$。

专题训练

见下面双指针题单的「四、双序列双指针」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

昨天 — 2026年4月18日首页

git fetch vs git pull: What Is the Difference?

Sooner or later, every Git user runs into the same question: should you use git fetch or git pull to get the latest changes from the remote? The two commands look similar, and both talk to the remote repository, but they do very different things to your working branch.

This guide explains the difference between git fetch and git pull, how each command works, and when to use one over the other.

Quick Reference

If you want to… Use
Download remote changes without touching your branch git fetch
Download and immediately integrate remote changes git pull
Preview incoming commits before merging git fetch + git log HEAD..origin/main --oneline
Update your branch but refuse merge commits git pull --ff-only
Rebase local commits on top of remote changes git pull --rebase

What git fetch Does

git fetch downloads commits, branches, and tags from a remote repository and stores them locally under remote-tracking references such as origin/main or origin/feature-x. It does not touch your working directory, your current branch, or the index.

Terminal
git fetch origin
output
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
From github.com:example/project
4a1c2e3..9f7b8d1 main -> origin/main
a6d4c02..1e2f3a4 feature-x -> origin/feature-x

The output shows which remote branches were updated. After the fetch, the local branch main is still pointing to whatever commit it was on before. Only the remote-tracking branch origin/main has moved.

You can then inspect what changed before doing anything with it:

Terminal
git log main..origin/main

This is the safe way to look at new work without merging or rebasing yet. It gives you a preview of what the remote has that your branch does not.

What git pull Does

git pull is a convenience command. Under the hood, it runs git fetch followed by an integration step (a merge by default, or a rebase if configured). In other words:

Terminal
git pull origin main

is roughly equivalent to:

Terminal
git fetch origin
git merge origin/main

After git pull, your current branch has moved forward, and your working directory may contain new files, updated files, or merge conflicts that need resolving. The change is immediate and affects your checked-out branch.

Side-By-Side Comparison

The difference between the two commands comes down to what they change in your repository.

Aspect git fetch git pull
Downloads new commits from remote Yes Yes
Updates remote-tracking branches (origin/*) Yes Yes
Updates your current local branch No Yes
Modifies the working directory No Yes
Can create merge conflicts No Yes
Safe to run at any time Yes Only when ready to integrate

git fetch is read-only from your branch’s perspective. git pull actively changes your branch.

When to Use git fetch

Use git fetch when you want to see what changed on the remote without integrating anything yet. This is useful before you start new work, before rebasing a feature branch, or when you want to inspect a teammate’s branch safely.

For example, after fetching, you can compare your branch with the remote like this:

Terminal
git log HEAD..origin/main --oneline

This shows commits that exist on the remote but not in your current branch. Running git fetch often is cheap and has no side effects on your work, which is why many developers do it regularly throughout the day.

When to Use git pull

Use git pull when you are ready to bring remote changes into your current branch and keep working on top of them. The common flow looks like this:

Terminal
git checkout main
git pull origin main

This is the quickest way to sync a branch with its upstream when you know the integration will be straightforward, such as on a shared main branch where you rarely have local commits. git pull always updates the branch you currently have checked out, so it is worth confirming where you are before you run it.

If you want a cleaner history without merge commits, configure pull to rebase:

Terminal
git config --global pull.rebase true

From that point on, git pull runs git fetch followed by git rebase instead of git merge. Your local commits are replayed on top of the fetched changes, producing a linear history.

Avoiding Merge Surprises

The biggest reason to prefer git fetch over git pull in day-to-day work is control. A bare git pull on a branch where you have local commits can create a merge commit, introduce conflicts, or rewrite history during a rebase, all in one step.

A safer pattern is:

Terminal
git fetch origin
git log HEAD..origin/main --oneline
git merge origin/main

You fetch first, review what is coming, then decide whether to merge, rebase, or defer the integration. For active feature branches, this workflow avoids most of the “what just happened to my branch” moments.

If you already ran git pull and need to inspect what happened, start with:

Terminal
git log --oneline --decorate -n 5

This helps you confirm whether Git created a merge commit, fast-forwarded the branch, or rebased your local work.

If the pull created a merge commit that you do not want, ORIG_HEAD often points to the commit your branch was on before the pull. In that case, you can reset back to it:

Terminal
git reset --hard ORIG_HEAD
Warning
git reset --hard discards uncommitted changes. Use it only when you are sure you do not need the current state of your working tree.

Useful Flags

A few flags make both commands more predictable in real workflows.

  • git fetch --all - Fetch from every configured remote, not only origin.
  • git fetch --prune - Remove remote-tracking branches that no longer exist on the remote.
  • git fetch --tags - Download tags in addition to branches.
  • git pull --rebase - Rebase your local commits on top of the fetched changes instead of merging.
  • git pull --ff-only - Update the branch only if Git can fast-forward it; prevents unexpected merge commits.

--ff-only is especially useful on shared branches. If your branch cannot move forward by simply advancing the pointer, the pull fails and you decide what to do next.

FAQ

Does git fetch modify any local files?
No. It only updates remote-tracking references such as origin/main. Your branches, working directory, and staging area stay the same.

Is git pull the same as git fetch plus git merge?
By default, yes. With pull.rebase set to true (or --rebase on the command line), it runs git fetch followed by git rebase instead.

Which one should I use daily?
Prefer git fetch for visibility and git pull --ff-only (or git pull --rebase) for the integration step. A bare git pull works, but it hides two operations behind one command.

How do I see what git fetch downloaded?
Use git log main..origin/main to see commits on the remote that are not yet in your local branch, or git diff main origin/main to compare the content directly.

Can git fetch cause conflicts?
No. Conflicts only happen when you merge or rebase the fetched commits into your branch. Fetching itself is conflict-free.

Conclusion

git fetch lets you inspect remote changes without touching your branch, while git pull fetches and integrates those changes in one step. If you want more control and fewer surprises, fetch first, review the incoming commits, and then merge or rebase on your own terms. For more Git workflow details, see the git log command and git diff command guides.

❌
❌