当 Android 手机『强行兼容』AirDrop - 肘子的 Swift 周报 #113
AirDrop 让使用者可以在各种不同类似的苹果设备上高效、无损的传输数据,它一直是苹果生态的专属且核心功能。但,这种情况现在出现了“奇怪”的变化。几天前,谷歌宣布在 Pixel 10 中,在没有苹果的参与下,为 Quick Share 提供了 AirDrop 的兼容机制,实现了安卓手机与苹果手机基于 AirDrop 的无线互通。
AirDrop 让使用者可以在各种不同类似的苹果设备上高效、无损的传输数据,它一直是苹果生态的专属且核心功能。但,这种情况现在出现了“奇怪”的变化。几天前,谷歌宣布在 Pixel 10 中,在没有苹果的参与下,为 Quick Share 提供了 AirDrop 的兼容机制,实现了安卓手机与苹果手机基于 AirDrop 的无线互通。
事件传递相关的两个方法
// 哪个视图响应事件返回哪个
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
// 点击位置是否在当前视图范围
-(BOOL)pointInside(CGPoint)point withEvent:(UIEvent *)event;
如图,View A中包含View B1、View B2,View B2中包含View C1,View C2既包含View C1的一部分,又包含View B2的一部分,View C1中包含View D。当点击View C2的空白区域时,系统如何找到事件响应者为View C2?
(1)事件传递流程当用户点击屏幕的某个位置,该事件会被传递给UIApplication,UIApplication又传递给当前的UIWindow,UIWindow会通过hitTest:WithEvent:方法返回响应的视图。hitTest:WithEvent:方法内部通过pointInside:withEvent:方法判断点击point是否在当前UIWindow范围内,如果在,则会遍历其中的所有子视图SubViews来查找最终响应此事件的视图,遍历方式为倒序遍历,即最后添加到UIWindow的视图最优先被遍历到,依次遍历,可以看作是递归调用。每个UIView中又都会调用其对应hitTest:WithEvent:方法,最终返回响应视图hit,如果hit有值,则hit视图就作为该事件的响应视图被返回,如果hit没有值,但在当前UIWindow范围内,则当前UIWindow作为事件的响应视图。
(2)hitTest:WithEvent:系统内部实现首先在hitTest:WithEvent:方法内部先判断当前视图的hidden属性、是否可交互、透明度是否大于0.01。如果该视图不同时满足上述3个条件,则返回nil,当前视图不作为事件的响应视图,当前视图的父视图继续遍历其他的子视图;如果该视图没有隐藏、用户可交互、透明度大于0.01,则会通过pointInside:WithEvent:方法判断点击的点是否在当前视图范围内,如果不在,则同样返回nil,当前视图仍不作为事件的响应者;如果在,则会通过倒序遍历当前视图的子视图,调用其子视图对应的hitTest:WithEvent:方法,如果某个视图返回了事件响应视图,则该返回的视图被作为事件的响应者,反之则继续遍历判断。如果遍历完后没有任何视图响应此事件,因为此事件点击的范围在当前视图范围内,则将当前视图作为事件响应者返回。
上述讲述了视图事件的传递流程,当视图事件传递后,最终事件由谁来响应呢,这就涉及视图的响应链、响应链的机制和流程。 如图,页面存在一个UILabel、一个UITextField、一个UIButton,实线箭头表示下一个响应者。
视图事件响应链相关的方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
例如,当点击View C2的空白处时,事件由谁来响应呢?首先由View C2接收事件,如果它不处理,就会把事件传递给View B2,如果View B2还不响应这个事件,View B2会通过响应链将事件传递给它的父视图View A,如果还不响应,则会沿着响应链一直向上传递,直到传递到UIApplicationDelegate仍然不对事件进行处理,则会忽略此事件。
自 iOS 17 起,SwiftUI 引入了 全新的 Observation 模型。
它用三个核心工具彻底重塑了数据管理方式:
@Observable —— 定义可观察的状态模型
@State —— 持有模型实例,等价于旧时代的 @StateObject
@Bindable —— 在视图中实现对 Observable 模型的双向绑定
如果你还在用 ObservableObject、@Published、@StateObject、@ObservedObject、@EnvironmentObject,是时候升级了:新范式更简单、更 Swift、更高性能。
本文将系统梳理 SwiftUI 最新的数据管理体系。
iOS 16 及以前,我们管理状态基本依赖:
ObservableObject
@Published
@StateObject
@ObservedObject
@EnvironmentObject
这些机制的问题:
装饰器太多,容易混乱
生命周期容易搞错(尤其是 @StateObject vs @ObservedObject)
@Published 对属性执行全局广播,性能不够优雅
环境写法不够类型安全
新模型的目标:让 SwiftUI 更简单、更自动、更智能。
新系统中的任何可观察模型,只要声明:
@Observable
class UserModel {
var name = "HanQiu"
var age = 23
}
不再需要:
ObservableObject
@Published
手动发布变更
所有存储属性都是可观察的,SwiftUI 会精确追踪变化来源。
在旧时代,创建页面级别持久的模型需要:
@StateObject var vm = UserModel()
在新系统中:
@State var vm = UserModel()
是的, @State 自动完成以前 @StateObject 的作用:
保持引用类型实例生命周期
在视图重建中保持稳定
触发视图刷新
只要你的模型是 @Observable 的,就可以用 @State 持有。
旧写法(子视图):
struct ProfileView: View {
@ObservedObject var vm: UserModel
}
新写法:
struct ProfileView: View {
var vm: UserModel
}
SwiftUI 会自动观察视图中“被使用的属性”。
你不需要告诉它“这个对象可观察”,它本身就知道(因为模型是 @Observable)。
旧写法:
@EnvironmentObject var settings: SettingsModel
新写法更强、更明确:
struct AppRoot: View {
@State var settings = SettingsModel()
var body: some View {
MainView()
.environment(settings)
}
}
@Environment(SettingsModel.self) var settings
减少误用,也更符合 Swift 语言本身的表达。
@Observable 模型虽然自动可观察,但 UI 控件(如 TextField)需要 双向绑定:
TextField("Name", text: $vm.name)
新模型中,属性只是普通 stored property,不是 Published,不具备 Binding 能力。
于是 Swift 引入:
为 View 提供 绑定视角的模型访问。
@Observable
class UserModel {
var name = ""
var age = 18
}
struct EditUserView: View {
@Bindable var user: UserModel
var body: some View {
Form {
TextField("Name", text: $user.name)
Stepper("Age: \(user.age)", value: $user.age)
}
}
}
只需标记 @Bindable,模型属性即可自动得到 $binding。
是否需要取决于:
| 情况 | 是否需要 @Bindable |
|---|---|
| 仅用于展示,不会修改模型 | ❌ No |
| 需要用 TextField / Toggle / Stepper 修改模型 | ✔ Yes |
| 子视图要修改父模型 | ✔ Yes |
| 完全只读视图 | ❌ No |
越“表单”风格的页面,越需要 @Bindable。
你也可以只在 body 内使用 Bindable:
var body: some View {
@Bindable var b = user // 局部绑定
VStack {
TextField("Name", text: $b.name)
Stepper("Age: \(b.age)", value: $b.age)
}
}
不会污染结构体属性定义,适合仅局部可编辑的 UI。
@Observable —— 使模型可观察
@State —— 在 View 中持有模型(生命周期 = 旧 @StateObject)
@Bindable —— 提供绑定能力,允许 UI 修改模型
一个“完整数据流”的表达式:
@Observable 定义状态 → @State 持有 → @Bindable 编辑 → SwiftUI 自动刷新
@Observable
class ProfileModel {
var name = "HanQiu"
var level = 1
}
struct ProfileView: View {
@State var profile = ProfileModel()
var body: some View {
VStack {
Text("Name: \(profile.name)")
Text("Level: \(profile.level)")
EditSection(profile: profile)
}
}
}
struct EditSection: View {
@Bindable var profile: ProfileModel
var body: some View {
VStack {
TextField("Name", text: $profile.name)
Stepper("Level: \(profile.level)", value: $profile.level)
}
.padding()
}
}
无需 @Published,不用 @StateObject,不需要 @ObservedObject。
SwiftUI 的数据管理彻底简化。
| 旧 API | 新 API |
|---|---|
| ObservableObject | @Observable |
| @Published | 不需要 |
| @StateObject | @State |
| @ObservedObject | 删除,直接传模型 |
| @EnvironmentObject | .environment(model) + @Environment(Model.self) |
| 双向绑定属性 | 使用 @Bindable |
SwiftUI 从 iOS17 开始进入 Observation 时代:
@Observable → 自动观察
@State → 管理模型生命周期
@Bindable → 构建表单/编辑 UI 的关键
更少的装饰器
更精准的性能优化
更符合 Swift 语言设计哲学
如果你写 SwiftUI,这套新范式未来几年都会是主流。