美文网首页iOS程序员iOS开发技术分享
(Swift) iOS Apps with REST APIs(

(Swift) iOS Apps with REST APIs(

作者: CD826 | 来源:发表于2016-07-21 17:44 被阅读291次

    本小节将会介绍有关报头的一些处理方式,并尝试进行最原始的图片加载、缓存等功能处理。最后使用PINRemoteImage组件完成图片的加载、显示、缓存等功能。

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

    自定义报头(Header)

    在Alamofire的请求中加入自定义报头(Header)有三种方式:第一种是通过Alamofire的管理器配置(Manager)为整个会话添加自定义报头(Header),第二种是通过报头参数为单独的某一个请求添加自定义报头,最后一种方式是通过URLRequestConvertible添加自定义报头。下面我们将逐个进行讲解。

    会话报头

    有些报头(Header)在每次进行API请求的时候都需要提交,如:API键值,或者Accept等。因此这些报头(Header)最好是在全局会话中进行设置,而不是每次请求的时候重复设置。对于这种情况,我们可以通过Alamofire的管理器(Manager)进行设置:

    let manager = Alamofire.Manager.sharedInstance   
    manager.session.configuration.HTTPAdditionalHeaders = 
      ["Accept": "application/json"]
    

    这样设置后,后面我们在请求的是否就需要通过Alamofire的管理器来,而不是Alamofire类方法,API键值就不需要在每次请求的时候再进行设置了(译者注:这里应该是说的Accept,不过API Key也是一样)。请求时,使用:

    let manager = Alamofire.Manager.sharedInstance 
    manager.request(...)
    

    替换原来的方式:

    Alamofire.request(...)
    

    Alamofire的管理器是可以被复用,因此我们在GitHubAPIManager中增加一个变量来保存它:

    class GitHubAPIManager {
      static let sharedInstance = GitHubAPIManager() 
      var alamofireManager:Alamofire.Manager
        
      let clientID: String = "1234567890"
      let clientSecret: String = "abcdefghijkl"
      init () {
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()    
        alamofireManager = Alamofire.Manager(configuration: configuration)
      }
      ...
    }
    

    在你的API管理器中增加Alamofire.Manager的实例。

    一次性请求的报头(Header)

    那如何为一个单独的请求设置报头(Header)呢?Alamofire(1.3版本之后)为我们提供了很方便的方法来达到这个目的。首先我们为自定义的报头创建一个字典(Dictionary):

    let headers = ["Accept": "application/json"]
    

    然后传递给我们要调用的manager.request(如果不想在请求中包含前面设置的全局报头则可以使用Alamofire.request):

    let urlString = "https://api.github.com/gists" 
    manager.request(.GET, urlString, headers: headers).responseJSON(...)
    

    或:

    let urlString = "https://api.github.com/gists" 
    Alamofire.request(.GET, urlString, headers: headers).responseJSON(...)  
    

    通过URLRequestConvertible设置报头

    因为我们使用Alamofire路由器(Router)来构建我们的URL请求,因此我们可以在NSMutableURLRequest中直接添加报头(Header)。如:

    let URL = NSURL(string: GistRouter.baseURLString)!
    let URLRequest = NSMutableURLRequest(URL:
      URL.URLByAppendingPathComponent(result.path))
    
    URLRequest.setValue("application/json", forHTTPHeaderField: "Accept") 
    

    这样当URL进行请求的时候就会使用这些报头。我们后面会使用这种方式将OAuth的访问令牌添加到报头中。

    报头小结

    许多功能中都会使用到报头(Header),如:缓存、cookies等。后面我们所要实现的三种验证方式都会使用到,在那里我们会更进一步详细说明使用它。

    检查你的开发文档中关于报头要求的章节,如果需要把它们添加到API管理器中。先不要担心有关认证的报头,我们会在认证章节中来处理。

    在实现认证功能之前,我们还是先继续完善表格视图中的基本功能。接下来,我们会实现拥有者头像加载及显示,视图的滚动加载,及下拉刷新等功能。

    图像的异步加载及缓存

    到目前为止,我们构建了一个Swift的App,能够通过GitHub的API获取gists。我们已经实现如下功能:

    • 通过GitHub的gists API获取公共的gists列表
    • 使用Alamofire的响应序列化器将JSON转换为Swift对象数组
    • 解析web服务返回JSON数据中的一些字段
    • 将列表在表格视图中显示

    在本章我们将添加一个新的功能,就是:为表格视图的每一个行显示gists拥有者的头像。我们将使用异步的方式,通过解析到的图像URL地址来加载图像。此外,我们还需要处理表格视图中单元格的复用问题,把图片加入到缓存中,并在显示的时候从中获取,而不是在每次需要显示的时候都从服务器中重复下载。

    如果你的API中也需要显示图片,那么请跟随本章节来完成它。如果没有,那么你可以先跳过本章,等后面需要时,即使不是在视图表格中显示,再返回来。

    当本章完成后,App界面看起来如下:

    如果你前面没有一直跟着本教程编写代码,你可以从GitHub上下载前一个章节的完整代码。

    如果你不想敲代码,你可以从GitHub上下载本章的代码。

    通过URL加载图片

    Gist类中的init(json:)方法中我们已经解析了gist拥有者头像的URL地址:

    self.ownerAvatarURL = json["owner"]["avatar_url"].string
    

    现在我们已经为从URL中获取图片准备好了,接下来让我们完成它。首先,在GitHubAPIManager中添加下面的函数:

    class GitHubAPIManager 
    {
      ...
       
      func imageFromURLString(imageURLString: String, completionHandler: 
        (UIImage?, NSError?) -> Void) {
        alamofireManager.request(.GET, imageURLString)
          .response { (request, response, data, error) in
            // use the generic response serializer that returns NSData 
            if data == nil {
              completionHandler(nil, nil)
              return
            }
            let image = UIImage(data: data! as NSData) 
            completionHandler(image, nil)
        }
      }
    }
    

    我们使用imageURLString发起一个GET请求。一旦我们得到响应(注意这里返回的类型是NSData,因为我们使用的.response方法),先对数据进行检查,并尝试转换成图片。如果转换成功,那么将它传给完成处理程序。如果失败(或者我们的请求根本不是图片),我们将退出,并返回一个错误给完成处理程序。

    将图像设置给UITableViewCell

    现在我们终于可以显示图片了。修改MasterViewController.cellForRowAtIndexPath:

    override func tableView(tableView: UITableView,
      cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
          
      let gist = gists[indexPath.row] 
      cell.textLabel!.text = gist.description 
      cell.detailTextLabel!.text = gist.ownerLogin 
      if let urlString = gist.ownerAvatarURL {
        GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: { 
          (image, error) in
          if let error = error {
            print(error)
          }
          if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) { 
            cellToUpdate.imageView?.image = image // will work fine even if image is nil 
            // need to reload the view, which won't happen otherwise
            // since this is in an async call
            cellToUpdate.setNeedsLayout()
          }
        })
      } else { 
        cell.imageView?.image = nil
      }
        
      return cell 
    }
    

    我们通过if let urlString = gist.ownerAvatarURL检查图像的URL是否有值。如果有值那么调用前面写的方法请求图片数据。否则,我们将清空图片单元格:

    GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
      ...
    } else { 
      cell.imageView?.image = nil
    }
    

    在完成处理程序中,我们先检查是否有错误,如果有把它打印出来。这里,我们没有使用guard那是因为在这种情况下,我们得先清空原来存在的图片,而guard将强迫我们返回,从而无法清除原来已经存在的图片:

    completionHandler: { (image, error) in 
      if let error = error {
        print(error)
      }
      if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) { 
        cellToUpdate.imageView?.image = image // will work fine even if image is nil 
        // need to reload the view, which won't happen otherwise
        // since this is in an async call
        cellToUpdate.setNeedsLayout()
      }
    })
    

    一旦我们得到图片就把它设置给单元格。这里会稍微有些复杂,因为imageFromURLString方法是异步的,而且UITableView又会复用单元格。因为在这里我们使用了dequeueReusableCellWithIdentifier,表格视图显示的时候会复用那些已经在不在显示区域的单元格。举个例子,假如我们有20个gists需要显示,但在一屏中仅能显示10个,这时候表格视图则大概只会创建12~14个。假如一个单于格已经滚出了屏幕,我们将无法给它设置图片,因为这时候有可能它已经被其它gist对象复用了,从而会造成数据错位的错误。

    解决这个问题的方法就是使用索引路径,一旦我们得到图片,通过该索引路径获取表格视图的单元格:

    if let cellToUpdate = self.tableview?.cellForRowAtIndexPath(indexPath)
    

    这样我们就可以把图片设置给单元格了。因为图片的加载是异步进行的,假如相应的单元格还是显示的,那么我们就可以使用cellToUpdate.setNeedsLayout()告诉视图我们已经改变了部分东西,需要进行重新绘制。

    需要注意的一点是,这里我们没有检查所要设置给单元格的图片是否是为空。如果因为某种原因图像为空,我们还得需要清除单元格中原来的图像,因此最好使用cellToUpdate.imageView?.image = result.value来设置。

    保存并运行。你应该可以看到每个gist拥有者的头像,如果他们设置了自己头像的话:

    如果你的API中也有图片,参考本小节来加载它们。

    优化

    我们为每一个单元格发起图片请求,但有时候当我们得到图片的时候已经不需要了,因为它已经滚出了屏幕的显示区域。一种优化方式就是当单元格滚出屏幕时取消Alamofire请求。但假如你的App中需要处理大量的图片,我估计你会不想这么干(因为你知道,这样会造成滚动不平滑)。现在我们的滚动看起来还是非常平滑的,并且我们还打算增加图片的缓存处理,因此刚才所说的那个问题已经不是现在急需处理的问题了。

    即使对于这么简单的应用,图片缓存的优化还是非常值得的,这样就不用每次都去请求了。接下来我们将快速的,使用一个简单的,只能在单次运行中有效的缓存,来看看如何工作的。然后,我们将使用PINRemoeImage替换我们的缓存,PINRemoeImage是一个非常智能的具有持久化的缓存。

    图片缓存

    MasterViewController中我们添加一个字典用来存放这些图像,并使用它们的URL作为key:

    var imageCache: Dictionary<String, UIImage?> = Dictionary<String, UIImage?>()
    

    这样我们就可以在获取图片后将它存放字典中(在cellForRowAtIndexPath函数中):

    if let urlString = gist.ownerAvatarURL { 
      GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
        (image, error) in
        if let error = error {
          print(error)
        }
        // Save the image so we won't have to keep fetching it if they scroll
        self.imageCache[urlString] = image
        if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) {
          cellToUpdate.imageView?.image = image // will work fine even if image is nil 
          // need to reload the view, which won't happen otherwise
          // since this is in an async call
          cellToUpdate.setNeedsLayout()
        }
      })
    } else { 
      cell.imageView?.image = nil
    }
    

    在获取图片前我们先检查所要加载的图片是否已经在缓存中:

    if let urlString = gist.ownerAvatarURL {
      if let cachedImage = imageCache[urlString] {
        cell.imageView?.image = cachedImage // will work fine even if image is nil 
      } else {
        GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
          ...
        })
      }
    } else { 
      cell.imageView?.image = nil
    }
    

    最终代码为:

    override func tableView(tableView: UITableView, cellForRowAtIndexPath
      indexPath: NSIndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
        
      let gist = gists[indexPath.row]
      cell.textLabel!.text = gist.description 
      cell.detailTextLabel!.text = gist.ownerLogin
      // set cell.imageView to display image at gist.ownerAvatarURL 
      if let urlString = gist.ownerAvatarURL {
        if let cachedImage = imageCache[urlString] {
          cell.imageView?.image = cachedImage // will work fine even if image is nil
        } else {
          GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
            (image, error) in
            if let error = error {
              print(error)
            }
            // Save the image so we won't have to keep fetching it if they scroll
            self.imageCache[urlString] = image
            if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) {
              cellToUpdate.imageView?.image = image // will work fine even if image is nil 
              // need to reload the view, which won't happen otherwise
              // since this is in an async call
              cellToUpdate.setNeedsLayout()
            }
          })
        }
      } else {
        cell.imageView?.image = nil 
      }
    
      return cell 
    }
    

    为了测试这些代码,需要设置一个断点,并单步执行看看代码是如何执行的。在需要设置断点的前面的行号中点击一下就可以设置断点了:

    然后运行。当运行到断点时会停止。这时你可以通过Xcode底部的面板查看变量的值,并逐步调试:

    点击step over按钮执行下一句:

    通过查看高亮显示的代码行,可以知道哪个代码路径被执行。如果头像URL有值,那么将尝试加载:

    如果没有,单元格将被设置为nil

    如果想继续执行直到下一个断点,可以点击continue按钮:

    当你每次运行App的时候,所有的图像在第一次显示的时候都将通过URL进行加载,因为这里我们并没有将缓存持久化。在下个小节我们将解决这个问题。当我们在屏幕上来回滚动时,表格视图将重复利用了已缓存的图像。你可以在我们从缓存中加载图像的代码处设置一个断点,这样就可以很方便的查看缓存是如何被复用的啦:

    当单元格初始化完成后,上下滚动时,你将看到断点处的代码被执行。

    更好的缓存: PINRemoteImage

    我们通过Alamofire的请求从JSON数据中解析出图像的URL,通过异步请求的方式把加载的图片设置给UITableViewCell。然后我们建立一个初级的缓存,在单次运行中让图片可以被复用,从而帮我们避免了重复请求。当然,在发行版的APP中还是需要建立一个可以持久化的缓存机制,以便在多次运行中图片都可以复用。

    PINRemoteImage是一个支持图片持久化的缓存处理库,那么接下来我们使用它,使得所要加载的图片只需要加载一次,并可以被重复使用。而且PINRemoteImage要比我们的非持久化缓存更容易集成和使用。

    使用CocoaPods将PINRemoteImage v1.2加入到你的工程中。并在MasterViewController中引入,然后删除我们刚才写的缓存代码:

    import UIKit
    import PINRemoteImage
      
    class MasterViewController: UITableViewController {
      ...
      
      ~~var imageCache: Dictionary<String, UIImage?> = Dictionary<String, UIImage?>()~~
    
      ...
    }
    

    PINRemoteImage最好和一个占位符图像一起使用。你可以把一个图像拖入到工程中,并把它改名为placeholder.png。这样如果用户没有设置头像的话,会显示我们刚才设置的占位符图像:

    override func tableView(tableView: UITableView, cellForRowAtIndexPath
      indexPath: NSIndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
        
      let gist = gists[indexPath.row] 
      cell.textLabel!.text = gist.description 
      cell.detailTextLabel!.text = gist.ownerLogin 
      cell.imageView?.image = nil
       
      // set cell.imageView to display image at gist.ownerAvatarURL
      if let urlString = gist.ownerAvatarURL, url = NSURL(string: urlString) { 
        cell.imageView?.pin_setImageFromURL(url, placeholderImage:
          UIImage(named: "placeholder.png")) 
      } else {
        cell.imageView?.image = UIImage(named: "placeholder.png")
      }
      
      return cell 
    }
    

    小结

    保存并运行。你会发现如果用户设置了头像的话将被加载显示,否则显示我们所设置的占位符图像:

    点击这里可以下载本章的最终代码。

    相关文章

      网友评论

        本文标题:(Swift) iOS Apps with REST APIs(

        本文链接:https://www.haomeiwen.com/subject/ropqjttx.html