iOS-SwiftiOSObjective C开发

(Swift)iOS Apps with REST APIs(十

2016-09-13  本文已影响155人  CD826

重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习Swift,看到这个教程对开发一个实际的APP非常有帮助,所以翻译共享给大家。原教程非常长,我会陆续翻译并发布,欢迎交流与分享。

本章讲的是详细视图中如何调用更多的Web服务。DetailViewController将会调用下面三个API端点:

前面我们所获取的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?

如果isStarrednil表示服务器还返回没有响应。否则,如果值是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 { 
    ...
  }
}

使用PUTDELETE调用来收藏和取消收藏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。

上一篇下一篇

猜你喜欢

热点阅读