编写 iOS / macOS REST 客户端的优雅方式
通过为 RESTful 资源提供可观察模型的客户端缓存,极大地简化了应用代码。
swift-*
分支 以获得旧版本支持)想要你的应用与远程 API 通信? 欢迎来到你的状态噩梦!
你需要在收到响应数据时立即显示它。 除非请求的屏幕不再可见。 除非当前可见的 UI 恰好需要相同的数据。 或者即将需要它。
你应该显示一个加载指示器(但要注意避免死循环的竞态条件),显示用户友好的错误(但不要冗余——不要出现模态警报的混乱!),为用户提供重试机制……并在后续请求成功时隐藏所有这些。
请务必避免冗余请求——以及冗余响应反序列化。 反序列化当然应该在后台线程中进行。 哦,请记住不要在回调闭包中意外保留你的 ViewController / 模型 / 辅助对象。 除非你应该这样做。
当然,你希望为创建的每个项目以稍微不同的临时方式从头开始重写所有这些代码。
可能出什么问题呢?
Siesta 通过提供一个以资源为中心的替代方案来结束这种头痛,而不是熟悉的以请求为中心的方法。
Siesta 提供了一个应用范围内的可观察的 RESTful 资源状态模型。 此模型回答三个基本问题
…并在这些问题的答案发生变化时广播通知。
Siesta 处理所有转换和极端情况,以漂亮的包装形式提供这些答案,让你专注于你的逻辑和 UI。
URLSession
,或者 Alamofire,或者注入你自己的 自定义适配器)。该项目最初是我们出于实际需要在几个 Bust Out Solutions 项目中编写的辅助代码。 当我们发现自己在项目之间复制代码时,我们就知道是时候开源它了。
对于开源过渡,我们花时间用 Swift 重写了我们的代码 — 并用 Swift重新思考了它,拥抱该语言以使 API 与概念一样简洁。
因此,Siesta 的代码既有旧的又有新的:在 App Store 上经过实战测试,然后在 Swifty 的一片新天地中重生。
让默认的事情在大多数时候都是正确的事情。
让正确的事情始终易于实现。
从需求出发。 不要为了寻找问题而发明解决方案。
以这些目标设计 API
…按优先级排序。
Siesta 需要 Swift 5.3+ 和 Xcode 12+。(如果你的版本仍然较旧,请使用 swift-*
分支)。
在 Xcode 中
https://github.com/bustoutsolutions/siesta
,然后单击“下一步”。请注意,Xcode 将显示 所有 Siesta 的可选和仅用于测试的依赖项,包括 Quick、Nimble 和 Alamofire。 别担心:这些实际上不会被捆绑到你的应用中(除非你使用 Alamofire)。
在你的 Podfile
中
pod 'Siesta', '~> 1.0'
如果要使用 UI 助手
pod 'Siesta/UI', '~> 1.0'
如果你想使用 Alamofire 作为你的网络提供程序而不是 Foundation 的 URLSession
pod 'Siesta/Alamofire', '~> 1.0'
(你还需要在配置你的 Siesta.Service
时传递一个 Alamofire.Manager
。 请参阅 API 文档 以获取更多信息。)
在你的 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。)如果你想使用它们,你需要手动将这些源文件包含在你的项目中。
将 Siesta 克隆为子模块到你选择的目录中,在本例中为 Libraries/Siesta
git submodule add https://github.com/bustoutsolutions/siesta.git Libraries/Siesta
git submodule update --init
将 Siesta.xcodeproj
作为子项目拖到你的项目树中。
在你的项目的 Build Phases 下,展开 Target Dependencies。 单击 + 按钮并添加 Siesta。
展开 Link Binary With Libraries 阶段。 单击 + 按钮并添加 Siesta。
单击左上角的 + 按钮以添加 Copy Files 构建阶段。 将目录设置为 Frameworks。 单击 + 按钮并添加 Siesta。
如果要使用 UI 助手,你需要为 SiestaUI
重复步骤 3-5。
请告诉我们,即使你最终解决了它。 了解人们在哪里遇到困难将有助于改进这些说明!
为你想要使用的 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 / 网络框架具有不同的主要目标
哪个框架最适合您的项目? 这取决于您的需求和偏好。
Siesta 具有强大的功能,但并不试图解决所有问题。 特别是,Moya 和 RestKit 解决了互补/替代的关注点,而 Alamofire 和 AFNetworking 提供了更强大的底层 HTTP 支持。 更复杂的是,有些框架是建立在其他框架之上的。 例如,当您使用 Moya 时,您也在使用 Alamofire。 Siesta 默认使用 URLSession,但如果您想使用其 SSL 信任管理功能,也可以堆叠在 Alamofire 之上。 各种组合都有可能。
考虑到以上所有因素,以下是一个功能比较¹
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 | Apple 核心 |
1. 免责声明:此表由 Siesta 的非全知作者编译。 有更正/补充吗? 请提交 PR。
2. “平凡”指的是只包含空白、注释、括号、分号和花括号的代码行。
尽管有这个功能列表,Siesta 仍然是一个相对精简的代码库 - 比 Alamofire 小,并且比 RestKit 轻 5.5 倍。
不仅仅是功能。 Siesta 解决的问题与其他 REST 框架不同。
其他框架本质上将 HTTP 视为一种 RPC 的形式。 新信息仅在与请求耦合的响应中到达 - 异步函数的返回值。
Siesta 将 “ST” 放回 “REST” 中,将状态转移的概念作为架构原则,并将观察状态的行为与转移状态的行为解耦。
如果这种方法听起来很吸引人,请尝试一下 Siesta。
此 repo 包含一个简单的示例项目。 要下载示例项目,安装其依赖项,并在本地运行它
pod try Siesta
(请注意,无需先在本地下载/克隆 Siesta;此命令会为您执行此操作。)要寻求帮助,请在 Stack Overflow 上发布问题,并使用 siesta-swift
标记它。 (请务必包含该标签。它会触发 Siesta 核心团队的通知。)这比提交 issue 更好,因为其他人可能与您有相同的问题,并且 Stack Overflow 答案比已关闭的 issue 更容易被发现。
属于 Stack Overflow 的问题
对于错误、功能请求或好主意,请提交 Github issue。 属于 Github issue 的问题
不确定选择哪个? 如果您建议更改 Siesta,请使用 Github issues。 如果您提出一个问题,该问题不会更改项目,因此即使您得到答案后仍然有效,那么请使用 Stack Overflow。
请记住,Siesta 由志愿者维护。 如果您没有立即得到问题的答案,请耐心等待; 我们都有工作、家庭、义务以及超越此项目的其他生活。
请善待彼此并遵守我们的行为准则。