惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Webroot Blog
Webroot Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
V2EX - 技术
V2EX - 技术
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
Recorded Future
Recorded Future
S
Schneier on Security
I
InfoQ
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The GitHub Blog
The GitHub Blog
S
Security @ Cisco Blogs
O
OpenAI News
W
WeLiveSecurity
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Simon Willison's Weblog
Simon Willison's Weblog
人人都是产品经理
人人都是产品经理
Cloudbric
Cloudbric
The Last Watchdog
The Last Watchdog
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
NISL@THU
NISL@THU
T
Tailwind CSS Blog
V
Visual Studio Blog
PCI Perspectives
PCI Perspectives
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Jina AI
Jina AI
D
DataBreaches.Net
B
Blog RSS Feed
N
News and Events Feed by Topic
N
News and Events Feed by Topic
H
Heimdal Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
腾讯CDC
Latest news
Latest news
V
Vulnerabilities – Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
V
V2EX
aimingoo的专栏
aimingoo的专栏
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Register - Security
The Register - Security
Help Net Security
Help Net Security

ZDDHUB

PixelsMeasure 开发第二年总结 PixelsMeasure 开发一年总结 Swift/SwiftUI 踩坑记 为什么说 GPT 利好程序员 ChatGPT 编程实现 Web 数字水印 Web 数字水印探究 Micro Frontends for Mobile URL 加载系统(URL Loading System) Protocol Buffers GraphQL MV* 软件设计架构 学习一个新技巧需要多久? 不停机数据库迁移 Rspec 如何 mock update 方法更新自己? Rails 使用 mysql2 出现的段错误 使用 Docker-compose 部署 Rails 应用到生产环境 Cocoa troubleshooting 独孤九剑 Dit (0x05) - 终端篇 Gem-based Jekyll theme 开发小记 Miscellaneous 前端手记 TodoMVC 之 Redux 篇 前端手记 TodoMVC 之 Server 篇 前端手记 TodoMVC 之 React 篇 前端手记 TodoMVC 之 CSS 篇 独孤九剑 Dit (0x04) - 测试篇 独孤九剑 Dit (0x03) - 缓存篇 英语小抄 LLDB debug Golang Make mistakes 大牛俱乐部上线啦 独孤九剑 Dit (0x02) - 数据结构篇 独孤九剑 Dit (0x01) - 总决 独孤九剑 Dit (0x00) - 我为什么要做 Dit 零值强制类型转换的使用 终端颜色输出重定向 Go语法简略 - 正则表达式 Makefile Go语法简略 - Duck框架探索 Go语法简略 - 依赖注入 Go语法简略 - web应用框架 Go语法简略 - 反射 Go语法简略 - 面向对象 Go语法简略 - goroutine Go语法简略 - 方法和接口 Go语法简略 - 基础篇 大牛 轻松科研 未来这几年 为 Android Studio 创建图标 Shell Git Vim 金庸答百问 论拖延症 Flex, A fast scanner generator 有理想的人 从虚拟到现实 常用视频转接口 Recognizer configuration on CentOS 整个世界清静了 《Python源码剖析》读书笔记
从 0 到 1 开发一款 IOS 应用 - Swift
zddhub · 2021-02-01 · via ZDDHUB

2021 年才做 IOS 应用开发,你觉得晚了吗?我认为时间刚刚好。Swift 发布 (2014 年) 才不到 10 年, ABI 稳定还不到两年。SwiftUI 也不到两年,势头正浓。Swift 的成熟让新人彻底抛弃 Objective-C 的历史包袱。再考虑到苹果努力打通各个平台的雄心壮志,未来无限可能,绝对值得投资。

本文带你从 0 到 1 开发一款类 Notes 应用,并使用 MVVM 模式构建程序。

为了更好的理解本文的内容,强烈建议先自学以下内容:

目标

学完本文,你将会实现一个如下图所示的 Notes 应用。

Notes

该版本支持的功能:

  • 文件夹列表:列出所有文件夹,支持新增文件夹
  • 查看文件夹中的 Notes 列表

步骤

程序 = 算法 + 数据结构。对于 Notes 这类应用来说,还用不上什么算法,主要的就是数据结构,想清楚应用的需求后,我一般分三步走来实现一个程序:

  1. 定义数据结构
  2. 画界面
  3. 响应交互和逻辑

这三步走完后,MVP 就有了,接下来进一步迭代,最终完成程序。

1. 数据结构 - Model

本文实现的 Notes 只支持文字编辑,不支持画图,插入照片等功能,所以数据结构非常简单,只需要两个 Model 就可以了。

  • Folder - 存储目录
struct Folder: Identifiable {
    var id: UUID = UUID()
    var name: String
    var createdAt: Date
    var updatedAt: Date
}
  • Note - 存储记事本
struct Note: Identifiable {
    var id: UUID = UUID()
    var title: String
    var content: String
    var folderId: UUID
    var createdAt: Date
    var updatedAt: Date
    var deletedAt: Date?
}

为了方便对 Model 的操作,这里提供一个 NoteService 协议,用来封装对 Model 的 CRUD。其定义如下:

protocol NoteService {
    func folderList() -> [Folder]

    func noteList(folderId: UUID) -> [Note]

    func addNote(_ node: Note) -> Void

    func updateNote(noteId: UUID, newContent: String) -> Note

    func addFolder(_ name: String) -> Void
}

为了快速实现原型,第一版使用一个 MockNoteService 来实现这个协议:

