普通视图
“闪电”完成2026人形机器人半马
宇树称打破人类1500米世界纪录
燃油成本飙升冲击航空业 多家航司上调费用并削减航线
C3安全大会成立多项AI安全生态联盟,华为、阿里云、ABB等参与
《swiftUI进阶 第9章SwiftUI 状态管理完全指南》
概述
状态管理是 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,仅当前视图使用 - 适合
Int、String、Bool等简单类型 - 当状态变化时,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
迁移步骤:
- 将
class SomeModel: ObservableObject改为@Observable class SomeModel - 移除所有
@Published包装器 - 将
@StateObject改为@State(如果对象是视图拥有的) - 将
@ObservedObject改为@Bindable(如果需要双向绑定) - 将
@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 应用。
参考资料
- Apple Documentation: State
- Apple Documentation: ObservableObject
- Apple Documentation: @Observable
- WWDC 2023: Discover Observation in SwiftUI
- Swift by Sundell: State management in SwiftUI
本内容为《SwiftUI 进阶》第9章,涵盖从基础到现代的全部状态管理技术。欢迎关注后续更新。
《 SwiftUI 进阶第8章:表单与设置界面》
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) // 分组样式
最佳实践
- 分组逻辑:按照功能将表单控件分组
- 标签清晰:为每个控件提供明确的标签
- 验证反馈:及时提供验证错误反馈
- 默认值:为控件设置合理的默认值
- 布局合理:使用合适的控件类型和布局
性能优化
- 避免复杂计算:不要在 body 中进行复杂计算
- 使用 @State 优化:合理使用 @State 管理表单状态
- 延迟加载:对于复杂表单,考虑使用延迟加载
与 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 官方文档 - Form
- Apple Developer Documentation: TextField
- Apple Developer Documentation: Toggle
- Apple Developer Documentation: Picker
- SwiftUI by Example: Forms
本内容为《SwiftUI 进阶》第八章,欢迎关注后续更新。
《SwiftUI 进阶第7章:导航系统》
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) // 自动样式
最佳实践
- 导航层次:保持导航层次清晰,避免过深的导航栈
- 标题设置:为每个页面设置合适的标题和显示模式
- 模态视图:合理使用 Sheet 展示临时内容
- 标签栏:控制标签数量(建议 3-5 个)
-
状态管理:使用
@State或@Observable管理导航状态
性能优化
-
延迟加载:使用
LazyView包装目标视图 - 导航栈管理:及时清理不需要的导航路径
- 避免过度动画:减少导航过程中的复杂动画
建议:
- 优先使用
NavigationStack而非NavigationView - 使用值类型传递而非对象引用
- 结合
@Observable或ObservableObject管理复杂导航状态
实战:多页面应用
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 官方文档 - NavigationStack
- Apple Developer Documentation: NavigationLink
- Apple Developer Documentation: Sheet
- Apple Developer Documentation: TabView
- Swift by Sundell: Navigation in SwiftUI
本内容为《SwiftUI 进阶》第七章,欢迎关注后续更新。
《SwiftUI 进阶第5章:数据处理与网络请求》
学习目标
- 掌握 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 中的数据处理与网络请求,包括:
-
数据模型的定义:使用
Identifiable和Decodable协议 - 本地数据处理:增删改查、列表展示
-
网络请求的实现:使用
URLSession和异步回调 - 数据状态管理:加载中、成功、失败三种状态
-
数据过滤:使用
filter方法按条件筛选 -
数据排序:使用
sorted方法自定义排序规则
通过这些技术,可以实现数据的获取、处理和展示,为应用提供丰富的数据源。在实际开发中,数据处理与网络请求是应用的核心功能之一,掌握这些技术对于开发高质量的 SwiftUI 应用至关重要。
参考资料
- SwiftUI 官方文档 - State
- Apple Developer Documentation: URLSession
- Apple Developer Documentation: JSONDecoder
- SwiftUI Networking Tutorial
- Hacking with Swift: SwiftUI networking
本内容为《SwiftUI 进阶》第五章,欢迎关注后续更新。
每日一题-下标对中的最大距离🟡
给你两个 非递增 的整数数组 nums1 和 nums2 ,数组下标均 从 0 开始 计数。
下标对 (i, j) 中 0 <= i < nums1.length 且 0 <= j < nums2.length 。如果该下标对同时满足 i <= j 且 nums1[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 <= 1051 <= nums2.length <= 1051 <= nums1[i], nums2[j] <= 105-
nums1和nums2都是 非递增 数组
《SwiftUI 进阶第4章:响应式布局》
![]()
学习目标
- 掌握 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)
}
}
动态网格布局
使用 LazyVGrid 和 GridItem 可以创建动态网格布局,根据屏幕尺寸自动调整列数。
示例代码:
// 根据水平尺寸类调整网格列数
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获取设备尺寸信息(horizontalSizeClass、verticalSizeClass) -
自适应布局:根据尺寸类调整布局结构(
VStack↔HStack) -
动态网格布局:使用
LazyVGrid和GridItem创建响应式网格 -
几何读取器:通过
GeometryReader获取父视图尺寸,实现精确布局 - 安全区域:处理状态栏、导航栏等系统 UI 遮挡问题
-
自适应文本:使用
.multilineTextAlignment()实现文本自动换行 - 条件内容:为不同设备类型显示不同的 UI 组件
- 动态间距:根据屏幕尺寸调整组件之间的间距
通过这些技术,可以创建在不同设备上都能良好显示的布局,提升用户体验。在实际开发中,响应式布局是确保应用在各种设备上都能正常显示的重要手段。
参考资料
- SwiftUI 官方文档 - Layout
- Apple Developer Documentation: GeometryReader
- Apple Developer Documentation: LazyVGrid
- Apple Developer Documentation: Environment
- WWDC 2020: Building SwiftUI Apps for All Screens
- SwiftUI Layout Essentials
本内容为《SwiftUI 进阶》第四章,欢迎关注后续更新。
《SwiftUI 进阶学习第3章:手势与交互》
手势基础
在 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 官方文档 - Gestures
- Apple Developer Documentation: TapGesture
- Apple Developer Documentation: DragGesture
- Apple Developer Documentation: MagnificationGesture
- Apple Developer Documentation: RotationGesture
- WWDC 2019: Building Custom Views with SwiftUI
本内容为《SwiftUI 进阶》第三章,欢迎关注后续更新。
《SwiftUI 进阶学习第2章:动画与过渡》
学习目标
- 掌握 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 官方文档 - Animation
- Apple Developer Documentation: withAnimation
- Apple Developer Documentation: Transition
- WWDC 2019: Building Custom Views with SwiftUI
- SwiftUI Animation Tutorial
本内容为《SwiftUI 高级教程》第二章,欢迎关注后续更新。
《SwiftUI 进阶学习第1章:高级视图组件》
![]()
概述
本章介绍 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()
}
}
}
最佳实践
- 响应式设计:确保组件在不同屏幕尺寸上都能正常显示
- 用户体验:为组件添加适当的标签和提示
-
性能优化:对于复杂列表,考虑使用
List或ForEach的性能优化技巧 - 可访问性:确保组件支持 VoiceOver 等辅助功能
- 自定义样式:根据应用的设计风格自定义组件的外观
总结
本章介绍了 SwiftUI 中的各种高级视图组件,包括日期选择器、时间选择器、分段控件、滑块、步进器、活动指示器、进度视图和列表分组。这些组件是构建现代 iOS 应用界面的重要工具,掌握它们的使用方法对于开发高质量的 SwiftUI 应用至关重要。
通过本章的学习,您应该能够:
- 熟练使用各种高级视图组件
- 根据实际需求配置和自定义组件
- 构建具有良好用户体验的界面
- 应用最佳实践来提高应用质量
参考资料
- SwiftUI DatePicker documentation
- SwiftUI Picker documentation
- SwiftUI Slider documentation
- SwiftUI Stepper documentation
- SwiftUI ProgressView documentation
本内容为《SwiftUI 进阶学习》第一章,欢迎关注后续更新。
下标对中的最大距离
方法一:双指针
提示 $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 经典双指针
双指针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)
枚举 $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)$。
专题训练
见下面双指针题单的「四、双序列双指针」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府
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.
git fetch originremote: 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:
git log main..origin/mainThis 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:
git pull origin mainis roughly equivalent to:
git fetch origin
git merge origin/mainAfter 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:
git log HEAD..origin/main --onelineThis 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:
git checkout main
git pull origin mainThis 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:
git config --global pull.rebase trueFrom 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:
git fetch origin
git log HEAD..origin/main --oneline
git merge origin/mainYou 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:
git log --oneline --decorate -n 5This 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:
git reset --hard ORIG_HEADgit 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 onlyorigin. -
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.
![]()