原先的
Swift
import SwiftUI
struct Todo : Identifiable{
let id=UUID()
var isdone:Bool
var title : String
}
struct ContentView: View {
private func deletetodo(at offsets : IndexSet){
todolist.remove(atOffsets: offsets)
}
@State private var todolist : [Todo] = []
@State private var message = ""
var body: some View {
NavigationStack{
Text("备忘录")
.font(.largeTitle)
.padding()
HStack{
TextField("输入代办事件",text :$message)
.padding(10)
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
}
.padding()
List{
ForEach($todolist){ $todo in
HStack{
Button{
todo.isdone.toggle()
} label: {
Image(systemName: todo.isdone ? "checkmark.circle.fill" : "circle" )
}
Text(todo.title)
}
}
.onDelete(perform: deletetodo)
}
.toolbar{
EditButton()
}
Spacer()
Button("添加"){
if !message.isEmpty{
let newTodo = Todo(isdone : false,title: message)
todolist.append(newTodo)
message = ""
}
}
.padding(8)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(8)
}
.padding()
}
}
#Preview {
ContentView()
}

改完后
Swift
import SwiftUI
import Combine
// MARK: - Model
struct Todo: Identifiable {
let id = UUID()
var isDone: Bool
var title: String
}
// MARK: - EventBus
enum TodoEvent {
case todoAdded(Todo)
case todoDeleted(IndexSet)
case todoToggled(Todo)
}
final class EventBus: @unchecked Sendable {
static let shared = EventBus()
private init() {}
let todoEventPublisher = PassthroughSubject<TodoEvent, Never>()
}
// MARK: - ViewModel (完全自己管理订阅,最规范)
final class TodoViewModel: ObservableObject {
@Published var todoList: [Todo] = []
@Published var inputMessage = ""
@Published var eventLog = "" // 日志放在 VM 里
private var cancellables = Set<AnyCancellable>()
init() {
subscribeEventBus() // VM 自己订阅事件
}
// MARK: - 事件订阅(VM 内部处理)
private func subscribeEventBus() {
EventBus.shared.todoEventPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
guard let self = self else { return }
switch event {
case .todoAdded(let todo):
self.eventLog = "添加成功:\(todo.title)"
case .todoDeleted:
self.eventLog = "删除成功"
case .todoToggled(let todo):
self.eventLog = "状态切换:\(todo.isDone ? "已完成" : "未完成")"
}
}
.store(in: &cancellables)
}
// MARK: - 业务逻辑
func addTodo() {
guard !inputMessage.isEmpty else { return }
let newTodo = Todo(isDone: false, title: inputMessage)
todoList.append(newTodo)
inputMessage = ""
EventBus.shared.todoEventPublisher.send(.todoAdded(newTodo))
}
func deleteTodo(at offsets: IndexSet) {
todoList.remove(atOffsets: offsets)
EventBus.shared.todoEventPublisher.send(.todoDeleted(offsets))
}
func toggleTodo(_ todo: Todo) {
guard let index = todoList.firstIndex(where: { $0.id == todo.id }) else { return }
todoList[index].isDone.toggle()
EventBus.shared.todoEventPublisher.send(.todoToggled(todoList[index]))
}
}
// MARK: - View
struct ContentView: View {
@StateObject private var vm = TodoViewModel()
var body: some View {
NavigationStack {
VStack(spacing: 16) {
Text("备忘录")
.font(.largeTitle)
.bold()
TextField("输入代办事件", text: $vm.inputMessage)
.padding(10)
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
if !vm.eventLog.isEmpty {
Text("跨页面通知:\(vm.eventLog)")
.foregroundColor(.green)
.font(.caption)
}
List {
ForEach($vm.todoList) { $todo in
HStack(spacing: 12) {
Button {
vm.toggleTodo(todo)
} label: {
Image(systemName: todo.isDone ? "checkmark.circle.fill" : "circle")
.foregroundColor(todo.isDone ? .green : .gray)
}
Text(todo.title)
.strikethrough(todo.isDone)
.opacity(todo.isDone ? 0.5 : 1)
}
}
.onDelete(perform: vm.deleteTodo)
}
.listStyle(.plain)
Button("添加") {
vm.addTodo()
}
.padding(8)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(8)
}
.padding()
.toolbar {
EditButton()
}
}
}
}
#Preview {
ContentView()
}

