NotificationCenter 是 iOS macOS 中最经典的 发布订阅 通信机制:
一个对象发送通知(Post),任意多个对象接收通知(Observer)。
SwiftUI 虽然有 @State、@Observable、Environment 等现代数据流方式,但 NotificationCenter 仍然非常有用
NotificationCenter.default.publisher() 返回创建的
NotificationCenter.Publisher。
post()
NotificationCenter
│
┌──────┴──────┐
│ │
Observer A Observer B
│ │
收到通知 收到通知import SwiftUI
// MARK: - 通知名称
extension Notification.Name {
static let userDidLogin = Notification.Name("userDidLogin")
}
// MARK: - 数据模型
struct User {
let name: String
}
struct ContentView: View {
@State private var status = "未登录"
@State private var username = "-"
@State private var age = 0
var body: some View {
VStack(spacing: 20) {
Text("状态:\(status)")
.font(.title2)
Text("用户名:\(username)")
Text("年龄:\(age)")
Divider()
Button("发送登录通知(object + userInfo)") {
let user = User(name: "Tom")
NotificationCenter.default.post(
name: .userDidLogin,
object: user,
userInfo: [
"username": "Tom",
"age": 18
]
)
}
}
.padding()
// 接收通知
.onReceive(
NotificationCenter.default.publisher(
for: .userDidLogin
)
) { notification in
print("收到通知")
// 读取 object
if let user = notification.object as? User {
print("object -> \(user.name)")
status = "\(user.name) 登录成功"
}
// 读取 userInfo
if let info = notification.userInfo {
if let name = info["username"] as? String {
username = name
print("username -> \(name)")
}
if let age = info["age"] as? Int {
self.age = age
print("age -> \(age)")
}
}
}
}
}
#Preview {
ContentView()
}整个流程图
Button
│
▼
NotificationCenter.default.post(...)
│
┌────────────┴────────────┐
│ │
object userInfo
User(name:"Tom") ["username":"Tom",
"age":18]
│ │
└────────────┬────────────┘
▼
NotificationCenter.default.publisher(...)
│
.onReceive()
│
┌────────────────┴────────────────┐
│ │
notification.object notification.userInfo
│ │
User(name:"Tom") username、age
│ │
└────────────────┬────────────────┘
▼
更新 @State
│
▼
SwiftUI 自动刷新界面例如:
import SwiftUI
struct ContentView: View {
@Environment(\.scenePhase)
private var scenePhase
@State private var logs: [String] = []
var body: some View {
NavigationStack {
List(logs.reversed(), id: \.self) { log in
Text(log)
}
.navigationTitle("App 生命周期 Demo")
}
// MARK: NotificationCenter
// App 已进入后台
.onReceive(
NotificationCenter.default.publisher(
for: UIApplication.didEnterBackgroundNotification
)
) { _ in
addLog("Notification:进入后台")
}
// App 即将进入前台
.onReceive(
NotificationCenter.default.publisher(
for: UIApplication.willEnterForegroundNotification
)
) { _ in
addLog("Notification:即将进入前台")
}
// App 已经激活
.onReceive(
NotificationCenter.default.publisher(
for: UIApplication.didBecomeActiveNotification
)
) { _ in
addLog("Notification:App Active")
}
// MARK: scenePhase
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
addLog("scenePhase:active")
case .inactive:
addLog("scenePhase:inactive")
case .background:
addLog("scenePhase:background")
@unknown default:
addLog("scenePhase:unknown")
}
}
}
private func addLog(_ text: String) {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let time = formatter.string(from: Date())
logs.append("[\(time)] \(text)")
print("[\(time)] \(text)")
}
}
#Preview {
ContentView()
}NotificationCenter.default.publisher()
│
▼
NotificationCenter.Publisher
│
├──────────────► .onReceive
│
├──────────────► .map(...)
│
├──────────────► .compactMap(...)
│
├──────────────► .filter(...)
│
├──────────────► .dropFirst(...)
│
├──────────────► .debounce(...)
│
└──────────────► .throttle(...)移动应用程序的UI对用户无响应是绝不可接受的,但有时你需要执行一些可能需要花费超过几毫秒的操作。 最显着的是……访问网络。 另一个例子……进行一些非常消耗CPU的分析,例如机器学习。当这些长期运行的任务在进行时,我们如何保持UI的响应能力?
我们在一个与UI正在运行的线程不同的“执行线程”上执行它们。
大多数现代操作系统(包括 iOS)都允许你指定要使用的“执行线程”。这些线程看起来似乎都在同时执行它们的代码。
去追踪什么代码在什么时候、什么地方运行,可能会非常棘手且艰难。在 Swift 中,系统在幕后为我们管理着这些执行线程!
这使得作为 Swift 程序员的我们,工作变得远没有那么棘手和艰难。但我们必须给系统提供一些关于它应该如何做到这一点的提示。
我们了解了使用线程来实现并发的好处,但其潜在的陷阱是什么?
数据竞争/数据竞态。在多个线程上同时进行读写的一块数据可能会遭到损坏。Swift 是如何帮助你确保这种情况绝不发生的?
Swift 通过又一种新的数据类型来同步对数据的访问:actor。
actor 是一种引用类型(就像类 class 一样),但它不支持继承。因此,除了它具备引用类型的语义之外,它看起来非常像一个结构体 struct。但是 actor 最主要的“核心本领”在于,它能够同步对其内部所有变量(vars)和函数(funcs)的访问。如果你不进行多线程编程,你永远不需要创建自己的 actor。如果你的多线程操作需要与 UI 协同工作,你可以通过 MainActor 来同步你的代码。
那么,一个 actor 是如何跨多个执行线程同步所有数据访问的呢?
首先,在任何给定的时间,actor 内部仅能有一个函数(或变量访问)处于运行状态。其次,所有函数都会运行至完毕,除非它们被挂起(以便稍后重新恢复执行)。由于绝不会有两个函数同时运行,因此 actor 的数据不会发生任何冲突。并且由于它们总是运行至完毕,或者运行到有明确标记的挂起点,因此它是可预测的。这就是同步!
我们称运行在某个 actor 上的函数或变量,对该 actor 是隔离的(isolated)。很多代码都是非隔离的(nonisolated),即不绑定到任何 actor。例如,你的数据模型(Model)在大部分时间里可能都是非隔离运行的。但 UI 中的代码总是隔离在一个被称为 MainActor 的 actor 中。
为了确保可预测性,你必须让 Swift 知道什么时候可能会发生挂起。因此,你必须做的一件事就是,使用关键字 async 来标记任何可以被挂起的函数。
因此,可以被挂起的函数被称为“异步函数(async functions)”。执行诸如网络访问等操作的函数,几乎总是异步(async)的。
因为它们在等待网络数据返回的过程中是可以被挂起的。async 是一种声明,表示该函数内部“可能”会发生挂起,而不是说一定会发生。
一个非常耗时的函数可能会将自己标记为 async,即使它并不会真正发生挂起。将一个函数标记为 async,会强制调用者去注意这件事
当你调用一个 async(异步)函数时,你必须用关键字 await 来标记该调用。当你调用一个与你处于不同 actor 中的函数时,你同样需要使用 await。
因为当你调用时,该 actor 可能正在另一个线程上执行它的其他代码。这样做是合理的,因为你将会等待(await)该异步(async)操作的完成。
关键问题在于:当一个函数执行到带有 await 的代码行时,它可能会被挂起!事实上,一个 actor 中的函数会被挂起的唯一情况,就是它在 await 另一个调用。 否则,该函数会一直运行至完毕。
当一个 actor 内部的函数被挂起时,该 actor 中的其他代码是被允许运行的。你只能在处于异步执行上下文的代码中使用 await。这意味着你要么处于一个自身被标记为 async 的函数中,要么你处于一个 Task 闭包中。
await 存在的全部原因,就是为了提醒你自己:你可能会在这里被挂起。你无法预知自己在那个 await 处会被挂起多长时间。 它可能是一段非常长的时间(或者也可能是零延迟)。并且当你恢复执行时,你所在的 actor 中的其他函数在此期间可能已经遭到调用了。
确保你考虑到这种可能性,是属于你自己的职责。
如果一个 actor 将一个参数传递给另一个 actor,并且它们都对该参数进行修改会怎么样?
那绝对会引发并发问题!
因此,如果你想跨越 actor 的隔离边界来传递参数,那么这些函数参数需要是 Sendable 的(即遵循了 Sendable 协议)。
大多数代码都是同步运行的(即其自身不涉及多线程问题)。在那种同步上下文中,你是不被允许使用 await 的。
那么,如果你需要调用一个 async(异步)函数该怎么办?你可以通过使用一个 Task 来实现这一点。
let task = Task(priority: TaskPriority? = nil) {
// 执行一些需要你使用 await 或者可能需要耗费很长时间的操作
}上面这行代码会立即返回。它会在“稍后的某个时间”执行闭包体(快则立即可执行,但绝不会永远不执行)。
返回的 task 实例允许你对该任务进行取消、让步、休眠或等待其返回结果值。
但通常情况下,我们只是让它直接在后台运行。闭包中的代码在运行时,将具有与声明 let task = 处相同的 actor 隔离属性。
如果你将一个闭包作为参数传递,而该闭包可能需要使用 await,会发生什么?在这种情况下,你所传递闭包的那个目标函数,必须将其对应的参数标记为 async。
有一些视图修饰符(View modifiers)会接受异步闭包作为参数……
.task(async () -> Void)
启动一个生命周期与该视图(View)相绑定的任务(Task).refreshable(async () -> Void) //
允许用户异步进行“下拉刷新”操作我们最希望保护其免受多线程干扰的重要“数据结构”就是UI(用户界面)。因此,存在一个专门为 UI 服务的 actor。
它被称为“主 actor(main actor)”。所有的 UI 活动都必须在主 actor 上进行。你所有的视图(View)函数和计算属性(computed vars)都会自动在主 actor 上运行。
然而,你的数据模型(Model)则不会。
@Observable 机制会在 MainActor(主 Actor)
上执行更新。
因此,只有当你的 Model 自己需要进行多线程工作时,你才真正需要使用 actor 隔离(actor isolation)。
在这种情况下,你通常可以直接把 MainActor 当作同步(Synchronization)的 actor。
你可以通过给函数、变量,或者整个 @Observable 类型添加 @MainActor 来做到这一点。
给函数、变量或类标记 @MainActor,意味着它们会被同步到 UI 所在的 actor(MainActor) 上执行。
只要你的代码不是执行耗时操作(time-consuming),这是一个开销很小、实现简单的同步方式。
但如果你正在进行重量级(heavy-duty)的多线程工作,那么你可能应该创建自己的 actor。
@Observable 本身不能是一个 actor,但你可以创建一个专门负责异步工作的 actor。
然后使用 Task 将工作派发到这个专用 actor 中执行,并等待(await)它完成。
这也是 SwiftUI 官方目前推荐的架构:
方式一:Task + @MainActor
Task { @MainActor in
model.count += 1
}等价于:创建一个新的异步任务,并保证这个任务在 MainActor 上运行。例如
Button("更新") {
Task { @MainActor in
count += 1
}
}方式二:MainActor.run
await MainActor.run {
count += 1
}它不会创建新的 Task,它只是:把当前任务切换到 MainActor,执行这一小段代码,然后继续执行。
func load() async {
let data = await fetchData()
await MainActor.run {
self.items = data
}
print("继续后台工作")
}后台线程
│
│ fetchData()
▼
拿到数据
│
│ await MainActor.run
▼
MainActor
更新 UI
│
▼
返回后台
继续执行| Task { @MainActor in } | await MainActor.run |
|---|---|
| 创建新的异步任务 | 不创建新任务 |
不需要 await |
必须 await |
| 不等待里面执行完成 | 会等待执行完成再继续 |
| 更适合”启动一个任务” | 更适合”切换一下线程执行一小段代码” |
Swift Compiler 支持 并发静态检查,需要项目设置中找到
Swift Compiler - Concurrency 打开 Strict Concurrency Checking
有 Minial、Targeted、Complete 三种。
import SwiftUI
import Observation
//======================================================
// MARK: - Sendable Model
//======================================================
struct User: Identifiable, Sendable {
let id = UUID()
let name: String
}
//======================================================
// MARK: - Heavy Worker Actor
//
// actor 保证内部数据不会发生 Data Race
// 所有耗时工作放这里
//======================================================
actor Worker {
private var requestCount = 0
/// 模拟网络请求
func fetchUsers() async -> [User] {
requestCount += 1
print("开始网络请求 \(requestCount)")
// 模拟等待网络
try? await Task.sleep(for: .seconds(2))
return [
User(name: "Tom"),
User(name: "Lucy"),
User(name: "Jack"),
User(name: "Rose"),
User(name: "Mike")
]
}
/// 模拟 CPU 密集型工作
func heavyCalculation() async -> Int {
print("开始CPU计算")
var sum = 0
for i in 0..<20_000_000 {
sum += i % 10
}
print("CPU计算完成")
return sum
}
}
//======================================================
// MARK: - Observable Model
//
// 官方推荐:Observable 放 MainActor
//======================================================
@MainActor
@Observable
class AppModel {
var users: [User] = []
var loading = false
var calculationResult = 0
var status = "Idle"
private let worker = Worker()
//==================================================
// async 函数
//==================================================
func loadUsers() async {
loading = true
status = "Loading..."
// await 调用 actor
let result = await worker.fetchUsers()
users = result
loading = false
status = "Network Finished"
}
//==================================================
// 演示 MainActor.run
//==================================================
func calculate() {
Task {
status = "Calculating..."
let result = await worker.heavyCalculation()
// 当前 Task 可能已经不在 MainActor
// 切回来更新 UI
await MainActor.run {
calculationResult = result
status = "Calculation Finished"
}
print("继续后台工作...")
}
}
//==================================================
// 演示 Task { @MainActor in }
//==================================================
func updateStatusLater() {
Task {
try? await Task.sleep(for: .seconds(1))
Task { @MainActor in
status = "Updated by Task @MainActor"
}
}
}
}
//======================================================
// MARK: - View
//
// View 永远运行 MainActor
//======================================================
struct ContentView: View {
@State
private var model = AppModel()
@State
private var task: Task<Void, Never>?
var body: some View {
NavigationStack {
List {
Section("状态") {
Text(model.status)
if model.loading {
ProgressView()
}
Text("CPU结果")
Text("\(model.calculationResult)")
}
Section("用户") {
ForEach(model.users) { user in
Text(user.name)
}
}
Section("Task") {
Button("开始网络请求") {
// 在同步环境中调用 async
task = Task {
await model.loadUsers()
}
}
Button("CPU计算(不卡UI)") {
model.calculate()
}
Button("Task + @MainActor") {
model.updateStatusLater()
}
Button("取消当前Task") {
task?.cancel()
model.status = "Cancelled"
}
}
}
.navigationTitle("Swift Concurrency Demo")
//--------------------------------------------------
// View 生命周期 Task
//--------------------------------------------------
.task {
print(".task 自动执行")
await model.loadUsers()
}
//--------------------------------------------------
// 下拉刷新
//--------------------------------------------------
.refreshable {
await model.loadUsers()
}
}
}
}
//======================================================
#Preview {
ContentView()
}程序启动:
ContentView
│
│
.task
│
▼
model.loadUsers()
│
▼
await Worker.fetchUsers()
│
│ suspend
▼
等待2秒
│
▼
返回 User[]
│
▼
MainActor 更新UI整个过程中:
UI
│
│ 可以滚动
│ 可以点击按钮
│ 可以动画
│
└─────────────── 没有被阻塞因为等待网络期间发生的是 await,而不是 sleep() ,await 会挂起 Suspend 当前任务,不会阻塞线程,更不会卡住 MainActor。
点击:CPU 计算(不卡UI),流程
Button
│
▼
Task
│
▼
Worker Actor
│
▼
heavyCalculation()
(2000万次循环)
│
▼
返回 Int
│
▼
await MainActor.run
│
▼
更新UI因为 Worker 是一个独立的 actor,耗时不会占用 MainActor,不会阻塞UI。