第7章:协议在跨平台开发中的应用
7.4 实战案例:跨平台待办事项应用
在前几节中,我们探讨了协议在跨平台开发中的作用,包括业务逻辑和 UI 层的协议化实现。本节将通过一个实战案例,整合这些概念,设计并实现一个跨平台的待办事项(To-Do)应用。该应用将支持 iOS 和 macOS(基于 SwiftUI),展示如何利用协议化编程(POP)实现逻辑复用、平台适配和模块化设计。
案例背景
我们要开发一个待办事项应用,支持以下功能:
- 添加和删除任务。
- 持久化存储任务数据。
- 显示任务列表并支持跨平台 UI。 目标是:
- 业务逻辑跨平台复用。
- UI 适配 iOS 和 macOS。
- 可测试且模块化。
系统模块划分
- 任务管理模块:处理任务的增删逻辑。
- 存储模块:管理任务数据的持久化。
- 视图模块:渲染任务列表和用户交互。
- 协调模块:整合各模块,驱动应用。
第一步:定义协议
// 任务实体
struct TaskItem: Identifiable, Codable {
let id: UUID
var title: String
var isCompleted: Bool
}
// 任务管理协议
protocol TaskService {
func addTask(title: String)
func deleteTask(id: UUID)
func allTasks() -> [TaskItem]
}
// 存储协议
protocol TaskStorage {
func saveTasks(_ tasks: [TaskItem])
func loadTasks() -> [TaskItem]
}
// 视图协议
protocol TaskView {
func updateTasks(_ tasks: [TaskItem])
func showError(_ message: String)
}
- 协议定义了模块职责,保持跨平台通用性。
第二步:实现跨平台业务逻辑
class TaskManager: TaskService {
private let storage: TaskStorage
private let view: TaskView
init(storage: TaskStorage, view: TaskView) {
self.storage = storage
self.view = view
refresh()
}
func addTask(title: String) {
var tasks = storage.loadTasks()
let newTask = TaskItem(id: UUID(), title: title, isCompleted: false)
tasks.append(newTask)
storage.saveTasks(tasks)
refresh()
}
func deleteTask(id: UUID) {
var tasks = storage.loadTasks()
if let index = tasks.firstIndex(where: { $0.id == id }) {
tasks.remove(at: index)
storage.saveTasks(tasks)
refresh()
} else {
view.showError("Task not found")
}
}
func allTasks() -> [TaskItem] {
storage.loadTasks()
}
private func refresh() {
view.updateTasks(allTasks())
}
}
TaskManager是跨平台逻辑,依赖注入存储和视图。
第三步:实现存储模块
为 iOS/macOS 提供文件存储实现:
struct FileTaskStorage: TaskStorage {
private let fileURL: URL
init(fileName: String) {
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
fileURL = documents.appendingPathComponent(fileName)
}
func saveTasks(_ tasks: [TaskItem]) {
do {
let data = try JSONEncoder().encode(tasks)
try data.write(to: fileURL)
} catch {
print("Save failed: \(error)")
}
}
func loadTasks() -> [TaskItem] {
do {
let data = try Data(contentsOf: fileURL)
return try JSONDecoder().decode([TaskItem].self, from: data)
} catch {
return [] // 返回空列表作为默认值
}
}
}
FileTaskStorage使用文件系统存储,跨 iOS/macOS 通用。
第四步:实现跨平台 UI
使用 SwiftUI 为 iOS 和 macOS 提供视图:
import SwiftUI
struct TaskListView: TaskView {
@ObservedObject private var viewModel: TaskManager
@State private var tasks: [TaskItem] = []
@State private var errorMessage: String?
init(viewModel: TaskManager) {
self.viewModel = viewModel
}
func updateTasks(_ tasks: [TaskItem]) {
self.tasks = tasks
}
func showError(_ message: String) {
errorMessage = message
}
var body: some View {
NavigationView {
List {
ForEach(tasks) { task in
HStack {
Text(task.title)
Spacer()
if task.isCompleted {
Image(systemName: "checkmark")
}
}
}
.onDelete { indexSet in
indexSet.forEach { viewModel.deleteTask(id: tasks[$0].id) }
}
}
.navigationTitle("To-Do List")
.toolbar {
Button(action: { viewModel.addTask(title: "New Task") }) {
Image(systemName: "plus")
}
}
.alert(isPresented: Binding(get: { errorMessage != nil }, set: { if !$0 { errorMessage = nil } })) {
Alert(title: Text("Error"), message: Text(errorMessage ?? ""))
}
}
.onAppear { viewModel.refresh() }
}
}
TaskListView使用 SwiftUI,跨 iOS 和 macOS 渲染。
第五步:集成应用
let storage = FileTaskStorage(fileName: "tasks.json")
let view = TaskListView(viewModel: TaskManager(storage: storage, view: TaskListView(viewModel: nil)))
let manager = TaskManager(storage: storage, view: view)
// 在 SwiftUI 应用中:
import SwiftUI
@main
struct TodoApp: App {
var body: some Scene {
WindowGroup {
TaskListView(viewModel: manager)
}
}
}
TaskManager协调存储和视图,驱动应用。
第六步:测试跨平台逻辑
为 TaskManager 添加单元测试:
struct MockTaskStorage: TaskStorage {
private var tasks: [TaskItem] = []
mutating func saveTasks(_ tasks: [TaskItem]) {
self.tasks = tasks
}
func loadTasks() -> [TaskItem] {
tasks
}
}
struct MockTaskView: TaskView {
var updatedTasks: [TaskItem] = []
var errors: [String] = []
mutating func updateTasks(_ tasks: [TaskItem]) {
updatedTasks = tasks
}
mutating func showError(_ message: String) {
errors.append(message)
}
}
import XCTest
class TaskManagerTests: XCTestCase {
func testAddAndDeleteTask() {
var storage = MockTaskStorage()
var view = MockTaskView()
let manager = TaskManager(storage: storage, view: view)
manager.addTask(title: "Test Task")
XCTAssertEqual(manager.allTasks().count, 1)
XCTAssertEqual(view.updatedTasks.first?.title, "Test Task")
let taskId = manager.allTasks().first!.id
manager.deleteTask(id: taskId)
XCTAssertEqual(manager.allTasks().count, 0)
XCTAssertEqual(view.updatedTasks.count, 0)
}
}
- 测试验证了任务增删逻辑和视图更新。
案例分析
- 跨平台复用:
TaskManager和协议跨 iOS/macOS 共享。
- UI 适配:
- SwiftUI 自动适配平台风格(iOS 列表、macOS 窗口)。
- 模块化:
- 存储、视图和管理模块独立,可替换。
- 测试性:
- Mock 对象隔离依赖,测试高效。
扩展性
添加云存储支持:
struct CloudTaskStorage: TaskStorage {
func saveTasks(_ tasks: [TaskItem]) {
print("Saved to cloud: \(tasks.map { $0.title })")
}
func loadTasks() -> [TaskItem] {
[TaskItem(id: UUID(), title: "Cloud Task", isCompleted: false)]
}
}
let cloudManager = TaskManager(storage: CloudTaskStorage(), view: TaskListView(viewModel: nil))
- 只需替换存储模块,逻辑无需调整。
注意事项
- SwiftUI 限制:目前仅支持 Apple 平台,Android 需其他 UI 方案。
- 存储路径:实际应用需处理文件路径兼容性。
- 异步优化:可为存储添加
async支持。
小结
通过跨平台待办事项应用,我们展示了协议如何实现业务逻辑复用、UI 适配和模块化设计。这一案例整合了跨平台开发的完整流程,为开发者提供了实用参考。下一章将探讨协议的高级用法,进一步提升编程能力。
