Swift UI 用 MVVM 架构 Combine EventBus 实现待办清单

原先的

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 个东西:

  1. 事件类型(添加、删除、切换)
  2. 事件总线(单例)
  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)     // 切换了待办状态
}

通俗解释

这就是 **"消息类型"**。

这里我们的事件分三种:

  1. todoAdded:添加了一个待办(带着新增的 Todo 数据)
  2. todoDeleted:删除了待办(带着删除的位置)
  3. 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 刷新

三个变量作用:

  1. todoList:待办列表
  2. inputMessage:输入框文字
  3. eventLog:事件提示文字(添加成功、删除成功)

第 2 部分:Combine 订阅回收(必须写,防止内存泄漏)

Swift 复制代码
private var cancellables = Set<AnyCancellable>()

你可以理解成:**用来存放所有 "监听"**不用的时候会自动销毁,不会内存泄漏。


第 3 部分:一启动就订阅事件(监听全局广播)

Swift 复制代码
init() {
    subscribeEventBus()
}

initViewModel 一创建就自动执行的方法我们让它一启动就立刻开始监听 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 件事:

  1. 存数据
  2. 处理所有按钮逻辑
  3. 修改数据
  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

就是只显示,不思考。

相关推荐
威迪斯特1 小时前
Cobra框架:Go语言命令行开发的现代化利器
开发语言·前端·后端·golang·cobra·交互模型·命令行框架
IT利刃出鞘2 小时前
Java反射--PropertyDescriptor的使用
java·开发语言
㳺三才人子2 小时前
容器內的 H2 控制台
开发语言·前端·javascript
Evand J2 小时前
【MATLAB程序】基于RSSI的RFID二维轨迹定位仿真介绍,EKF滤波增加轨迹定位精度。附下载链接
开发语言·matlab·平面·滤波·定位·导航
遇见火星2 小时前
Firewalld 防火墙实战指南 + TCPWrapper 七层访问控制
开发语言·windows·python
guygg882 小时前
MATLAB实现Bouc-Wen模型动力响应计算
开发语言·matlab
aini_lovee2 小时前
基于MATLAB实现行人检测
开发语言·matlab
所愿ღ2 小时前
SSM框架-Spring1
java·开发语言·笔记·spring
威迪斯特2 小时前
Gorilla框架:Go语言生态中的模块化开发利器
运维·开发语言·后端·golang·web框架·维护·gorilla