内容简介:通过网络获取数据是移动应用程序中最常见的一种任务。因此,像 afnetwork 和 Alamofire 这样的网络库在iOS开发者中大受欢迎,也就不奇怪了。即使是这样,你仍然要在 app 中编写和管理大量重复代码,以便从网络获取和显示数据。其中一些任务包括:
通过网络获取数据是移动应用程序中最常见的一种任务。因此,像 afnetwork 和 Alamofire 这样的网络库在iOS开发者中大受欢迎,也就不奇怪了。
即使是这样,你仍然要在 app 中编写和管理大量重复代码,以便从网络获取和显示数据。其中一些任务包括:
- 管理重复的请求。
- 当不再需要时取消请求,比如用户离开页面时。
- 在后台线程中抓取和处理数据,在主线程中更新 UI。
- 将响应解析和转换成模型。
- 显示、隐藏加载进度。
- 收到数据时显示数据。
Siesta 是一个网络库,它自动完成这些任务并简化抓取和显示网络数据的代码。
Siesta 采用了以资源为中心而不是以请求为中心的策略,提供了一种在 app 范围内的可观察 RESTFul 资源状态的模型。
注:本教程假设你懂得用基本的 URLSession 进行网络请求。 如果你不明白,请阅读我们的 URLSession 教程:入门教程 。
开始
在本教程中,你将编写一个”披萨猎手” app,允许用户搜索附近的披萨店。
警告:当本教程结束,你可能会有点饿!
使用本教程顶部或页尾的 Download Materials 按钮下载开始项目。
打开 PizzaHunter.xcworkspace 项目,Build & run。你会看到:
app 包含了两个 View controller:
- RestaurantsListViewController: 显示某个位置附近的披萨店清单。
- RestaurantDetailsViewController: 显示某个披萨店的详情。
因为视图控制器还没有和数据源进行连接,app 现在显示的是空白。
注:写到这里的时候 Siesta 当前版本是 1.3,它还没有升级至 Swift 4.1。编译项目时你会看到几个 deprecation 警告。不用管它们,你的项目可以正常工作。
Yelp API
你将用 Yelp API 来搜索某个城市中的披萨店。
这是获取披萨店列表的 API:
GET https://api.yelp.com/v3/businesses/search
返回的数据是:
{ "businesses": [ { "id": "tonys-pizza-napoletana-san-francisco", "name": "Tony's Pizza Napoletana", "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/d8tM3JkgYW0roXBygLoSKg/o.jpg", "review_count": 3837, "rating": 4, ... }, { "id": "golden-boy-pizza-san-francisco", "name": "Golden Boy Pizza", "image_url": "https://s3-media3.fl.yelpcdn.com/bphoto/FkqH-CWw5-PThWCF5NP2oQ/o.jpg", "review_count": 2706, "rating": 4.5, ... } ] }
使用 Siesta 发起网络请求
首先创建一个 YelpAPI 类。
选择 File ▸ New ▸ File 菜单,选择 Swift file 然后点 Next。文件名为 YelpAPI.swift,然后 Create。编辑文件内容为:
import Siesta class YelpAPI { }
这样就导入了 Siesta 并创建了一个空的 YelpAPI 类。
Siesta Service
现在来编写发起 API 请求的代码。在 YelpAPI 类中添加:
static let sharedInstance = YelpAPI() // 1 private let service = Service(baseURL: "https://api.yelp.com/v3", standardTransformers: [.text, .image, .json]) private init() { // 2 LogCategory.enabled = [.network, .pipeline, .observers] service.configure("**") { // 3 $0.headers["Authorization"] = "Bearer B6sOjKGis75zALWPa7d2dNiNzIefNbLGGoF75oANINOL80AUhB1DjzmaNzbpzF-b55X-nG2RUgSylwcr_UYZdAQNvimDsFqkkhmvzk6P8Qj0yXOQXmMWgTD_G7ksWnYx" // 4 $0.expirationTime = 60 * 60 // 60s * 60m = 1 hour } }
上述代码分成了几个步骤:
- 每个 API Service 都是一个 Siesta 中的 Service 类。因为披萨猎手只需要和唯一的 API——Yelp 打交道,所以你只需要一个 Service 类。
- 告诉 Siesta 你需要在控制台中输出的粒度。
- Yelp API 需要客户端在每个 HTTP 请求头中发送 token 进行验证。每个账号的 token 都是唯一的。对于本教程,你可以用自己的 token 替代。
- 超时时间设置为 1 小时,因为店铺数据的变化不是那么经常。
然后,为 YelpAPI 创建一个 工具 方法,返回一个 Resource 对象:
func restaurantList(for location: String) -> Resource { return service .resource("/businesses/search") .withParam("term", "pizza") .withParam("location", location) }
Resource 对象会根据指定的位置获取一个披萨店的数组,并将之转换为对订阅者有效的 any 对象。RestaurantListViewController 会用这个 Resource 在 UITableView 中显示出披萨店列表。你现在就把它们拼接起来,就能看到 Siesta 的效果。
Resource 和 ResourceObserver
打开 RestaurantListViewController.swift 导入 Siesta:
import Siesta
然后在类中增加一个实例变量 restaurantListResource:
var restaurantListResource: Resource? { didSet { // 1 oldValue?.removeObservers(ownedBy: self) // 2 restaurantListResource? .addObserver(self) // 3 .loadIfNeeded() } }
当对 restaurantListResource 属性赋值时, 你做这些事情:
- 删除已有的观察者。
- 将 RestaurantListViewController 添加为观察者。
- 告诉 Siesta ,是否要从 Resource 加载数据(根据缓存超时时间)。
因为 RestaurantListViewController 被添加为观察者,它必须实现 ResourceObserver 协议。添加如下扩展:
// MARK: - ResourceObserver extension RestaurantListViewController: ResourceObserver { func resourceChanged(_ resource: Resource, event: ResourceEvent) { restaurants = resource.jsonDict["businesses"] as? [[String: Any]] ?? [] } }
如何实现了 ResourceObserver 协议的对象都会收到 Resource 更新通知。
这些通知会调用 resourceChanged(_:event:), 参数是发生改变的 Resource 对象。你可以检索 event 参数,进一步了解是什么发生了改变。
现在可以调用 restaurantList(for:) 了。
当用户从下拉框中选择新的地点,RestaurantListViewController 的 currentLocation 会发生改变。
这时,你应该用新选择的地址去刷新 restaurantListResource。这需要修改当前的 currentLocation 定义:
var currentLocation: String! { didSet { restaurantListResource = YelpAPI.sharedInstance.restaurantList(for: currentLocation) } }
如果现在运行 app,Siesta 会在控制台中打印如下消息:
Siesta:network │ GET https://api.yelp.com/v3/businesses/search?location=Atlanta&term=pizza Siesta:observers │ Resource(…/businesses/search?location=Atlanta&term=pizza)[L] sending requested event to 1 observer Siesta:observers │ ↳ requested → <PizzaHunter.RestaurantListViewController: 0x7ff8bc4087f0> Siesta:network │ Response: 200 ← GET https://api.yelp.com/v3/businesses/search?location=Atlanta&term=pizza Siesta:pipeline │ [thread ᎰᏮᏫᎰ] ├╴Transformer ⟨*/json */*+json⟩ Data → JSONConvertible [transformErrors: true] matches content type "application/json" Siesta:pipeline │ [thread ᎰᏮᏫᎰ] ├╴Applied transformer: Data → JSONConvertible [transformErrors: true] Siesta:pipeline │ [thread ᎰᏮᏫᎰ] │ ↳ success: { businesses = ( { categories = ( { alias = pizza; title = Pizza; } ); coordinat… Siesta:pipeline │ [thread ᎰᏮᏫᎰ] └╴Response after pipeline: success: { businesses = ( { categories = ( { alias = pizza; title = Pizza; } ); coordinat… Siesta:observers │ Resource(…/businesses/search?location=Atlanta&term=pizza)[D] sending newData(network) event to 1 observer Siesta:observers │ ↳ newData(network) → <PizzaHunter.RestaurantListViewController: 0x7ff8bc4087f0>
这些消息可以让你了解到 Siesta 正在做些什么:
- 发起 GET 请求搜索 Atlanta 的披萨店。
- 通知观察者,也就是 RestaurantListViewController 关于这个请求。
- 返回 200 响应码。
- 将原始数据转换成 JSON。
- 发送 JSON 给 RestaurantListViewController。
你可以在 RestaurantListViewController 的resourceChanged(_:event:) 方法中打个断点,然后在控制台中输入命令:
po resource.jsonDict["businesses"]
以查看 JSON 数据。你必须跳过当观察者第一次被添加,但数据还没有进来之前的那次 resourceChanged 调用。
要在 table view 中显示披萨店列表,你必须在 restaurant 属性被修改时刷新 table view。在 RestaurantListViewController 修改 restaurants 属性定义:
private var restaurants: [[String: Any]] = [] { didSet { tableView.reloadData() } }
Build & run,你会看到:
哇!你已经给自己找了一些美味的披萨了。:]
添加小菊花
当某个地方的披萨店列表正在加载时,还没有向用户显示一个小菊花呢!
Siesta 使用了 ResourceStatusOverlay, 它是一个自带的小菊花控件,当 app 正在加载网络数据时他自动显示。
要使用 ResourceStatusOverlay, 首先在 RestaurantListViewController 中声明一个它的变量:
private var statusOverlay = ResourceStatusOverlay()
现在在 viewDidLoad() 中,将它添加到视图树中:
statusOverlay.embed(in: self)
它必须在 view 每次布局 subview 时正确布局。要确保这一点,在 viewDidLoad() 方法下面添加这个方法:
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() statusOverlay.positionToCoverParent() }
最后,将它添加为 restaurantListResource 的观察者,让 Siesta 自动显示和隐藏它。在 restaurantListResource 的 didSet 方法的 .addObserver(self) 和 .loadIfNeeded() 之间加入此句:
.addObserver(statusOverlay, owner: self)
Build & run ,看看小菊花的效果:
你可能注意到了,当你选到同一个城市时,第二次基本上是立即就显示了结果。这是因为第一次加载是从 API 加载的。但 Siesta 会将响应进行缓存,当后续请求到同一个城市时,响应是从内存缓冲中返回的:
Siesta 转换器
对于任何生产性 app,最好将响应表示为定义良好的模型对象而不是非类型化的字典和数组。Siesta 提供了轻松将原始 JSON 转换为对象模型的钩子。
Restaurant Model
披萨猎手保存了每个披萨店的 id、name、url。现在,它是从 Yelp 返回的 JSON 中直接检索数据。让 Restaurant 实现 Codable 可以让你自动实现一个清晰的、类型安全的 JSON 解码。
打开 Restaurant.swift 将结构体定义为:
struct Restaurant: Codable { let id: String let name: String let imageUrl: String enum CodingKeys: String, CodingKey { case id case name case imageUrl = "image_url" } }
注:如果你不知道 Codable 和 CodingKey,请阅读我们的 Swift 4 教程:编码、解码和序列化 。
如果你回去看一眼你从 API 中返回的 JSON,披萨店列表是被包裹在一个 businesses 字典中的:
{ "businesses": [ { "id": "tonys-pizza-napoletana-san-francisco", "name": "Tony's Pizza Napoletana", "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/d8tM3JkgYW0roXBygLoSKg/o.jpg", "review_count": 3837, "rating": 4, ... },
你还需要一个结构体,将 API 的响应解包为一个 businesses 数组。在 Restaurant.swift 中添加代码:
struct SearchResults<T: Decodable>: Decodable { let businesses: [T] }
模型映射
打开 YelpAPI.swift 在 init() 方法中添加代码:
let jsonDecoder = JSONDecoder() service.configureTransformer("/businesses/search") { try jsonDecoder.decode(SearchResults<Restaurant>.self, from: $0.content).businesses }
这个转换器使用 API 端点 /business/search 的资源作为参数,将响应 JSON 传递给 SearchResults 的初始化方法。也就是说你可以创建一个资源,返回一个 Restaurant 对象的数组。
另外一个不起眼但很重要的地方是从 Service 的标准 transformers 中去掉 .json。修改 service 的属性定义:
private let service = Service(baseURL: "https://api.yelp.com/v3", standardTransformers: [.text, .image])
这会让 Siesta 知道不要在 JSON 类型的响应中使用标准 transformer,而使用你提供的自定义的 transform。
RestaurantListViewController
现在修改 RestaurantListViewController 以便它能够处理模型对象,而不是原始 JSON。
打开 RestaurantListViewController.swift 修改 restaurants 的类型为 Restaurant 数组:
private var restaurants: [Restaurant] = [] { didSet { tableView.reloadData() } }
修改 tableView(_:cellForRowAt:) 方法为使用 Restaurant 模型。将下面代码:
cell.nameLabel.text = restaurant["name"] as? String cell.iconImageView.imageURL = restaurant["image_url"] as? String
替换为:
cell.nameLabel.text = restaurant.name cell.iconImageView.imageURL = restaurant.imageUrl
最后,修改 resourceChanged(_:event:) 方法,从 resource 中抽取类型化的模型对象而不是 JSON 字典:
// MARK: - ResourceObserver extension RestaurantListViewController: ResourceObserver { func resourceChanged(_ resource: Resource, event: ResourceEvent) { restaurants = resource.typedContent() ?? [] } }
typedContent() 是一个便利方法,如果值不为空,返回这个 Resource 的最新结果的类型化的值,否则返回空。
Build & run,你会看到没有任何改变。但是,因为使用了强类型,代码更健壮和安全了。
实现披萨店详情
如果你到达这一步,那么接下来的这部分就轻松了。你使用类似的步骤来抓取披萨店详情,并使用 RestaurantDetailsViewController 进行显示。
RestaurantDetails 模型
首先,需要让 RestaurantDetails 和 Location 结构体实现 Codable,以便能够使用强类型的模型。
打开 RestaurantDetails.swift ,让 RestaurantDetails 和 Location 实现 Codable :
struct RestaurantDetails: Codable { struct Location: Codable {
然后,让 RestaunantDetails 实现下列 CodingKey,就像我们在 Restaurant 中所做的一样。在 RestaurantDetails 中添加下列代码:
enum CodingKeys: String, CodingKey { case name case imageUrl = "image_url" case rating case reviewCount = "review_count" case price case displayPhone = "display_phone" case photos case location }
最后,为 Location 添加 CodingKey:
enum CodingKeys: String, CodingKey { case displayAddress = "display_address" }
模型映射
在 YelpAPI 的 init() 方法中,你可以重用之前创建并添加给 transformer 的 jsonDecoder,告诉 Siesta 将披萨店详情 JSON 转换成 RestaurantDetials。打开 YelpAPI.swift 在之前的 service.configureTransformer 一句之上添加:
service.configureTransformer("/businesses/*") { try jsonDecoder.decode(RestaurantDetails.self, from: $0.content) }
另外在 YelpAPI 中加一个工具函数,创建一个用于查询披萨店详情的 Resource 对象:
func restaurantDetails(_ id: String) -> Resource { return service .resource("/businesses") .child(id) }
到目前为止还算顺利。现在,准备进入试图控制器,使用新模型。
在 RestaurantDetailsViewController 中设置 Siesta
RestaurantDetailsViewController 是用户点击披萨店列表时显示的 view controller。打开 RestaurantDetailsViewController.swift 在 restaurantDetail 下面添加代码:
// 1 private var statusOverlay = ResourceStatusOverlay() override func viewDidLoad() { super.viewDidLoad() // 2 YelpAPI.sharedInstance.restaurantDetails(restaurantId) .addObserver(self) .addObserver(statusOverlay, owner: self) .loadIfNeeded() // 3 statusOverlay.embed(in: self) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 4 statusOverlay.positionToCoverParent() }
- 和之前一样,当加载内容时,显示一个 statusOverlay。
- 然后在 viewDidLoad 中用指定的 restaurantId 请求披萨店详情。同时将 self 和 spinner 添加为观察者,监听网络请求状态,以便网络响应返回时进行处理。
- 和之前一样,在 view controller 中添加 spinner。
- 最后,如果布局改变,将 spinner 放在正确的地方。
导航到 RestaurantDetialsViewController 页面
你可能注意到了,app 还不能跳转到披萨店详情页面。要解决这个问题,打开 RestaurantListViewController.swift 找到如下扩展:
extension RestaurantListViewController: UITableViewDelegate {
在这个扩展中增加委托方法:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard indexPath.row <= restaurants.count else { return } let detailsViewController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "RestaurantDetailsViewController") as! RestaurantDetailsViewController detailsViewController.restaurantId = restaurants[indexPath.row].id navigationController?.pushViewController(detailsViewController, animated: true) tableView.deselectRow(at: indexPath, animated: true) }
这里,你简单构造了一个详情页面,将选择的披萨店传给它,然后将它 push 到导航栈中。
Build & run。选择点击列表中的披萨店,搞定!
如果你返回,再次点击同一家披萨店,你会看到详情页面刷的一下就出来了。这是 Siesta 的本地缓存的另外一个例子,提供了良好的用户体验:
这样,你就用 Yelp API 和 Siesta 框架实现了一个披萨店搜索 app。
接下来去哪里?
通过底部的 Download Materials 按钮下载完整的项目代码。
如果你需要阅读 Siesta 文档,那么它的 GitHub 页是一个很好的资源。
要更进一步学习 Siesta,请参考下列资源:
- Security and authentication options in Siesta
- Transform pipeline for fine grain control of JSON to model transformation
- Siesta API Documentation
希望本文对你有所帮助。请在论坛中留言或提问。
以上所述就是小编给大家介绍的《[译]如何用 Siesta 编写 RESTful app》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 基于顺丰同城接口编写sdk,java三方sdk编写思路
- 使用 Clojure 编写 OpenWhisk 操作,第 1 部分: 使用 Lisp 方言为 OpenWhisk 编写简明的代码
- 编写一个Locust文件
- 编写现代 JavaScript 代码
- 性能测试报告编写技巧
- 为vscode编写扩展
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
从0开始做运营 入门篇
张亮 / 4.99元
此书是《从零开始做运营》系列的入门篇。 在互联网产品经理热的今天,关于传统的网站与产品运营的书籍一直非常缺乏,很多有志于互联网行业的年轻人并不明白一款产品、一个网站的策划、上线、成长、成熟直到衰落的过程中,除了产品和网站本身的设计之外,还有一块非常重要的工作是针对网站与产品生命周期的持续运营。 网站与产品运营是一个非常辛苦而非常有趣的事情,希望本书可以为有志于从事互联网网站与产品运营的......一起来看看 《从0开始做运营 入门篇》 这本书的介绍吧!