第 1 步:先写 Model(最干净)
Model 只存数据,不写逻辑。
Swift
import SwiftUI
import Combine
// 1. 数据模型
struct Todo: Identifiable {
let id = UUID()
var isDone: Bool
var title: String
}
第 2 步:写 EventBus(跨页面通知)
作用:任何页面都能发消息、收消息。
我们写 3 个东西:
- 事件类型(添加、删除、切换)
- 事件总线(单例)
- 让它支持并发安全
Swift
// 2. 事件类型
enum TodoEvent {
case todoAdded(Todo)
case todoDeleted(IndexSet)
case todoToggled(Todo)
}
// 3. 事件总线(全局唯一)
final class EventBus: @unchecked Sendable {
static let shared = EventBus()
private init() {}
let todoEventPublisher = PassthroughSubject<TodoEvent, Never>()
}
这两段代码 = 全局广播器(EventBus) 作用:一个地方发消息 → 所有地方都能收到消息 也就是你要的:跨页面通知。
第一段:enum TodoEvent
代码
Swift
enum TodoEvent {
case todoAdded(Todo) // 新增了一个待办
case todoDeleted(IndexSet) // 删除了待办
case todoToggled(Todo) // 切换了待办状态
}
通俗解释
这就是 **"消息类型"**。
这里我们的事件分三种:
- todoAdded:添加了一个待办(带着新增的 Todo 数据)
- todoDeleted:删除了待办(带着删除的位置)
- todoToggled:点了勾选框(带着被修改的 Todo)
为什么用 enum?
- 类型安全,不会写错消息
- 可以附带数据
- 接收时用
switch一清二楚
第二段:EventBus 类(核心)
Swift
final class EventBus: @unchecked Sendable {
static let shared = EventBus() // 1. 全局单例
private init() {} // 2. 禁止外面新建
let todoEventPublisher = PassthroughSubject<TodoEvent, Never>() // 3. 广播通道
}
我一行一行讲。
1. final class EventBus
class:引用类型,保证全局只有一个对象final:不让别人继承,保证安全稳定
2. @unchecked Sendable
解决你刚才看到的红色报错!
- Swift 6 强制检查线程安全
- 加这个 = 告诉编译器: "我保证这个类跨线程用是安全的,你别报错了"
因为我们这个类里没有可变数据,所以完全安全。
3. static let shared = EventBus()
这就是 "全局唯一" 的秘密任何地方想发消息、收消息,都用:
Swift
EventBus.shared
就像全世界只有一个广播电台。
4. private init()
锁住单例,不让别人乱新建
如果不加这句,别人可以写:
Swift
let a = EventBus()
let b = EventBus()
那就变成两个电台,互相收不到消息。
加了 private init(),外面绝对无法创建新实例。
5. PassthroughSubject<TodoEvent, Never>
**这是 Combine 的 "广播通道"**功能只有两个:
- send() → 发消息
- sink() → 收消息
<TodoEvent, Never> 意思:
- 发的消息类型是
TodoEvent Never= 永远不会出错
整个 EventBus 的工作流程(超级通俗)
发送消息(ViewModel 里)
Swift
EventBus.shared.todoEventPublisher.send(.todoAdded(newTodo))
= 电台播放一条新闻
接收消息(任何页面都能收)
Swift
EventBus.shared.todoEventPublisher
.sink { event in
// 收到消息!
}
= 打开收音机收听
用生活比喻
TodoEvent= 你要广播的内容EventBus.shared= 全世界唯一的电台PassthroughSubject= 电台信号塔send()= 主播说话sink()= 听众打开收音机
为什么要这么设计?(跨页面原理)
因为:
- 页面 A 不用认识页面 B
- ViewModel 不用认识 View
- 全部通过 EventBus 传话
这就是解耦,大型项目必须这么写。
第 3 步:写 ViewModel(核心大脑)
MVVM 里:
- VM 管所有逻辑
- View 只管显示
- 所有数据放 VM
Swift
// 4. ViewModel
final class TodoViewModel: ObservableObject {
// 数据
@Published var todoList: [Todo] = []
@Published var inputMessage = ""
@Published var eventLog = ""
// Combine 订阅回收
private var cancellables = Set<AnyCancellable>()
// 一启动就订阅事件
init() {
subscribeEventBus()
}
// 订阅 EventBus
private func subscribeEventBus() {
EventBus.shared.todoEventPublisher
.receive(on: .main)
.sink { [weak self] event in
guard let self = self else { return }
switch event {
case .todoAdded(let todo):
self.eventLog = "添加成功:\(todo.title)"
case .todoDeleted:
self.eventLog = "删除成功"
case .todoToggled(let todo):
self.eventLog = "状态:\(todo.isDone ? "完成" : "未完成")"
}
}
.store(in: &cancellables)
}
// 添加
func addTodo() {
guard !inputMessage.isEmpty else { return }
let newTodo = Todo(isDone: false, title: inputMessage)
todoList.append(newTodo)
inputMessage = ""
EventBus.shared.todoEventPublisher.send(.todoAdded(newTodo))
}
// 删除
func deleteTodo(at offsets: IndexSet) {
todoList.remove(atOffsets: offsets)
EventBus.shared.todoEventPublisher.send(.todoDeleted(offsets))
}
// 切换状态
func toggleTodo(_ todo: Todo) {
guard let i = todoList.firstIndex(where: { $0.id == todo.id }) else { return }
todoList[i].isDone.toggle()
EventBus.shared.todoEventPublisher.send(.todoToggled(todoList[i]))
}
}
这段代码是 整个 app 的大脑View(页面)只管显示,ViewModel 管:
- 数据
- 逻辑
- 事件发送
- 事件接收
- 状态更新
先看最外层
Swift
final class TodoViewModel: ObservableObject {
}
1. final class
- 用
class是因为它是引用类型,能保证全局用同一个大脑 final不让别人继承,更安全
2. ObservableObject
- 只要加这个,View 就能自动感知数据变化并刷新 UI
第 1 部分:数据(大脑里存什么)
Swift
// 数据
@Published var todoList: [Todo] = []
@Published var inputMessage = ""
@Published var eventLog = ""
核心:@Published
它是 Combine 自动广播器 意思:只要这个变量变了 → 自动通知 View 刷新
三个变量作用:
todoList:待办列表inputMessage:输入框文字eventLog:事件提示文字(添加成功、删除成功)
第 2 部分:Combine 订阅回收(必须写,防止内存泄漏)
Swift
private var cancellables = Set<AnyCancellable>()
你可以理解成:**用来存放所有 "监听"**不用的时候会自动销毁,不会内存泄漏。
第 3 部分:一启动就订阅事件(监听全局广播)
Swift
init() {
subscribeEventBus()
}
init 是 ViewModel 一创建就自动执行的方法我们让它一启动就立刻开始监听 EventBus 的消息。
第 4 部分:订阅 EventBus(核心!)
Swift
private func subscribeEventBus() {
EventBus.shared.todoEventPublisher
.receive(on: .main) // 保证在主线程更新UI
.sink { [weak self] event in // 收到事件
guard let self = self else { return }
switch event {
case .todoAdded(let todo):
self.eventLog = "添加成功:\(todo.title)"
case .todoDeleted:
self.eventLog = "删除成功"
case .todoToggled(let todo):
self.eventLog = "状态:\(todo.isDone ? "完成" : "未完成")"
}
}
.store(in: &cancellables) // 存进订阅池
}
我一句一句讲:
1. EventBus.shared.todoEventPublisher
连接全局广播器
2. receive(on: .main)
UI 更新必须在主线程,不加会崩溃
3. sink { }
收到事件就会执行这里 相当于:监听广播
4. [weak self]
防止内存泄漏必须写
5. guard let self = self else { return }
防止 ViewModel 已销毁还执行代码
6. switch event
判断收到的是哪种事件:
- 添加
- 删除
- 切换状态
7. store(in: &cancellables)
把这个监听放进订阅池,自动管理生命周期
第 5 部分:添加待办(业务逻辑)
Swift
func addTodo() {
guard !inputMessage.isEmpty else { return } // 空的不添加
let newTodo = Todo(isDone: false, title: inputMessage)
todoList.append(newTodo) // 加入列表
inputMessage = "" // 清空输入框
EventBus.shared.todoEventPublisher.send(.todoAdded(newTodo)) // 发通知
}
作用:
- 创建待办
- 加到列表
- 清空输入框
- 发全局通知(跨页面可用)
第 6 部分:删除待办
Swift
func deleteTodo(at offsets: IndexSet) {
todoList.remove(atOffsets: offsets)
EventBus.shared.todoEventPublisher.send(.todoDeleted(offsets))
}
第 7 部分:切换完成状态
Swift
func toggleTodo(_ todo: Todo) {
guard let i = todoList.firstIndex(where: { $0.id == todo.id }) else { return }
todoList[i].isDone.toggle() // 切换 true/false
EventBus.shared.todoEventPublisher.send(.todoToggled(todoList[i])) // 发通知
}
用最简单的话总结 ViewModel 干什么
ViewModel = 整个 app 的大脑
它做 4 件事:
- 存数据
- 处理所有按钮逻辑
- 修改数据
- 发送 / 接收全局事件(EventBus)
View 完全不处理逻辑,只负责:
- 显示数据
- 按钮按下去调用 VM 的方法
第 4 步:写 View(只负责显示)
View 永远不写逻辑,只调用 VM 的方法。
Swift
struct ContentView: View {
@StateObject private var vm = TodoViewModel()
var body: some View {
NavigationStack {
VStack(spacing: 16) {
Text("备忘录")
.font(.largeTitle)
TextField("输入待办", text: $vm.inputMessage)
.padding()
.background(.gray.opacity(0.2))
.cornerRadius(8)
if !vm.eventLog.isEmpty {
Text(vm.eventLog)
.foregroundColor(.green)
}
List {
ForEach($vm.todoList) { $todo in
HStack {
Button {
vm.toggleTodo(todo)
} label: {
Image(systemName: todo.isDone ? "checkmark.circle.fill" : "circle")
}
Text(todo.title)
.strikethrough(todo.isDone)
}
}
.onDelete(perform: vm.deleteTodo)
}
Button("添加") {
vm.addTodo()
}
.padding()
.background(.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding()
.toolbar { EditButton() }
}
}
}
第 5 步:预览
Swift
#Preview {
ContentView()
}
🔥 现在你已经搓完了!
运行一下,完全正常、无报错、无警告。
我用最简单的话讲清楚每个部分的作用
1. Model
就是数据长什么样只存变量,不干活。
2. EventBus
就是全局广播器一个页面发通知,所有页面都能收到。
3. ViewModel
就是大脑
- 存数据
- 处理逻辑
- 发送事件
- 接收事件View 完全不碰逻辑。
4. View
就是脸只显示,不思考。