class MockNoteService: NoteService {
    func folderList() -> [Folder] {
        return folders
    }

    func noteList(folderId: UUID) -> [Note] {
        return notes.filter { (note: Note) -> Bool in
            note.folderId == folderId
        }
    }

    func addNote(_ node: Note) {
        notes.append(node)
    }

    func updateNote(noteId: UUID, newContent: String) -> Note {
        let contents = newContent.split(separator: "\n")
        let index: Int = notes.firstIndex { $0.id == noteId }!
        notes[index].title = String(contents.first!)
        if contents.count > 1 {
            notes[index].content = contents[1..<contents.count].joined(separator: "\n")
        }
        return notes[index]
    }

    func addFolder(_ name: String) -> Void {
        let folder = Folder(name: name, createdAt: Date(), updatedAt: Date())
        folders.append(folder)
    }

    // MARK: Mock data
    var folders: [Folder]
    var notes: [Note]

    init() {
        self.folders = [
            Folder(name: "Default", createdAt: Date(), updatedAt: Date())
        ]

        self.notes = [...]
    }
}

Pre-2. ViewState MVVM 架构

界面当然选择使用 SwiftUI 实现。学过的 SwiftUI Tutorials 使用 MV 架构,我们的 Notes 也简单到可以选用这种架构,但 MV 架构会把业务逻辑分散在 Model 和 View 层,并且在不同层次的 View 中使用全局状态,在复杂应用中难于维护。本文练习使用 ViewState MVVM 架构

ViewState MVVM 架构QuickBird Studios 团队基于 MVVM 架构,适配 SwiftUI 的一种实现。

ViewState MVVM 架构 为每个 View 实现一个 ViewModel 类,该 ViewModel 类用来管理对应 View 的状态和输入,不同的输入对应不同的行为。架构图如下:

ViewState MVVM Diagram

ViewModel 的实现

引入 ViewModel 有两个目的:

  • 使 View 和 Model 解耦,View 不直接操作 Model,通过 ViewModel 来完成。
  • ViewModel 负责业务逻辑,业务逻辑的改变不会影响到 View。View 也不会/不能直接修改 ViewModel 封装的状态,需要触发行为实现状态改变,这点和 Redux 思想类似。

因此,实现一个 ViewModel 协议,View 持有遵循该协议的 ViewModel 类。ViewModel 类封装了 View 的状态和行为,state 只实现了 get 方法,在外部不可写。如下所示:

protocol ViewModel: ObservableObject where ObjectWillChangePublisher.Output == Void {
    associatedtype State
    associatedtype Input

    var state: State { get }
    func trigger(_ input: Input)
}

Input 具体实现成枚举型,表示不同的行为,通过触发不同的行为来更改状态。

2. 画界面

NoteListView 持有一个 NoteListState,包含绘制 Note List 的所有数据。

struct NoteListView: View {
    @ObservedObject
    var viewModel: AnyViewModel<NoteListState, NoteListInput>

    var body: some View {
        List(viewModel.state.notes) { note in
            NavigationLink(destination: NoteDetailView(service: viewModel.state.service, note: note).navigationBarTitleDisplayMode(.inline)) {
                NoteRowView(note: note)
            }
        }
        .navigationBarTitle(viewModel.state.folder.name)
    }

    init(service: NoteService, folder: Folder) {
        self.viewModel = AnyViewModel(NoteListViewModel(service: service, folder: folder))
    }
}

每一个 Item 都是一个 NoteRowView, 具体包含 Note 的 title,更新时间和摘要。

struct NoteRowState {
    var note: Note
    var updatedAtString: String
}

struct NoteRowView: View {
    @ObservedObject
    var viewModel: AnyViewModel<NoteRowState, Never>

    var body: some View {
        VStack(alignment: .leading) {
            Text(viewModel.state.note.title).font(.headline)
            HStack {
                Text(viewModel.state.updatedAtString)
                Text(viewModel.state.note.content).font(.subheadline).lineLimit(1)
            }
        }
    }

    init(note: Note) {
        self.viewModel = AnyViewModel(NoteRowViewModel(note: note))
    }
}

3. 响应交互和逻辑

逻辑代码放在对应的 ViewModel 里面,SwiftUI 接受到用户事件后,trigger 一个 Input 给 ViewModel,ViewModel 处理具体的业务。例如 NoteListView 在 onAppear 方法里 reload 数据:

enum NoteListInput {
    case reload
}

struct NoteListView: View {
    @ObservedObject
    var viewModel: AnyViewModel<NoteListState, NoteListInput>

    var body: some View {
        List(...)
        .navigationBarTitle(viewModel.state.folder.name)
        .onAppear {
            self.reload()
        }
    }
}

private extension NoteListView {
    func reload() {
        viewModel.trigger(.reload)
    }
}

View 把 reload 事件转发给 viewModel,viewModel 根据事件类型,从 service 中获取 Note List,并更新 state。

class NoteListViewModel: ViewModel {
    @Published var state: NoteListState
    func trigger(_ input: NoteListInput) {
        switch input {
        case .reload:
            self.state.notes = state.service.noteList(folderId: state.folder.id)
        }
    }
}

这样,一个 NoteList 页面就做好了。

写在最后

看到这里,一个简单的 Note List 页面就做好了。本文给出的代码只展示了关键部分,对于细节,请大家自行实现。

项目代码在这里,将择时机开源。没开源前如果对源码感兴趣,欢迎邮件 索要。

参考资料

如果你喜欢这篇文章,欢迎赞赏作者以示鼓励