重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习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我们需要以下两个字段:
- Description: 字符串
- Public 还是 Private:布尔值
每个文件我们需要的内容有:
- 文件名称:字符串
- 文件内容:字符串
下面让我们看看如何获取用户的输入并将它们转换为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已经会显示在列表中了。
小结
本章的代码。
网友评论