普通视图
iOS Audio后台模式下能否执行非Audio逻辑
测试设备:iPhone 13mini / iOS 26
验证方法
- 开启
Audio, AirPlay, and Picture in Picture模式,播放声音 - 执行与声音无关的代码和网络请求逻辑,并写入日志文件,代码如下所示
- 让App进入后台,App持续播放声音,过一段时间(5分钟)回到App前台,检查日志文件中是否预期内容
private func test() {
var sum = 0
timer?.invalidate()
timer = YYTimer(timeInterval: 1, repeats: true, block: { t in
sum += 1
// 验证代码执行
DDLogInfo("[BackgroundTest] Timer fired. Sum: \(sum)")
// 验证网络请求
let url = URL(string: "https://www.baidu.com")!
URLSession.shared.dataTask(with: url) { _, response, error in
if let error = error {
DDLogInfo("[BackgroundTest] Network failed: \(error.localizedDescription)")
} else if let httpResponse = response as? HTTPURLResponse {
DDLogInfo("[BackgroundTest] Network success. Status: \(httpResponse.statusCode)")
}
}.resume()
})
}
日志结果:
2025-11-27 10:23:13.575 [INFO] [BackgroundTest] Timer fired. Sum: 5
2025-11-27 10:23:13.612 [INFO] [BackgroundTest] Network success. Status: 200
2025-11-27 10:23:14.008 [INFO] Report OnlineStatus background, badge number 1 // 进入后台
2025-11-27 10:23:14.582 [INFO] [BackgroundTest] Timer fired. Sum: 6
2025-11-27 10:23:14.612 [INFO] [BackgroundTest] Network success. Status: 200
2025-11-27 10:23:15.586 [INFO] [BackgroundTest] Timer fired. Sum: 7
2025-11-27 10:23:15.622 [INFO] [BackgroundTest] Network success. Status: 200
.....//省略
2025-11-27 10:28:19.578 [INFO] [BackgroundTest] Timer fired. Sum: 311
2025-11-27 10:28:19.610 [INFO] [BackgroundTest] Network success. Status: 200
2025-11-27 10:28:20.579 [INFO] [BackgroundTest] Timer fired. Sum: 312
2025-11-27 10:28:20.611 [INFO] [BackgroundTest] Network success. Status: 200
2025-11-27 10:28:21.574 [INFO] [BackgroundTest] Timer fired. Sum: 313
2025-11-27 10:28:21.602 [INFO] [BackgroundTest] Network success. Status: 200
2025-11-27 10:28:22.449 [INFO] Report OnlineStatus foreground // 回到前台
2025-11-27 10:28:22.573 [INFO] [BackgroundTest] Timer fired. Sum: 314
2025-11-27 10:28:22.600 [INFO] [BackgroundTest] Network success. Status: 200
结论
- 开启
Audio, AirPlay, and Picture in Picture模式,开启声音,进入后台,可以正常执行与声音无关的代码逻辑和网络请求
注:仅为测试结果,不排除系统会做任何形式的拦截或中断
当我决定同时做 iOS 和 Android:独立开发者的真实双平台之路
这是一位独立开发者跨上双平台之路的完整记录:从 iOS 的舒适区,到 Android 的碎片化现实;从协作模式、交互差异,到商店后台、支付体系和中国安卓生态的真实挑战。产品在变,他的理解和心态也在变。或许能让仍只在苹果生态中的你看到另一条可能的路径。
SwiftUI 手势冲突:修复 Navigation 返回手势
欢迎大家给我点个 star!Github: RickeyBoy
问题背景
在开发过程中遇到一个体验上的冲突问题,当用户在使用可横向翻页的视图(如 TabView 的 page 样式)时,第一页无法从屏幕边缘滑动返回上一页。返回手势总是被 TabView 的手势拦截,具体表现可以看下面这个 gif 图:
![]()
原因分析
为什么会这样?
- 手势竞争问题:
- Navigation Controller:提供边缘滑动返回手势
- TabView:拥有用于页面切换的横向拖动手势
2. 优先级冲突:
- 两个手势都识别横向滑动
- TabView 的手势先捕获触摸
- Navigation 手势永远没有机会响应
SwiftUI 的局限性
SwiftUI 没有内置的方式来协调这些手势,解决冲突,所以我们必须深入到 UIKit,自行解决冲突。
如何解决
关键点:在第一页时,我们需要两个手势同时激活,但响应不同的方向:
- 向右滑动(从左边缘) → Navigation 返回手势
- 向左滑动 → TabView 翻页
当然,这个要实现上述的逻辑,需要通过 UIKit 来进行手势冲突的逻辑处理。
解决方案
步骤 1:识别手势
获取到互相冲突的两个手势:
-
Navigation Gesture:位于
UINavigationController.interactivePopGestureRecognizer - Content Gesture:位于可滚动内容上(如 UIScrollView.panGestureRecognizer)
.introspect(.viewController, on: .iOS(.v16, .v17, .v18)) { viewController in
guard let navigationController = viewController.navigationController,
let interactivePopGesture = navigationController.interactivePopGestureRecognizer else {
return
}
coordinator.configure(with: interactivePopGesture)
}
.introspect(.scrollView, on: .iOS(.v16, .v17, .v18)) { scrollView in
coordinator.conflictingGesture = scrollView.panGestureRecognizer
}
步骤 2:创建 Coordinator
构建一个实现 UIGestureRecognizerDelegate 的 Coordinator,他的职责如下:
- 存储两个手势
- 通过 Delegate 回调管理它们的交互
- 处理生命周期(设置和清理)
public final class NavigationSwipeBackCoordinator: NSObject, UIGestureRecognizerDelegate {
/// Closure that determines whether swipe-back should be enabled
public var shouldEnableSwipeBack: (() -> Bool)?
/// The conflicting gesture that should work simultaneously
public weak var conflictingGesture: UIPanGestureRecognizer?
private weak var interactivePopGesture: UIGestureRecognizer?
private weak var originalDelegate: UIGestureRecognizerDelegate?
public func configure(with gesture: UIGestureRecognizer) {
guard interactivePopGesture == nil else { return }
interactivePopGesture = gesture
originalDelegate = gesture.delegate
gesture.delegate = self
}
// ... cleanup and delegate methods
}
步骤 3:启用同时识别 RecognizeSimultaneously
实现 gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:):
- 当两个手势需要同时工作时返回 true
- 允许两者检测触摸而不会互相拦截
public func gestureRecognizer(
_: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
// Only allow simultaneous recognition with the conflicting gesture we're managing
return otherGestureRecognizer == conflictingGesture
}
步骤 4:添加条件逻辑
实现 gestureRecognizerShouldBegin(_:):
- 检查当前状态(例如检查是否位于第一页)
- 只在适当的时候允许 Navigation 手势
- 在用户应该滚动内容时阻止返回手势
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
return true
}
// Check swipe direction
let translation = panGesture.translation(in: panGesture.view)
let velocity = panGesture.velocity(in: panGesture.view)
let isSwipingRight = translation.x > 0 || velocity.x > 0
// Only allow back gesture for right swipes
guard isSwipingRight else { return false }
// Check app-specific condition (e.g., "am I on the first page?")
return shouldEnableSwipeBack?() ?? false
}
步骤 5:管理生命周期
- 设置:保存原始状态,安装自定义 Delegate
- 清理:恢复原始状态以避免副作用
public func cleanup() {
interactivePopGesture?.delegate = originalDelegate
interactivePopGesture = nil
originalDelegate = nil
shouldEnableSwipeBack = nil
conflictingGesture = nil
}
步骤 6:封装为 SwiftUI Modifier
创建可复用的 ViewModifier:
- 封装所有 UIKit 复杂性
- 提供简洁的 SwiftUI API
- 响应式更新状态
public extension View {
func enableNavigationSwipeBack(when condition: @escaping () -> Bool) -> some View {
modifier(NavigationSwipeBackModifier(shouldEnable: condition))
}
}
// Usage
.enableNavigationSwipeBack(when: { selectedIndex == 0 })
实现模式
┌─────────────────────────────────────┐
│ SwiftUI View │
│ .enableSwipeBack(when: condition) │
└────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ ViewModifier │
│ - Manages lifecycle │
│ - Updates condition reactively │
└────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Gesture Coordinator │
│ - Implements delegate callbacks │
│ - Coordinates both gestures │
│ - Stores original state │
└─────────────────────────────────────┘
使用方法
在任何会阻止 Navigation 返回手势的横向滑动视图上,应用 enableNavigationSwipeBack modifier。
基本语法
.enableNavigationSwipeBack(when: { condition })
when 闭包用于判断何时应该启用返回手势。它在手势开始时实时计算,确保能响应最新的状态。
示例:分页 TabView
TabView(selection: $selection) {
ForEach(items) { item in
ItemView(item: item)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.enableNavigationSwipeBack(when: { selectedItemIndex == 0 })
注意:此方案需要 SwiftUIIntrospect 库来访问底层 UIKit 视图。
效果
当用户位于第一页时,自动允许边缘滑动返回手势
![]()
SwiftUI快速入门指南-Modifier篇
背景
本文帮助有Swift基础的同学,快速入门SwiftUI,基于cursour整理
主要分为四个部分:
- 关键字
- Modifier
- 布局
- Viewbuilder
1. 什么是 Modifier?
Modifier 是用于修改视图外观和行为的方法。每个 modifier 都会返回一个新的视图。
Text("Hello")
.font(.title) // 修改字体
.foregroundColor(.blue) // 修改颜色
.padding() // 添加内边距
.background(.yellow) // 添加背景
核心概念:
- ✅ Modifier 不修改原视图,而是创建新视图
- ✅ 支持链式调用
- ✅ 顺序很重要!
2. Modifier 分类
A. 文本 Modifier
Text("SwiftUI Modifier")
// 字体
.font(.title)
.font(.system(size: 24, weight: .bold, design: .rounded))
.fontWeight(.semibold)
// 颜色
.foregroundColor(.blue)
.foregroundStyle(.red)
// 样式
.italic()
.bold()
.underline()
.strikethrough()
.kerning(2) // 字间距
.tracking(3) // 字符间距
.baselineOffset(5) // 基线偏移
// 多行
.lineLimit(3)
.lineSpacing(8)
.multilineTextAlignment(.center)
.truncationMode(.tail)
B. 布局 Modifier
VStack {
Text("布局示例")
}
// 内边距
.padding()
.padding(.horizontal, 20)
.padding(.top, 10)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20))
// 尺寸
.frame(width: 200, height: 100)
.frame(minWidth: 100, maxWidth: .infinity)
.frame(maxHeight: 300)
// 对齐
.frame(width: 300, height: 200, alignment: .topLeading)
// 偏移
.offset(x: 10, y: 20)
// 位置
.position(x: 100, y: 100)
C. 背景和边框 Modifier
Text("样式示例")
// 背景
.background(.blue)
.background(Color.blue.opacity(0.3))
.background(
LinearGradient(
colors: [.blue, .purple],
startPoint: .leading,
endPoint: .trailing
)
)
// 边框
.border(.red, width: 2)
// 圆角边框
.cornerRadius(10)
.clipShape(RoundedRectangle(cornerRadius: 15))
.clipShape(Circle())
// 描边
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.red, lineWidth: 2)
)
D. 阴影和效果 Modifier
Text("效果示例")
// 阴影
.shadow(radius: 5)
.shadow(color: .gray, radius: 10, x: 5, y: 5)
// 模糊
.blur(radius: 3)
// 透明度
.opacity(0.8)
// 旋转
.rotationEffect(.degrees(45))
.rotation3DEffect(.degrees(45), axis: (x: 1, y: 0, z: 0))
// 缩放
.scaleEffect(1.5)
.scaleEffect(x: 1.2, y: 0.8)
E. 交互 Modifier
Text("点击我")
// 点击
.onTapGesture {
print("被点击了")
}
.onTapGesture(count: 2) {
print("双击")
}
// 长按
.onLongPressGesture {
print("长按")
}
// 拖拽
.gesture(
DragGesture()
.onChanged { value in
print("拖拽中")
}
)
// 禁用
.disabled(true)
F. 生命周期 Modifier
Text("生命周期")
// 出现
.onAppear {
print("视图出现")
}
// 消失
.onDisappear {
print("视图消失")
}
// 值变化
.onChange(of: someValue) { oldValue, newValue in
print("值改变了")
}
// 任务
.task {
await loadData()
}
3. Modifier 顺序的重要性 ⚠️
这是最容易出错的地方!Modifier 的顺序会产生完全不同的结果。
// 示例 1: 先 padding 后 background
Text("Hello")
.padding(20) // 先添加内边距
.background(.blue) // 背景覆盖整个区域(包括 padding)
// 结果:蓝色背景包含文字和内边距
// 示例 2: 先 background 后 padding
Text("Hello")
.background(.blue) // 背景只覆盖文字
.padding(20) // 在背景外添加内边距
// 结果:蓝色背景只包含文字,外面有空白
// 边框和圆角
Text("示例")
.padding()
.background(.blue)
.cornerRadius(10) // ✅ 正确:圆角应用到背景
.border(.red, width: 2) // 边框在圆角外
Text("示例")
.padding()
.cornerRadius(10) // ❌ 错误:圆角应用到文字(没效果)
.background(.blue)
.border(.red, width: 2)
// Frame 和 Background
Text("示例")
.frame(width: 200, height: 100)
.background(.blue) // ✅ 蓝色填满整个 frame
Text("示例")
.background(.blue)
.frame(width: 200, height: 100) // ❌ 蓝色只在文字周围
4. 自定义 Modifier
方法一:使用 ViewModifier 协议
// 定义自定义 modifier
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.white)
.cornerRadius(10)
.shadow(color: .gray.opacity(0.4), radius: 5, x: 0, y: 2)
}
}
// 扩展 View 以便于使用
extension View {
func cardStyle() -> some View {
self.modifier(CardModifier())
}
}
// 使用
Text("卡片样式")
.cardStyle()
方法二:直接扩展 View
extension View {
func primaryButton() -> some View {
self
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(.blue)
.cornerRadius(10)
}
}
// 使用
Text("登录")
.primaryButton()
带参数的自定义 Modifier
struct BorderModifier: ViewModifier {
var color: Color
var width: CGFloat
var cornerRadius: CGFloat
func body(content: Content) -> some View {
content
.padding()
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(color, lineWidth: width)
)
}
}
extension View {
func customBorder(
color: Color = .blue,
width: CGFloat = 2,
cornerRadius: CGFloat = 8
) -> some View {
self.modifier(BorderModifier(
color: color,
width: width,
cornerRadius: cornerRadius
))
}
}
// 使用
Text("自定义边框")
.customBorder(color: .red, width: 3, cornerRadius: 15)
5. 条件 Modifier
// 方法一:使用 @ViewBuilder
extension View {
@ViewBuilder
func `if`<Transform: View>(
_ condition: Bool,
transform: (Self) -> Transform
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
// 使用
Text("条件样式")
.if(isHighlighted) { view in
view
.font(.largeTitle)
.foregroundColor(.red)
}
// 方法二:三元运算符
Text("示例")
.foregroundColor(isActive ? .blue : .gray)
.font(isLarge ? .title : .body)
// 方法三:使用 modifier
struct ConditionalModifier: ViewModifier {
var condition: Bool
func body(content: Content) -> some View {
if condition {
content
.background(.yellow)
.cornerRadius(10)
} else {
content
.background(.gray)
}
}
}
// 使用
Text("条件")
.modifier(ConditionalModifier(condition: isSpecial))
6. 组合 Modifier 实战示例
struct ProfileCard: View {
@State private var isLiked = false
var body: some View {
VStack(spacing: 12) {
// 头像
Image(systemName: "person.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 80, height: 80)
.foregroundColor(.blue)
.clipShape(Circle())
.overlay(
Circle()
.stroke(.gray, lineWidth: 2)
)
.shadow(radius: 5)
// 名字
Text("张三")
.font(.title2)
.fontWeight(.bold)
// 描述
Text("iOS 开发工程师")
.font(.subheadline)
.foregroundColor(.secondary)
// 按钮
Button(action: { isLiked.toggle() }) {
HStack {
Image(systemName: isLiked ? "heart.fill" : "heart")
Text(isLiked ? "已关注" : "关注")
}
.font(.headline)
.foregroundColor(isLiked ? .red : .white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(isLiked ? .white : .blue)
.cornerRadius(20)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(isLiked ? .red : .blue, lineWidth: 2)
)
}
}
.padding(20)
.background(.white)
.cornerRadius(15)
.shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
.padding()
}
}
7. 常用 Modifier 组合模板
extension View {
// 卡片样式
func card() -> some View {
self
.padding()
.background(.white)
.cornerRadius(12)
.shadow(color: .gray.opacity(0.3), radius: 8, x: 0, y: 4)
}
// 主按钮样式
func primaryButtonStyle() -> some View {
self
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(
LinearGradient(
colors: [.blue, .purple],
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(10)
.shadow(radius: 5)
}
// 输入框样式
func textFieldStyle() -> some View {
self
.padding()
.background(.gray.opacity(0.1))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(.gray.opacity(0.5), lineWidth: 1)
)
}
// 标签样式
func tag(color: Color = .blue) -> some View {
self
.font(.caption)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(color.opacity(0.2))
.foregroundColor(color)
.cornerRadius(12)
}
}
// 使用示例
struct ContentView: View {
@State private var email = ""
var body: some View {
VStack(spacing: 20) {
// 卡片
VStack {
Text("用户信息")
Text("详细内容")
}
.card()
// 输入框
TextField("邮箱", text: $email)
.textFieldStyle()
// 按钮
Text("登录")
.primaryButtonStyle()
// 标签
HStack {
Text("热门").tag(color: .red)
Text("新品").tag(color: .green)
Text("推荐").tag(color: .blue)
}
}
.padding()
}
}
8. 高级 Modifier 技巧
A. 环境 Modifier
影响所有子视图:
VStack {
Text("标题")
Text("副标题")
Text("内容")
}
.font(.title) // 所有子视图都使用 title 字体
.foregroundColor(.blue) // 所有子视图都是蓝色
B. 几何读取器配合 Modifier
GeometryReader { geometry in
Text("响应式")
.frame(width: geometry.size.width * 0.8)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
}
C. 动画 Modifier
struct AnimatedView: View {
@State private var isExpanded = false
var body: some View {
RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)
.fill(isExpanded ? .blue : .red)
.frame(width: isExpanded ? 200 : 100, height: 100)
.animation(.spring(response: 0.5, dampingFraction: 0.6), value: isExpanded)
.onTapGesture {
isExpanded.toggle()
}
}
}
9. Modifier 最佳实践
✅ 推荐做法
// 1. 提取重复的 modifier 为自定义 modifier
extension View {
func standardCard() -> some View {
self
.padding()
.background(.white)
.cornerRadius(10)
.shadow(radius: 5)
}
}
// 2. 注意顺序
Text("示例")
.padding()
.background(.blue)
.cornerRadius(10) // 正确顺序
// 3. 使用语义化命名
extension View {
func errorStyle() -> some View {
self.foregroundColor(.red).bold()
}
func successStyle() -> some View {
self.foregroundColor(.green).bold()
}
}
❌ 避免做法
// 1. 避免过长的 modifier 链
Text("Bad")
.font(.title).foregroundColor(.blue).padding().background(.yellow).cornerRadius(10).shadow(radius: 5).opacity(0.9)
// 太长了!应该换行
// 2. 避免重复代码
Text("Button 1")
.padding()
.background(.blue)
.cornerRadius(10)
Text("Button 2")
.padding()
.background(.blue)
.cornerRadius(10)
// 应该提取为自定义 modifier
// 3. 避免错误的顺序
Text("Wrong")
.cornerRadius(10) // 错误:在 background 之前
.background(.blue)
总结
Modifier 核心要点:
- ✅ Modifier 创建新视图,不修改原视图
- ✅ 顺序非常重要
- ✅ 支持链式调用
- ✅ 可以自定义和复用
- ✅ 使用语义化命名
- ✅ 注意性能(避免过度嵌套)
SwiftUI快速入门指南-关键字篇
背景
本文帮助有Swift基础的同学,快速入门SwiftUI,基于cursour整理
主要分为四个部分:
- 关键字
- Modifier
- 布局
- Viewbuilder
Some
some 表示"某个特定的类型,该类型遵循某个协议"。它的特点是:
- 隐藏具体类型:调用者不知道具体是什么类型,只知道它遵循某个协议
- 类型固定:返回的始终是同一个具体类型(编译器知道)
- 类型推断:编译器会自动推断出具体类型
some vs any 核心区别
| 特性 | some | any |
|---|---|---|
| 类型确定 | 编译时确定,固定不变 | 运行时可变 |
| 性能 | 快(静态派发) | 慢(动态派发,有装箱开销) |
| 类型一致性 | 必须始终返回同一类型 | 可以返回不同类型 |
| 引入版本 | Swift 5.1 | Swift 5.6 |
| 使用场景 | 返回类型、属性 | 需要类型灵活性时 |
// some - 固定的具体类型
func makeSomeView() -> some View {
Text("Hello") // 每次调用都返回 Text 类型
}
// any - 可以是任何符合协议的类型
func makeAnyView(condition: Bool) -> any View {
if condition {
return Text("Hello") // 这次返回 Text
} else {
return Image("icon") // 下次可能返回 Image
}
}
关键字
| 属性包装器 | 用途 | 拥有数据 | 数据类型 | 典型场景 |
|---|---|---|---|---|
| @State | 当前View状态处理 | ✅ 是 | 值类型 | 简单的 UI 状态 |
| @Binding | 父子View间状态传递 | ❌ 否 | 任意 | 子视图修改父状态 |
| @StateObject | 当前View引用对象,对象的生命周期在当前View | ✅ 是 | 引用类型 | 视图的 ViewModel |
| @ObservedObject | 父子View间对象状态传递,对象在父View | ❌ 否 | 引用类型 | 传入的对象 |
| @EnvironmentObject | 跨View间状态传递 | ❌ 否 | 引用类型 | 全局共享数据 |
| @Environment | 系统环境 | ❌ 否 | 系统提供 | 系统设置和服务 |
1. @State - 私有状态 用于管理视图内部的简单值类型状态。
struct CounterView: View {
@State private var count = 0
@State private var isOn = false
@State private var name = ""
var body: some View {
VStack {
Text("计数: \(count)")
Button("增加") {
count += 1 // 修改会触发视图刷新
}
Toggle("开关", isOn: $isOn)
TextField("姓名", text: $name)
}
}
}
特点:
- ✅ 用于值类型(Int, String, Bool, struct 等)
- ✅ 视图拥有这个状态
- ✅ 声明为 private
- ✅ SwiftUI 管理其生命周期
- ✅ 修改会自动刷新视图
2. @Binding - 双向绑定
创建对父视图状态的双向绑定。
struct ParentView: View {
@State private var isPresented = false
var body: some View {
VStack {
Button("显示") {
isPresented = true
}
// 传递绑定
ChildView(isPresented: $isPresented)
}
}
}
struct ChildView: View {
@Binding var isPresented: Bool // 绑定到父视图的状态
var body: some View {
Toggle("显示状态", isOn: $isPresented)
// 修改会同步到父视图
}
}
特点:
- ✅ 创建双向连接
- ✅ 子视图可以读写父视图的状态
- ✅ 使用 $ 传递绑定
- ✅ 不拥有数据
3. @StateObject - 引用类型的拥有者
用于创建和拥有 ObservableObject 实例
// 1. 创建可观察对象
class ViewModel: ObservableObject {
@Published var items: [String] = []
@Published var isLoading = false
func loadData() {
isLoading = true
// 加载数据...
items = ["Item 1", "Item 2"]
isLoading = false
}
}
// 2. 在视图中使用
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
List(viewModel.items, id: \.self) { item in
Text(item)
}
.onAppear {
viewModel.loadData()
}
}
}
3. @ObservedObject - 引用类型的观察者
用于观察已存在的 ObservableObject(不拥有)。
class ViewModel: ObservableObject {
@Published var count = 0
}
struct ParentView: View {
@StateObject private var viewModel = ViewModel() // 拥有
var body: some View {
ChildView(viewModel: viewModel) // 传递
}
}
struct ChildView: View {
@ObservedObject var viewModel: ViewModel // 观察(不拥有)
var body: some View {
VStack {
Text("计数: \(viewModel.count)")
Button("增加") {
viewModel.count += 1
}
}
}
}
特点:
- ✅ 观察从外部传入的对象
- ❌ 不拥有对象
- ⚠️ 视图重建时可能导致对象重新初始化(如果使用不当)
5. @EnvironmentObject - 环境对象 在视图层级中共享对象,无需逐层传递。
class UserSettings: ObservableObject {
@Published var username = "Guest"
@Published var isDarkMode = false
}
@main
struct MyApp: App {
@StateObject private var settings = UserSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings) // 注入
}
}
}
struct ContentView: View {
@EnvironmentObject var settings: UserSettings // 自动获取
var body: some View {
VStack {
Text("用户: \(settings.username)")
SettingsView() // 子视图也能访问
}
}
}
struct SettingsView: View {
@EnvironmentObject var settings: UserSettings // 直接访问
var body: some View {
Toggle("深色模式", isOn: $settings.isDarkMode)
}
}
特点:
- ✅ 跨层级共享数据
- ✅ 无需逐层传递
- ⚠️ 如果未注入会崩溃
- ✅ 适合全局状态(用户设置、主题等)
6. @Environment - 系统环境值
访问 SwiftUI 提供的系统环境值。
struct MyView: View {
@Environment(\.colorScheme) var colorScheme // 深色/浅色模式
@Environment(\.dismiss) var dismiss // 关闭动作
@Environment(\.horizontalSizeClass) var sizeClass // 尺寸类别
var body: some View {
VStack {
Text("当前模式: \(colorScheme == .dark ? "深色" : "浅色")")
Button("关闭") {
dismiss()
}
}
}
}
常用环境值:
- .colorScheme - 颜色方案
- .dismiss - 关闭当前视图
- .horizontalSizeClass / .verticalSizeClass - 尺寸类别
- .locale - 本地化
- .accessibilityEnabled - 辅助功能
最佳实践
// 1. 简单值用 @State
@State private var count = 0
// 2. 创建对象用 @StateObject
@StateObject private var viewModel = ViewModel()
// 3. 传递对象用 @ObservedObject
@ObservedObject var viewModel: ViewModel
// 4. 传递绑定用 @Binding
@Binding var isPresented: Bool
// 5. 全局共享用 @EnvironmentObject
@EnvironmentObject var settings: AppSettings