美文网首页iOS 开发 iOS Developer程序员
(Swift)iOS Apps with REST APIs(十

(Swift)iOS Apps with REST APIs(十

作者: CD826 | 来源:发表于2016-09-22 17:37 被阅读162次

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

    新建一个Gist要麻烦一点,因为我们需要获取用户的输入。所以,我们首先得创建一个新的视图用来采集用户的输入。但是在这之前,我们先来看看所要调用的API。

    新建API是允许匿名调用的,但是我们这里还是为当前用户创建,因为在前面的APP开发中我们已经实现了用户认证。

    带JSON参数的POST API调用

    POST方式调用https://api.github.com/gists可以创建一个Gist。调用时我们需要传入的JSON参数类似下面:

    {
      "description": "the description for this gist", 
      "public": true,
      "files": {
        "file1.txt": {
          "content": "String file content"
        }
      }
    }
    

    因此,对于Gist我们需要以下两个字段:

    1. Description: 字符串
    2. Public 还是 Private:布尔值

    每个文件我们需要的内容有:

    1. 文件名称:字符串
    2. 文件内容:字符串

    下面让我们看看如何获取用户的输入并将它们转换为API调用需要JSON格式。之前,已经有了一个File类来负责文件的处理,因此我们将继续使用它:

    func createNewGist(description: String, isPublic: Bool, files: [File],  
      completionHandler: (Result<Bool, NSError>) -> Void) {
    

    这里,我们已经获取了用户输入的值,那么该如何转换为JSON呢?Alamofire所期望的JSON参数是一个[String: AnyObject]字典格式。它可以是由数组,字典和字符串等组合而成。

    首先,我们将isPublic由布尔值转换为字符串:

    let publicString: String 
    if isPublic {
      publicString = "true" 
    } else {
      publicString = "false"
    }
    

    然后转换文件列表。这里把文件列表转换为一个小的JSON字典,如下:

    "file1.txt": {
      "content": "String file1 content"
    }
    "file2.txt": {
      "content": "String file2 content"
    }
    ...
    

    对于文件对象需要content属性,并且我们能够使用文件名称和内容创建一个File对象,因此File修改如下:

    class File: ResponseJSONObjectSerializable { 
      var filename: String?
      var raw_url: String?
      var content: String?
    
      required init?(json: JSON) { 
        self.filename = json["filename"].string 
        self.raw_url = json["raw_url"].string
      }
      
      init?(aName: String?, aContent: String?) { 
        self.filename = aName
        self.content = aContent
      }
    }
    

    回到CreateNewGist方法中,让我们将文件列表转换为一个字典:

    var filesDictionary = [String: AnyObject]() 
    for file in files {
      if let name = file.filename, content = file.content { 
        filesDictionary[name] = ["content": content]
      }
    }
    

    然后将它合并到一个字典中:

    let parameters:[String: AnyObject] = [ 
      "description": description, 
      "isPublic": publicString,
      "files" : filesDictionary
    ]
    

    然后在路由中增加API调用:

    enum GistRouter: URLRequestConvertible {
      static let baseURLString:String = "https://api.github.com"
      
      ...
      case Create([String: AnyObject]) // POST https://api.github.com/gists
      
      var URLRequest: NSMutableURLRequest { 
        var method: Alamofire.Method {
          switch self { 
          ...
          case .Create:
            return .POST 
          }
        }
        
        let result: (path: String, parameters: [String: AnyObject]?) = { 
          switch self {
          ...
          case .Create(let params):
            return ("/gists", params) 
          }
        }()
        
        ...
        
        return encodedRequest 
      }
    }
    

    当创建了URL请求时,路由器将把参数添加进去,并指示其格式为JSON。这些代码之前我们早就写好了,只是一直都没有使用:

    let encoding = Alamofire.ParameterEncoding.JSON
    let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)
    

    现在我们就可以构建请求了:

    alamofireManager.request(GistRouter.Create(parameters)) 
      .response { (request, response, data, error) in
        if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) { 
          completionHandler(.Failure(authError))
          return
        }
        
        if let error = error {
          print(error) 
          completionHandler(.Success(false)) 
          return
        }
        completionHandler(.Success(true)) 
    }
    

    以上代码合起来如下:

    func createNewGist(description: String, isPublic: Bool, files: [File], completionHandler: 
      Result<Bool, NSError> -> Void) {
      let publicString: String
      if isPublic {
        publicString = "true" 
      } else {
        publicString = "false"
      }
      
      var filesDictionary = [String: AnyObject]() 
      for file in files {
        if let name = file.filename, content = file.content { 
          filesDictionary[name] = ["content": content]
        }
      }
      let parameters:[String: AnyObject] = [ 
        "description": description, 
        "isPublic": publicString,
        "files" : filesDictionary
      ]
      
      alamofireManager.request(GistRouter.Create(parameters)) 
        .response { (request, response, data, error) in
          if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) { 
            completionHandler(.Failure(authError))
            return
          }
          if let error = error {
            print(error) 
            completionHandler(.Success(false)) 
            return
          }
          completionHandler(.Success(true)) 
      }
    }
    

    如果你的APP也有这个需求,那么在你的API管理器中添加一个POST调用来创建一个新的对象。

    下面让我们来构建界面,以便获得用户的输入。

    创建带有验证的输入表单

    接下来我们使用XLForm库创建一个表单来获取用户的输入。XLForm是iOS开发中常用的一个框架,且内置了输入验证功能。

    使用CocoaPods将XLForm v3.0添加到项目中。

    只有当用户查看自己的Gist列表的时候才可以使用该功能。和编辑按钮一样,我们将在视图控制器(MasterViewController)的右上角增加一个+按钮,这个按钮当用户切换到自己的Gist列表的时候显示,当用户切换为公共或收藏列表时隐藏。该按钮不像编辑按钮那样,有默认的动作来处理,所以将使用insertNewObject方法来响应该按钮:

    @IBAction func segmentedControlValueChanged(sender: UISegmentedControl) { 
      // only show add button for my gists
      if (gistSegmentedControl.selectedSegmentIndex == 2) {
        self.navigationItem.leftBarButtonItem = self.editButtonItem()
        let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self,
          action: "insertNewObject:") 
        self.navigationItem.rightBarButtonItem = addButton
      } else { 
        self.navigationItem.leftBarButtonItem = nil 
        self.navigationItem.rightBarButtonItem = nil
      }
      loadGists(nil) 
    }
    

    此外,还要把viewDidLoad中的添加按钮部分的代码删除:

    override func viewDidLoad() {
      super.viewDidLoad()
      // Do any additional setup after loading the view, typically from a nib.
      
      // let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self,
      //   action: "insertNewObject:")
      // self.navigationItem.rightBarButtonItem = addButton
      if let split = self.splitViewController {
        let controllers = split.viewControllers
        self.detailViewController = (controllers[controllers.count-1] as!
          UINavigationController).topViewController as? DetailViewController
      }
    }
    

    insertNewObject方法将用来显示新建表单(当然,这里我们还没有创建该表单):

    // MARK: - Creation
    func insertNewObject(sender: AnyObject) {
      let createVC = CreateGistViewController(nibName: nil, bundle: nil) 
      self.navigationController?.pushViewController(createVC, animated: true)
    }
    

    这里我们最好先创建CreateGistViewController否则程序将崩溃。接下来创建一个名称为CreateGistViewController.swift文件,然后在该文件中引入XLForm,并让所定义的类继承XLFormViewController:

    import Foundation
    import XLForm
    
    class CreateGistViewController: XLFormViewController { 
    
    }
    

    然后在表单中添加字段。为了保持简单,我们这里只允许添加一个文件。因为,我们自定义了UIViewController,所以需要提供一个初始化函数:required init(coder aDecoder: NSCoder)。该方法中将调用父类的init函数及我们定义的initializeForm函数。

    我们还希望能够从xib文件或者故事板文件中初始化,因此,这里我们还需要覆写:init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!):

    class CreateGistViewController: XLFormViewController { 
      required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.initializeForm() 
      }
      
      override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) { 
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 
        self.initializeForm()
      }
      
      private func initializeForm() { 
        ...
      }
    }
    

    现在我们就可以在initalizeForm函数中增加字段了。通过XLFormDescriptor对象,XLFormViewController知道该如何显示。因此,我们将使用它来创建我们的表单,并添加一些区段和行。和表视图不同的是,在创建的时候我们将一次性添加所有的区段和行。每一行包含了:类型、标题以及tag。tag将用来获取这些行对象。我们还可以指定那些是必填字段:

    private func initializeForm() {
      let form = XLFormDescriptor(title: "Gist")
      
      // Section 1
      let section1 = XLFormSectionDescriptor.formSection() as XLFormSectionDescriptor 
      form.addFormSection(section1)
      
      let descriptionRow = XLFormRowDescriptor(tag: "description", rowType: 
        XLFormRowDescriptorTypeText, title: "Description")
      descriptionRow.required = true 
      section1.addFormRow(descriptionRow)
      
      let isPublicRow = XLFormRowDescriptor(tag: "isPublic", rowType: 
        XLFormRowDescriptorTypeBooleanSwitch, title: "Public?")
      isPublicRow.required = false 
      section1.addFormRow(isPublicRow)
      
      let section2 = XLFormSectionDescriptor.formSectionWithTitle("File 1") as 
        XLFormSectionDescriptor
      form.addFormSection(section2)
      
      let filenameRow = XLFormRowDescriptor(tag: "filename", rowType: 
        XLFormRowDescriptorTypeText, title: "Filename")
      filenameRow.required = true 
      section2.addFormRow(filenameRow)
      
      let fileContent = XLFormRowDescriptor(tag: "fileContent", rowType: 
        XLFormRowDescriptorTypeTextView, title: "File Content")
      fileContent.required = true 
      section2.addFormRow(fileContent)
    
      self.form = form 
    }
    

    这里没有把isPublicRow设置为必填字段,因此用户也没有必要必须点击设置的开关。用户使用默认值就可以了,而且表单也知道该如何进行处理。

    接下来还需要添加一些按钮,左上角添加一个取消按钮,右上角添加一个保存按钮。当用户点击取消按钮的时候,我们将返回上一个页面就可以了。对于保存按钮则需要来实现:

    class CreateGistViewController: XLFormViewController { 
      override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: 
        UIBarButtonSystemItem.Cancel, target: self, action: "cancelPressed:") 
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: 
        UIBarButtonSystemItem.Save, target: self, action: "savePressed:")
      }
      
      func cancelPressed(button: UIBarButtonItem) { 
        self.navigationController?.popViewControllerAnimated(true)
      }
      
      func savePressed(button: UIBarButtonItem) { 
        // TODO: implement
      }
      
      ...
    }
    

    在你的APP中添加一个表单,来创建一个新的对象。接下来我们将完成验证及POST请求。

    假如现在你运行APP,并在表视图中点击+按钮,那么将显示一个表单视图,当你填写值后点击保存按钮的时候,很不幸,一点反映都没有。因为,我们还没有实现相应的处理,接下来我们将完成这个处理。首先调用XLMForm内置的formValidationErrors函数来检查必填字段。如果有错误,使用内置的showFormValidationError来显示这些错误,以便用户可以修正:

    func savePressed(button: UIBarButtonItem) {
      let validationErrors = self.formValidationErrors() as? [NSError] 
      if validationErrors?.count > 0 {
        self.showFormValidationError(validationErrors!.first)
        return
      }
    

    如果没有错误,我们将先关闭表视图的编辑模式:

    self.tableView.endEditing(true)
    

    然后可以使用之前所设置的tag来获取用户的输入,获取方式如下:

    form.formRowWithTag("tagForRow")?.value as? Type
    

    首先是isPublic,如果用户没有点击开关,默认使用false:

    let isPublic: Bool
    if let isPublicValue = form.formRowWithTag("isPublic")?.value as? Bool {
      isPublic = isPublicValue 
    } else {
      isPublic = false
    }
    

    接下来是String属性。这里,可以使用一个if let语句就可以获取三个属性(它们不会为空,因为之前已经做了必填检查)。然后就可以使用它们的值创建一个文件了:

    if let description = form.formRowWithTag("description")?.value as? String, 
      filename = form.formRowWithTag("filename")?.value as? String, 
      fileContent = form.formRowWithTag("fileContent")?.value as? String {
        var files = [File]()
        if let file = File(aName: filename, aContent: fileContent) {
          files.append(file)
        }
    

    最后,我们使用用户的输入发起一个API调用。如果调用失败给出相应的提示,调用成功将返回我的Gist列表视图:

    GitHubAPIManager.sharedInstance.createNewGist(description, isPublic: isPublic, 
      files: files, completionHandler: {
      result in
      guard result.error == nil, let successValue = result.value
        where successValue == true else { 
        if let error = result.error {
          print(error)
        }
        
        let alertController = UIAlertController(title: "Could not create gist", 
          message: "Sorry, your gist couldn't be deleted. " +
          "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) 
        alertController.addAction(okAction)
        
        self.presentViewController(alertController, animated:true, completion: nil)
        return
      }
      self.navigationController?.popViewControllerAnimated(true) 
    })
    

    完整的代码如下:

    func savePressed(button: UIBarButtonItem) {
      let validationErrors = self.formValidationErrors() as? [NSError] 
      if validationErrors?.count > 0 {
        self.showFormValidationError(validationErrors!.first)
        return
      }
      self.tableView.endEditing(true)
      let isPublic: Bool
      if let isPublicValue = form.formRowWithTag("isPublic")?.value as? Bool {
        isPublic = isPublicValue 
      } else {
        isPublic = false 
      }
      if let description = form.formRowWithTag("description")?.value as? String, 
        filename = form.formRowWithTag("filename")?.value as? String, 
        fileContent = form.formRowWithTag("fileContent")?.value as? String {
          var files = [File]()
          if let file = File(aName: filename, aContent: fileContent) {
            files.append(file)
          }
    
          GitHubAPIManager.sharedInstance.createNewGist(description, isPublic: isPublic,
            files: files, completionHandler: {
            result in
            guard result.error == nil, let successValue = result.value
              where successValue == true else { 
              if let error = result.error {
                print(error)
              }
              let alertController = UIAlertController(title: "Could not create gist", 
                message: "Sorry, your gist couldn't be deleted. " +
                "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) 
              alertController.addAction(okAction)
              self.presentViewController(alertController, animated:true, completion: nil)
              return
            }
            self.navigationController?.popViewControllerAnimated(true) 
        })
      }
    }
    

    现在你可以进行测试了。

    对你的表单进行验证,并调用相应的创建API。

    运行后发现有什么问题了么?当我们返回到MasterViewController后,即使已经调用了loadGists新增加的Gist也还是没有显示:

    override func viewDidAppear(animated: Bool) { 
      super.viewDidAppear(animated)
      
      let defaults = NSUserDefaults.standardUserDefaults() 
      if (!defaults.boolForKey("loadingOAuthToken")) {
        loadInitialData()
      }
    }
    

    看来这里的响应被缓存了。所以我们需要告诉Alamofire这里不需要使用缓存。

    因为Alamofire使用NSURLCache,所以在GitHubAPIManager中添加一个简单的方法就可以清除缓存了:

    func clearCache() {
      let cache = NSURLCache.sharedURLCache() 
      cache.removeAllCachedResponses()
    }
    

    当成功创建了Gist后,就可以清除缓存,以便重新加载:

    func createNewGist(description: String, isPublic: Bool, files: [File], 
      completionHandler: (Bool?, NSError?) -> Void) {
      ...
      
      alamofireManager.request(GistRouter.Create(parameters)) 
        .response { (request, response, data, error) in
          if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) { 
            completionHandler(.Failure(authError))
            return
          }
          if let error = error {
            print(error) 
            completionHandler(.Success(false)) 
            return
          }
          self.clearCache() 
          completionHandler(.Success(true))
        }
    }
    

    再运行,你就会发现刚创建的Gist已经会显示在列表中了。

    小结

    本章的代码

    相关文章

      网友评论

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

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