Siesta

编写 iOS / macOS REST 客户端的优雅方式

Build Status Version Platforms Docs GitHub license Twitter: @siestaframework

通过为 RESTful 资源提供可观察模型的客户端缓存,极大地简化了应用代码。

目录

概述

文档

它的用途是什么?

问题

想要你的应用与远程 API 通信? 欢迎来到你的状态噩梦!

你需要在收到响应数据时立即显示它。 除非请求的屏幕不再可见。 除非当前可见的 UI 恰好需要相同的数据。 或者即将需要它。

你应该显示一个加载指示器(但要注意避免死循环的竞态条件),显示用户友好的错误(但不要冗余——不要出现模态警报的混乱!),为用户提供重试机制……并在后续请求成功时隐藏所有这些。

请务必避免冗余请求——以及冗余响应反序列化。 反序列化当然应该在后台线程中进行。 哦,请记住不要在回调闭包中意外保留你的 ViewController / 模型 / 辅助对象。 除非你应该这样做。

当然,你希望为创建的每个项目以稍微不同的临时方式从头开始重写所有这些代码。

可能出什么问题呢?

解决方案

Siesta 通过提供一个以资源为中心的替代方案来结束这种头痛,而不是熟悉的以请求为中心的方法。

Siesta 提供了一个应用范围内的可观察的 RESTful 资源状态模型。 此模型回答三个基本问题

…并在这些问题的答案发生变化时广播通知。

Siesta 处理所有转换和极端情况,以漂亮的包装形式提供这些答案,让你专注于你的逻辑和 UI。

特性

它不做什么

起源

该项目最初是我们出于实际需要在几个 Bust Out Solutions 项目中编写的辅助代码。 当我们发现自己在项目之间复制代码时,我们就知道是时候开源它了。

对于开源过渡,我们花时间用 Swift 重写了我们的代码 — 并用 Swift重新思考了它,拥抱该语言以使 API 与概念一样简洁。

因此,Siesta 的代码既有旧的又有新的:在 App Store 上经过实战测试,然后在 Swifty 的一片新天地中重生。

设计理念

让默认的事情在大多数时候都是正确的事情。

让正确的事情始终易于实现。

从需求出发。 不要为了寻找问题而发明解决方案。

以这些目标设计 API

  1. 使客户端代码易于阅读
  2. 使客户端代码易于编写
  3. 保持 API 清洁。
  4. 保持实现整洁。

…按优先级排序。


安装

Siesta 需要 Swift 5.3+ 和 Xcode 12+。(如果你的版本仍然较旧,请使用 swift-* 分支)。

Swift 包管理器

在 Xcode 中

请注意,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 文档中没有的另一个步骤

(有关最近 Xcode 版本中 Carthage 的深入讨论,请参阅 此处。)

Extensions/ 中的代码不是 Carthage 构建的 Siesta.framework 的一部分。(这包括其他库的可选集成,例如 Alamofire。)如果你想使用它们,你需要手动将这些源文件包含在你的项目中。

Git 子模块

  1. 将 Siesta 克隆为子模块到你选择的目录中,在本例中为 Libraries/Siesta

    git submodule add https://github.com/bustoutsolutions/siesta.git Libraries/Siesta
    git submodule update --init
    
  2. Siesta.xcodeproj 作为子项目拖到你的项目树中。

  3. 在你的项目的 Build Phases 下,展开 Target Dependencies。 单击 + 按钮并添加 Siesta。

  4. 展开 Link Binary With Libraries 阶段。 单击 + 按钮并添加 Siesta。

  5. 单击左上角的 + 按钮以添加 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)
      }
    }
  }
}

两个版本的缩略图,供你比较代码的乐趣

Code comparison

相同的功能。 是的,真的。

(好吧,它们不是完全相同。 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 的独特之处在于什么?

不仅仅是功能。 Siesta 解决的问题与其他 REST 框架不同

其他框架本质上将 HTTP 视为一种 RPC 的形式。 新信息仅在与请求耦合的响应中到达 - 异步函数的返回值。

Siesta 将 “ST” 放回 “REST” 中,将状态转移的概念作为架构原则,并将观察状态的行为与转移状态的行为解耦。

如果这种方法听起来很吸引人,请尝试一下 Siesta。


文档

示例

此 repo 包含一个简单的示例项目。 要下载示例项目,安装其依赖项,并在本地运行它

  1. 如果尚未安装,请安装 CocoaPods ≥ 1.0。
  2. 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 由志愿者维护。 如果您没有立即得到问题的答案,请耐心等待; 我们都有工作、家庭、义务以及超越此项目的其他生活。

请善待彼此并遵守我们的行为准则