/kvasir-ios

收集读书过程中的句摘与段摘 App

Primary LanguageSwift

Kvasir - iOS

开发过程记录

整体结构

路由

使用组件

如何定义路由

  • 在使用 URLNavigator 的基础上,利用 Swift 的特性,将路由定义成枚举,将路由类型化
  • 路由的构造
    • 自定义了一个 SchemaBuilder , 可以进行链式调用构造路由模版和具体路由

视图通信

设计模式

  • 使用传统的 MVC, 但利用了 Realm 的通知特性,将大部分视图的更新操作通过订阅通知的方式进行
  • 使用了路由组件,扩大了 ViewController 的灵活性,可以在根据路由对目标 ViewController 进行更多的定制,因此每个 ViewController 都实现了 Configurable 协议
    • 协议定义了一个构造方法,这个构造方法接受一个字典作为参数,将自定义的配置通过字典进行传递
    • 而 Coordinator 也是实现了 Configurable 协议,可以复用对应 ViewController 的配置

Coordinator 协议

  • 针对关于列表的 ViewController, 其对应的 Coordinator 对实现一个 ListQueryCoordinatorable 协议
    • 该协议定义了一系列方法用于规范操作
      • 列表数据初次获取的回调
      • 列表数据有更新的回调
      • 数据出错的回调
      • 创建查询的方法
    • 由于查询列表的操作规范了起来,因此,在需要用到列表数据的地方,都可以重用对应的 Coordinator, 配置好相关的回调即可

列表 Coordinator

  • 基本上,一个 UIViewController 对应一个 Coordinator, Coordinator 的作用
    • 负责与 Realm 通信,订阅 Realm 的消息,处理好需要变化的 UI 数据后回调视图更新的方法
    • 负责提交,修改数据时的数据验证

数据库通信

操作协议

  • 对于数据库的操作,增删查改的操作类似,因此结合了范性,创建了一个 Repositorable 协议
  • 利用 Swift 可以对 Protocol 提供默认实现的特性,将部分类似的数据库操作提供了默认实现
    • 当某种类型的数据需要特定的操作时,就可以通过新增或重写的方法来提供

操作队列

  • 自定义了两个队列,分别用于对数据的读操作( RealmReadingQueue )和写操作( RealmWritingQueue ),便于 Debug

单例

  • 由于各种类型的 Repository 的使用频率非常高,可以说是在每个页面都涉及,因此,将 Repository 都设置成了单例,避免了 Repository 频繁创建对象

功能

数据的备份与恢复

使用组件

GCDWebServer

设计

  • 利用 GCDWebServer 创建一个简单的 RESTful API 服务,提供了
    • 备份与恢复的接口
    • 获取网页的静态文件接口
    • 在电脑端通过网页访问设备上开启的服务,进行数据的下载与上传
    • 电脑网页端使用 React 构建,将打包后的静态文件放到 App 的 Bundle 中

实现细节

总体设计
  • 利用多线程多任务处理
  • 多线程工具方面
    • 使用 GCD 是最高效简单的方法,不用手动管理线程,但是任务的进度不可控,不能及时取消
    • 使用比较底层的 Thread 过于多个任务来说,可以直接对任务进度进行管理,但线程方面的管理比较麻烦,容易出错
    • 使用 Operation , 由于 Operation 是建立在 GCD 上,因此可以利用 GCD 的长处。同时 Operation 提供了对任务的监听,可以对任务进行操作,并且可以设置任务间的依赖
  • 创建了一个 ConcurrentableOperation 基本任务,提供并行特性
  • 创建了一个 DataOperation 数据任务,继承于 ConcurrentableOperation ,主要规范了数据任务的流程,构造方法(必须使用一个 URL 进行初始化,指定生成文件,读取文件的位置)
(从电脑端)下载(备份)文件
  • 所有备份任务都是 ExportOperation 的子类,继承于 DataOperation
  • 规范了备份逻辑
    1. 将数据转换为二进制数据
    2. 将二进制数据转换为 JSON 数据
    3. 将 JSON 写入文件
  • 针对每种类型的数据,可各自生成对应备份任务
(从电脑端)上传(恢复)文件
  • 所有恢复任务都是 ImportOperation 的子类,继承于 DataOperation
  • 规范了恢复逻辑
    1. 将备份文件(JSON)数据转换为 struct 类型数据
    2. 通过 struct 类型数据创建出 Realm 类型的对象
    3. 存入 Realm
  • 针对每种类型的数据,可各自生成对应的恢复任务
其他细节
  • 任务进行过程中,遇到了错误,会立即取消其他的任务
  • 备份或恢复的文件,源文件都是一个压缩文件,使用 SSZipArchive 进行解压缩操作
  • 任务依赖
    • 恢复任务
      • 数据转换任务依赖解压任务
      • 各类型数据间的关系恢复任务,依赖数据转换任务
      • 数据库写入任务依赖数据间关系恢复任务
    • 备份任务
      • 压缩任务依赖所有数据文件生成任务
      • 每个数据文件生成任务依赖数据转换任务

遇到问题

  • 在上传文件时,使用 POST 方法,观察到会有 preflight OPTIONS response 501 的问题

    • 解决方法:在服务端创建响应 OPTIONS 方法,直接返回 200
  • 跨域问题,在开发电脑网页端页面时,调用设备开启的服务

    • 解决方法:服务端设置响应头部 Access-Control-Allow-Origin

搜索功能

  • 根据关键字搜索含有关键字的句摘和段摘,并高亮所有关键字

  • 遇到问题

    • 线程不一致
    • Realm 支持从不同线程进行查询和写入,但从 Realm 生成的数据库操作对象(如 Results),只能在同一线程进行访问
    • 因此,查找数据,与找出需要高亮数据的操作,需要在同一线程中进行
    • 使用 GCD 是无法完成上述任务,因为使用 GCD 时,面向的是队列,而不是线程。GCD 本身维护着一个线程池,每一个任务的执行都不保证在同一个线程上
    • 解决办法:在查询期间,使用 RunLoop 维护一个保活线程,保证期间的查询与高亮查找操作都在同一个线程,也避免每次关键字的修改都创建新的线程

其他

  • 模仿 Kingfisher 对 UIImage 扩展做法(包含特定的命名空间),自己实现了一个将与项目相关扩展归纳在某个命名空间下