(Swift)iOS Apps with REST APIs(十三) -- 收藏功能

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

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

  • 从另外一个Web服务中获取更多的数据:是否已经关注指定的Gist
  • PUTDELETE收藏取消收藏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?

如果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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容