(Swift)iOS Apps with REST APIs(十
重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习Swift,看到这个教程对开发一个实际的APP非常有帮助,所以翻译共享给大家。原教程非常长,我会陆续翻译并发布,欢迎交流与分享。
本章讲的是详细视图中如何调用更多的Web服务。DetailViewController
将会调用下面三个API端点:
- 从另外一个Web服务中获取更多的数据:是否已经关注指定的Gist
-
PUT
和DELETE
:收藏和取消收藏Gist
前面我们所获取的JSON数据中并没有给出我们是否收藏过该Gist。这个需要从另外一个新的API(GET /gists/:id/star
)来获得。文档中指出如果我们已经收藏过该Gist,服务器返回204 No Content
,否则返回'404 Not Found'。
是否已收藏?
在收藏一个Gist前,我们需要判断一下是否已经收藏过该Gist。因此,需要在GitHubAPIManager
中添加一个函数来处理这个功能。这里,我们可以使用.validate(statusCode:204...204)
来判断是否服务器返回了204响应,因此该函数余下的部分是非常简单的:
// MARK: Starring / Unstarring / Star status
func isGistStarred(gistId: String, completionHandler: Result<Bool, NSError> -> Void) {
// GET /gists/:id/star
alamofireManager.request(GistRouter.IsStarred(gistId))
.validate(statusCode: [204])
.response { (request, response, data, error) in
// 204 if starred, 404 if not
if let error = error {
print(error)
if response?.statusCode == 404 {
completionHandler(.Success(false))
return
}
completionHandler(.Failure(error))
return
}
completionHandler(.Success(true))
}
}
路由器中修改如下:
enum GistRouter: URLRequestConvertible {
static let baseURLString:String = "https://api.github.com"
...
case IsStarred(String) // GET https://api.github.com/gists/\(gistId)/star
var URLRequest: NSMutableURLRequest {
var method: Alamofire.Method {
switch self {
...
case .IsStarred:
return .GET
}
}
let result: (path: String, parameters: [String: AnyObject]?) = {
switch self {
...
case .IsStarred(let id):
return ("/gists/\(id)/star", nil)
}
}()
...
return encodedRequest
}
}
表视图中的收藏状态
在DetailViewController
中我们希望有一个变量来表示该Gist是否已经被收藏或者我们的API调用是否还没有返回响应。所以这里最好使用一个可选类型:
var isStarred: Bool?
如果isStarred
为nil
表示服务器还返回没有响应。否则,如果值是true/false
则表示我们是否已关注该Gist。所以,下面让我们在视图显示的时候来调用isGistStarred
:
func configureView() {
// Update the user interface for the detail item.
if let _: Gist = self.gist {
fetchStarredStatus()
if let detailsView = self.tableView {
detailsView.reloadData()
}
}
}
func fetchStarredStatus() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
}
if let status = result.value where self.isStarred == nil { // just got it
self.isStarred = status
// TODO: update display
}
})
}
}
另外,我们还需要在表视图第一个区段中增加一行来显示是否已经收藏。该行需要在调用API响应后动态的来插入,因此这里将调用tableView?.insertRowsAtIndexPaths
来插入这一行(第一区段的第三行):
func fetchStarredStatus() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
}
if let status = result.value where self.isStarred == nil { // just got it
self.isStarred = status
self.tableView?.insertRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
此外,还需要更新表视图数据源中的方法,让表视图知道如何显示:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if let _ = isStarred {
return 3
}
return 2
} else {
return gist?.files?.count ?? 0
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
if indexPath.section == 0 {
if indexPath.row == 0 {
cell.textLabel?.text = gist?.description
} else if indexPath.row == 1 {
cell.textLabel?.text = gist?.ownerLogin
} else {
if let starred = isStarred {
if starred {
cell.textLabel?.text = "Unstar"
} else {
cell.textLabel?.text = "Star"
}
}
}
} else {
if let file = gist?.files?[indexPath.row] {
cell.textLabel?.text = file.filename
// TODO: add disclosure indicators
}
}
return cell
}
在表视图的单元格中如果还没有收藏那么显示Star
,否则显示Unstar
。下面让处理用户的点击事件:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0 {
if indexPath.row == 2 { // star or unstar
if let starred = isStarred {
if starred {
// unstar
unstarThisGist()
} else {
// star
starThisGist()
}
}
}
} else if indexPath.section == 1 {
...
}
}
使用PUT
和DELETE
调用来收藏和取消收藏Gist
为了实现starThisGist()
和unstarThisGist()
我们需要在GitHubAPIManager
中增加两个API调用(现在你已经熟悉怎么添加了吧?):
func starGist(gistId: String, completionHandler: (NSError?) -> Void) {
alamofireManager.request(GistRouter.Star(gistId))
.response { (request, response, data, error) in
if let error = error {
print(error)
return
}
completionHandler(error)
}
}
func unstarGist(gistId: String, completionHandler: (NSError?) -> Void) {
alamofireManager.request(GistRouter.Unstar(gistId))
.response { (request, response, data, error) in
if let error = error {
print(error)
return
}
completionHandler(error)
}
}
路由器中:
enum GistRouter: URLRequestConvertible {
static let baseURLString:String = "https://api.github.com"
...
case Star(String) // PUT https://api.github.com/gists/\(gistId)/star
case Unstar(String) // DELETE https://api.github.com/gists/\(gistId)/star
var URLRequest: NSMutableURLRequest {
var method: Alamofire.Method {
switch self {
...
case .Star:
return .PUT
case .Unstar:
return .DELETE
}
}
let result: (path: String, parameters: [String: AnyObject]?) = {
switch self {
...
case .Star(let id):
return ("/gists/\(id)/star", nil)
case .Unstar(let id):
return ("/gists/\(id)/star", nil)
}
}()
...
return encodedRequest
}
}
在DetailViewController
中我们可以使用当前Gist的ID,并通过这些函数来获取收藏的状态,然后更新isStarred
,最后让表视图重绘,以便显示收藏的状态:
func starThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.starGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
} else {
self.isStarred = true
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
func unstarThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.unstarGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
} else {
self.isStarred = false
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
从你的需求中挑选一些尚未使用过的,把它们添加到API管理器及路由中。然后和界面进行整合。注意这里先不要选择创建和删除,因为后面我们会讲到这些功能的实现。
登陆验证
这里,我们还需要为这些调用增加登陆验证,就像前面getGists
函数。这样当用户尚未登陆时就会看到一个更友好的提示:
func isGistStarred(gistId: String, completionHandler: Result<Bool, NSError> -> Void) {
alamofireManager.request(GistRouter.IsStarred(gistId))
.validate(statusCode: [204])
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(.Failure(authError))
return
}
...
}
}
func starGist(gistId: String, completionHandler: (NSError?) -> Void) {
alamofireManager.request(GistRouter.Star(gistId))
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(authError)
return
}
...
}
}
func unstarGist(gistId: String, completionHandler: (NSError?) -> Void) {
alamofireManager.request(GistRouter.Unstar(gistId))
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(authError)
return
}
...
}
}
checkUnauthorized
函数将为这几种情况创建合适的错误。下面我们还需要更新DetailViewController
中的方法,以便让用户可以看到这些错误:
class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
var alertController: UIAlertController?
...
func fetchStarredStatus() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title:
"Could not get starred status", message: error.description,
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true,
completion: nil)
}
}
if let status = result.value where self.isStarred == nil { // just got it
self.isStarred = status
self.tableView?.insertRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
...
}
我们通过检查错误域和代码:error.domain == NSURLErrorDomain && error.code == NSURLErrorUserAuthenticationRequired
。如果是一个OAuth错误,那么我们可以通过UIAlertController
来显示一个错误提示给用户。
收藏和取消收藏功能非常类似,除了在调用失败时所要显示的错误信息有所不同。另外,显示收藏状态不是用户特殊请求,因此当出现错误时我们并不会中断用户当前的操作:
func starThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.starGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could not star gist",
message: error.description, preferredStyle: .Alert)
} else {
self.alertController = UIAlertController(title: "Could not star gist",
message: "Sorry, your gist couldn't be starred. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
}
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
} else {
self.isStarred = true self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
func unstarThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.unstarGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could not unstar gist",
message: error.description, preferredStyle: .Alert)
} else {
self.alertController = UIAlertController(title: "Could not unstar gist",
message: "Sorry, your gist couldn't be unstarred. " +
" Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
}
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
} else {
self.isStarred = false self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
如果需要,在你的API调用中增加登陆检查。
小结
本章代码.
接下来的章节我们将完成删除和创建功能。然后再讨论一下当用户离线时,对这种基于Web的APP该如何处理。最后再总结一下,以便让你能够扩展这个应用,以便做出你自己的APP。