iOS 开发 iOS Developer程序员

(Swift)iOS Apps with REST APIs(十

2016-09-22  本文已影响162人  CD826

重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习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已经会显示在列表中了。

小结

本章的代码

上一篇 下一篇

猜你喜欢

热点阅读