编写iOS/macOS REST客户端的优雅方式
通过提供RESTful资源的可观察模型客户端缓存来极大地简化应用程序代码。
- 操作系统 iOS 10+、macOS 10.11+、tvOS 9.0+
- 语言 用Swift编写,支持Swift和Objective-C应用程序
- 工具要求 Xcode 11.3+、Swift 5.1+(有关对旧版的支撑,请见
swift-*
分支) - 许可证 MIT
目录
概览
文档
这是用它做什么的?
问题所在
想要您的应用与远程API通信?欢迎来到您的噩梦状态!
每当响应数据到达时,您需要显示该数据。除非请求屏幕不再可见。除非某个当前可见的UI元素恰好需要相同的数据。或者,它即将需要。
应该显示加载指示器(但要注意可能导致它永远不断旋转的竞争条件),显示对用户友好的错误信息(但不能重复——不要弹出模态警告!),提供用户重试机制……当后续请求成功时,隐藏所有这些操作。
确保避免冗余请求——以及冗余的响应反序列化。当然,反序列化应该在后台线程上进行。记得,不要在没有意识的情况下在回调闭包中保留您的ViewController/模型/助手对象。除非您应该这么做。
自然地,您会想要在创建每一个新项目时,以略有不同的即兴方法从头开始重写所有这些。
可能出什么问题呢?
解决方案
Siesta通过提供针对熟悉的以请求为中心的方法的《以资源为中心》的替代方案来结束这个头痛。
Siesta提供了一个应用级别的可观察模型,以RESTful资源的状态作为模型。此模型回答三个基本问题
- 此资源最新的数据是什么,如果有的话?
- 最新的请求是否产生了错误?
- 是否有请求正在进行中?
……并且当这些问题的答案发生变化时,它会发送通知。
Siesta处理所有转换和边缘案例,以封装这些答案,并精美地包装上,让您能够专注于您的逻辑和UI。
特性
- 解耦视图、模型、控制器生命周期与网络请求生命周期
- 解耦请求发起与请求配置
- 消除易出错的状态跟踪逻辑
- 消除冗余的网络请求
- 对所有错误提供统一处理:编码、网络、服务器端、解析
- 高度可扩展,多线程响应反序列化
- 对 JSON、文本和图像提供透明内置解析(可关闭)
- 考虑上传、下载以及延迟的平滑进度报告
- 透明处理 Etag / If-Modified-Since
- 预包装的 UI 辅助器,用于加载和错误处理、远程图片
- 易于调试的自定义日志功能
- 使用 Swift 编写,采用优秀的以 Swift 为中心的 API(Siesta API),但是...
- ...也通过兼容层,在 Objective-C 中运行良好。
- 轻量级。不会获得智慧并尝试摧毁你。
- 稳健的回归测试
- 文档和更多文档
它不能做什么
- 它不会重新发明网络。Siesta 将网络操作委托给您选择的库(默认为
URLSession
,或 Alamofire,或注入您自己的自定义适配器)。 - 它不隐藏 HTTP。相反,Siesta 力求暴露 HTTP 的全部丰富性,同时提供便利,简化常见的使用模式。您可以构建一个适用于自己特定需求的抽象层,或直接使用 Siesta 的优秀 API 来处理请求和响应实体。
- 它不会自动进行响应与模型的映射。这意味着 Siesta 不会限制您的响应模型,或强迫您有任何模型。您可以添加响应转换器以输出应用程序偏好的模型,或者直接与解析后的 JSON 一起工作。
起源
这个项目最初是我们为几个Bust Out Solutions项目编写的辅助代码,出于实际需要。当我们发现自己在项目之间复制代码时,我们知道是时候开源它了。
为了开源迁移,我们花费了时间将我们的代码重写为 Swift,并且在 Swift 中重新思考它,拥抱这种语言使得 API 如概念一样干净。
因此,Siesta 的代码既是旧的也是新的:在应用商店经过实战检验后,又在 Swifty 的新绿地上重生。
设计哲学
让默认之事成为大多数时候的正确之事。
让正确之事始终变得容易。
从需求出发。不要寻找问题去发明解决方案。
设计API遵循以下目标
- 使客户端代码易于阅读。
- 使客户端代码易于编写。
- 保持API整洁。
- 保持实现有序。
……按此优先级顺序。
安装
Siesta需要Swift 5和Xcode 11。(如果您仍在使用较旧版本,请使用swift-*
分支。)
Swift Package Manager
在Xcode中
-
文件 → Swift 包 → 添加包依赖…
-
在URL字段中输入
https://github.com/bustoutsolutions/siesta
并点击下一步。 -
版本设置的默认值适合大多数项目。点击下一步。
-
勾选“Siesta”旁边的复选框。
- 如果您想使用任何UI辅助工具,请也勾选“SiestaUI”。
- 如果您想使用Siesta的Alamofire扩展,请也勾选“Siesta_Alamofire”。
-
点击“完成”。
-
Siesta尚不支持由依赖提供的资源。这意味着,如果您
- 在上面包含了
SiestaUI
- 并且您计划使用
ResourceStatusOverlay
- 并使用其默认初始化器而不是提供自己的自定义UI布局,
……那么您需要将
ResourceStatusOverlay.xib
复制到您自己的项目中。SwiftPM最近增加了对该特性的支持,Siesta将在下一个版本中添加它。
- 在上面包含了
请注意,Xcode将显示Siesta的所有可选和测试只依赖项,包括Quick、Nimble和Alamofire。请放心:这些实际上不会被打包到您的应用中(除非您使用Alamofire)。
CocoaPods
在您的Podfile
中
pod 'Siesta', '~> 1.0'
如果您想使用UI辅助工具,请按以下操作:
pod 'Siesta/UI', '~> 1.0'
如果您想使用 Alamofire 作为您的网络提供程序,而不是使用 Foundation 的 URLSession
pod 'Siesta/Alamofire', '~> 1.0'
(您还需要在配置您的 Siesta.Service
时传递一个 Alamofire.Manager
。有关更多信息,请参阅 API 文档。)
Carthage
在您的 Cartfile
中
github "bustoutsolutions/siesta" ~> 1.0
按照 Carthage 指令 将 Siesta.framework
添加到您的项目中。如果您想使用 UI 辅助工具,您还需要将 SiestaUI.framework
添加到您的项目中。
截至本文撰写之时,您需要遵循一个附加步骤,Carthage 文档中并未提及
- 构建设置 → 框架搜索路径 →
$(PROJECT_DIR)/Carthage/Build/iOS/
(有关 Xcode 近期版本的 Carthage 的深入讨论见 此处。)
Extensions/
中的代码并不属于 Carthage 构建出的 Siesta.framework
。 (这目前仅包括 Alamofire 支持。)如果您想使用这些源文件,需要手动将它们包含到您的项目中。
Git Submodule
-
将 Siesta 作为子模块克隆到您选择的目录中,在本例中为 Libraries/Siesta
git submodule add https://github.com/bustoutsolutions/siesta.git Libraries/Siesta git submodule update --init
-
将
Siesta.xcodeproj
作为子项目拖入您的项目树中。 -
在您的项目构建阶段下,展开目标依赖。点击加号按钮,并添加 Siesta。
-
展开链接二进制库阶段。点击加号按钮,并添加 Siesta。
-
在右上角点击加号按钮添加一个复制文件构建阶段。设置目录为 Frameworks。点击加号按钮,并添加 Siesta。
如果您想使用 UI 辅助工具,需要重复步骤 3-5,针对 SiestaUI
进行操作。
安装难题?
请 告诉我们,即使您最终找到了解决方案。了解人们在何处遇到难题将有助于改进这些说明!
基本用法
为要使用的 REST API 创建一个共享服务实例
let MyAPI = Service(baseURL: "https://api.example.com")
现在注册您的视图控制器——或者视图,内部粘合类,反应性信号/序列,您喜欢的东西——以便在特定资源状态改变时接收通知
override func viewDidLoad() {
super.viewDidLoad()
MyAPI.resource("/profile").addObserver(self)
}
使用这些通知来填充您的UI
func resourceChanged(_ resource: Resource, event: ResourceEvent) {
nameLabel.text = resource.jsonDict["name"] as? String
colorLabel.text = resource.jsonDict["favoriteColor"] as? String
errorLabel.text = resource.latestError?.userMessage
}
或者如果您不喜欢委托,Siesta支持闭包观察者
MyAPI.resource("/profile").addObserver(owner: self) {
[weak self] resource, _ in
self?.nameLabel.text = resource.jsonDict["name"] as? String
self?.colorLabel.text = resource.jsonDict["favoriteColor"] as? String
self?.errorLabel.text = resource.latestError?.userMessage
}
注意,当我们调用jsonDict
时,并不会进行实际的JSON解析。JSON已经在主线程外、在GCD队列上被解析,而且与其他框架不同,它只解析一次,不管有多少观察者。
当然,您可能不希望在所有控制器中都使用原始JSON。您可以配置Siesta自动将原始响应转换为模型
MyAPI.configureTransformer("/profile") { // Path supports wildcards
UserProfile(json: $0.content) // Create models however you like
}
……现在观察者看到的是模型而不是JSON了
MyAPI.resource("/profile").addObserver(owner: self) {
[weak self] resource, _ in
self?.showProfile(resource.typedContent()) // Response now contains UserProfile instead of JSON
}
func showProfile(profile: UserProfile?) {
...
}
在视图出现时触发一个感知陈旧性、抑制重复请求的加载
override func viewWillAppear(_ animated: Bool) {
MyAPI.resource("/profile").loadIfNeeded()
}
……这样您就有了网络化的UI。
添加一个加载指示器
MyAPI.resource("/profile").addObserver(owner: self) {
[weak self] resource, event in
self?.activityIndicator.isHidden = !resource.isLoading
}
……或者最好使用Siesta的预配置ResourceStatusOverlay
视图,免费获得活动指示器、格式良好的错误信息以及重试按钮
class ProfileViewController: UIViewController, ResourceObserver {
@IBOutlet weak var nameLabel, colorLabel: UILabel!
@IBOutlet weak var statusOverlay: ResourceStatusOverlay!
override func viewDidLoad() {
super.viewDidLoad()
MyAPI.resource("/profile")
.addObserver(self)
.addObserver(statusOverlay)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
MyAPI.resource("/profile").loadIfNeeded()
}
func resourceChanged(_ resource: Resource, event: ResourceEvent) {
nameLabel.text = resource.jsonDict["name"] as? String
colorLabel.text = resource.jsonDict["favoriteColor"] as? String
}
}
注意,这个例子不是玩具代码。与其故事板一起,这个小的类是一个全副武装且可以正常运作的基于REST的用户界面。
袜子还在吗?
看看AFNetworking对UIImageView
的受人尊敬的扩展,用于异步按需加载数据和网络图像。真的,去快速查看这段代码并理解所有它所做的酷炫事情。花几分钟时间。我会等你的。我是一个README。我不会走开。
明白了?好。
这是您如何使用Siesta实现相同功能的示例
class RemoteImageView: UIImageView {
static var imageCache: Service = Service()
var placeholderImage: UIImage?
var imageURL: URL? {
get { return imageResource?.url }
set { imageResource = RemoteImageView.imageCache.resource(absoluteURL: newValue) }
}
var imageResource: Resource? {
willSet {
imageResource?.removeObservers(ownedBy: self)
imageResource?.cancelLoadIfUnobserved(afterDelay: 0.05)
}
didSet {
imageResource?.loadIfNeeded()
imageResource?.addObserver(owner: self) { [weak self] _,_ in
self?.image = self?.imageResource?.typedContent(
ifNone: self?.placeholderImage)
}
}
}
}
这两个版本的缩略图,供您比较代码使用
相同的函数
(好吧,好吧,它们并不完全相同。Siesta版本具有更健壮的缓存行为,并且如果刷新,它会自动更新显示图片。)
已经包括了一个更完善的RemoteImageView
版本在Siesta中 —— 但UI免费资源不是重点。即使是“更少的代码”也不是重点。重点是Siesta提供了你真正需要的优雅的抽象 ,这可以解决你的问题,使你的代码更简单,更不易破碎。
与其他框架的比较
流行的REST/网络框架有不同的主要目标
- URLSession是苹果的标准iOS HTTP库(并且通常是大多数项目的需求)。
- Siesta通过可观察资源缓存解决了状态问题。
- Alamofire为URLSession提供了一个Swifty、现代感包装器。
- Moya 通过封装 Alamofire 来隐藏 HTTP URL 和参数。
- RestKit 将 HTTP 与 JSON 相结合
↔ 对象模型↔ Core Data 映射。 - AFNetworking 是一个现代感的 Obj-C 网络API包装器,包括一系列相关实用工具。
哪个最适合你的项目?这取决于你的需求和口味。
Siesta 功能强大,但并不试图解决一切。特别是,Moya 和 RestKit 解决互补/替代问题,而 Alamofire 和 AFNetworking 提供更稳健的底层 HTTP 支持。进一步的比较变得更加复杂,一些框架建立在其他框架之上。例如,当你使用 Moya 时,你也在使用 Alamofire。Siesta 默认使用 URLSession,但如果你想使用其 SSL 信任管理功能,也可以建立在 AlibabaQuick 之上。组合的方式很多。
考虑到所有这些,以下是一些功能对比¹
Siesta | Alamofire | RestKit | Moya | AFNetworking | URLSession | |
---|---|---|---|---|---|---|
HTTP 请求 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
异步响应回调 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
内存缓存 | ✓ | |||||
防止重复请求 | ✓ | |||||
防止重复解析 | ✓ | |||||
解析常见格式 | ✓ | ✓ | ✓ | |||
基于路由的解析 | ✓ | ✓ | ||||
基于内容类型的解析 | ✓ | |||||
文件上传/下载任务 | ✓ | ~ | ✓ | ✓ | ||
对象模型映射 | ✓ | |||||
Core Data 集成 | ✓ | |||||
隐藏 HTTP | ✓ | |||||
UI 辅助工具 | ✓ | ✓ | ||||
主要语言 | Swift | Swift | Obj-C | Swift | Obj-C | Obj-C |
非简单行代码² | 2609 | 3980 | 13220 | 1178 | 3936 | ? |
建立在 | 任何(可注入的) | URLSession | AFNetworking | Alamofire | NSURLSession / NSURLConnection | 苹果核心 |
1. 声明:此表由 Siesta 的非全能作者编制。纠正/补充?请 提交 PR。
2. “简单”指只包含空白、注释、括号、分号和大括号的行。
尽管有此功能列表,Siesta 仍然是一个相对紧凑的代码库——比 AlibabaQuick 小,比 RestKit 轻了 5.5 倍。
Siesta 有何独特之处?
不仅仅是有功能。Siesta 解决了其他 REST 框架不同的一个问题。
其他框架本质上将 HTTP 视为一种 远程过程调用 的形式。新信息只有在与请求结合的响应中才会出现——异步函数的返回值。
Siesta 恢复了 “REST” 中 “ST” 的含义,将 “状态转移” 视为一个架构原则,并将观察状态的行为与转移状态的行为解耦。
如果这种方法听起来吸引人,不妨试一下 Siesta。
文档
示例
此仓库包含一个简单的示例项目 示例项目链接。要下载示例项目,安装其依赖项,并在本地运行
- 如果您还没有安装,请安装 CocoaPods ≥ 1.0。
pod try Siesta
(注意:不需要先本地下载克隆 Siesta;此命令会为您完成)
支持
要 寻求帮助,请在 Stack Overflow 上发帖,并使用 siesta-swift
作为标签。 (请确保包含该标签。它会触发 Siesta 核心团队的提醒。) 这比提交问题更好,因为其他人可能也遇到同样的问题,而 Stack Overflow 的答案比已关闭的问题更易被发现。
Stack Overflow 上应解决的问题
- “我该如何...?”
- “有方法...吗?”
- “Siesta 适合...吗?”
- “我遇到了这个错误...”
对于 错误、功能请求或好主意,请在 Github 上提交问题。属于 Github 问题的内容
- “当我执行 操作 x 时,我期望得到 结果 y 但得到的是 结果 z”
- “应该有一种方法...”
- “项目 x 的文档缺失/表述不清”
不确定选择哪个?如果您是 提出对 Siesta 的更改,请使用 Github 的问题。如果您询问的是不改变项目的 问题,那么使用 Stack Overflow。
两件大小适中但很重要的事情
请注意,Siesta 是由志愿者维护的。如果您收到问题的答案需要一段时间,请耐心等待;我们都有工作、家庭、义务以及在此项目之外的生活。
请相互尊重,并遵守我们的行为守则。