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

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Jina AI
Jina AI
Stack Overflow Blog
Stack Overflow Blog
量子位
博客园_首页
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Forbes - Security
Forbes - Security
IT之家
IT之家
N
News and Events Feed by Topic
S
Security Affairs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Webroot Blog
Webroot Blog
Recorded Future
Recorded Future
L
LangChain Blog
Y
Y Combinator Blog
AI
AI
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
Help Net Security
Help Net Security
Cyberwarzone
Cyberwarzone
L
Lohrmann on Cybersecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google Online Security Blog
Google Online Security Blog
V2EX - 技术
V2EX - 技术
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
PCI Perspectives
PCI Perspectives
I
Intezer
T
Tenable Blog
G
Google Developers Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
云风的 BLOG
云风的 BLOG
C
CXSECURITY Database RSS Feed - CXSecurity.com
有赞技术团队
有赞技术团队
O
OpenAI News
P
Proofpoint News Feed
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Last Week in AI
Last Week in AI
S
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
Blog — PlanetScale
Blog — PlanetScale

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 页面就做好了。本文给出的代码只展示了关键部分,对于细节,请大家自行实现。

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

参考资料

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