2014 年 6 月 Apple 发布 Swift 以来,如何在 Swift 中进行网络编程一直成为程序猿们关注的焦点。甚至,Chris Lattner,Swift 的作者之一,也发推说过,在 Swift 中解析 JSON 还有很长的路要走。因此,许多人开始寻求替代方案。尽管,在 Swift 中也有处理 JSON 解析的内建类,但是对开发者来说并不是很友好。幸运的是,Alamofire 出现了。Alamofire 是一个可以帮助我们解析 JSON 的强有力网络库,它由 Objective-C 中同类网络库 AFNetworking 的作者编写。
在这个又臭又长、近乎 3500 多词(译者注:in English)的教程中,我们将探讨一系列广泛的网络基本话题,并建立一个假日待办应用。
同时,你会从本教程中学到:如何使用和解析 JSON,如何自定义服务器端,如何使用 Heroku 和 MongoLab 等工具,HTTP 的工作原理(包括 GET,POST 和 DELETE 请求),如何使用 git 和终端(terminal)以及如何使用 Cocoapods。如果你觉得上面提到的内容太多了,那就对了,拿一杯咖啡,就让我们开始吧。
哦,AppCoda 的所有作者祝大家节日开心!:blush:
注意:本教程是一个进阶教程,涵盖了很多东西。而且,我假设你已经对 iOS 和 Swift 有了很坚实的了解。文章中诸如 tableviews, autolayout,delegate 等话题都不会深入的解释原理。你如果记不清这些内容,可以先去学习我们推出的 优秀课程 ,然后再回来看本教程。
为了实现本教程要实现的功能,我已用 Node.js 写了一个服务器后端。这里需要给那些对它不熟悉的人解释一下,Node.js 是一个基于 Javascript、运行在 Google Chrome 的 V8 引擎中的运行时环境。长话短说,总之它是一个特别可靠,速度特别快,特别厉害的东西,哈哈。
为了搞定这个后端,我同时也使用了 Restify 和 MongoDB。MongoDB 是在 Web 开发人员中很流行的一个 no-SQL 数据库。我们可以使用 MongoDB 存储所有我们相关的数据。
当我刚开始使用 Node 的时候,我不知道这些东西都是怎么运行的,其他我所浏览的一些博客也从没有解释 Node 到底是怎么工作的。因此,尽管这是个 iOS 的博客,但我还是要介绍一下 Javascript 和 Node 服务器的工作原理。
我搜遍了网络,都没有一个详细的教程引导你创建一个 API 与 iOS 应用程序交互的步骤,从现在开始就有了。
像我之前提到的一样,Node.js 是一个很强大的服务器端开发技术,它建立在 Chrome 的运行时环境上。因此,它是高度异步的和非阻塞的(如果你不知道我说的是什么意思,其实很简单,大概就是:使用主线程或者应用的主要部分不会被阻塞)。多线程是一种可以防止延迟且能提高项目效率的编程技术。你把应用想象成一条高速公路,如果只有一条通道,却有 20 辆车要通过,那么他们就很有可能会堵车。如果一条高速公路有三条都有出入口的通道,那么堵车的机会就很小。多线程就可以这样来理解。在一个多线程的环境里,代码执行在不同的线程就可以避免应用阻塞,从而防止程序奔溃。
Node 是由 Joyent 开发并维持的,Joyent 是一家位于旧金山的云计算公司。
如果你仍然不清楚所有这些是怎么运行的,想想后端具体干了些什么吧。下面列出来一些:
我们的 MongoDB 放在 MongoLab 的主机上,Node 服务器放在 Heroku 上。Heroku 由 Salesforce 提供支持,可以作为 Node,Rails,Python等应用的主机服务商。MongoLab 也是一家可以当 MongoDB 主机的服务商。
在我们开始写代码之前,你应该了解 HTTP 请求以及如何在我们的应用里使用。
GET 请求 - GET 请求会查询我们的数据库,然后获取内容。GET 请求可以被限制,使得其只能获取一个、多个或全部的内容。事实上,每次你访问 google.com 或浏览你的 Facebook/Twitter 主页,你都会发起 GET 请求(可能你之前都不知道这个东西)!
POST 请求 - POST 请求会发送数据到服务器,然后保存这个数据。举个例子,当你在 Facebook 或 Twitter 上写好文字,然后按 Post/Tweet 按钮的时候,你就发起了 POST 请求。
UPDATE 请求 - UPDATE 请求可以让你修改已经存在的内容。当你编辑一条 Facebook 消息时,其实使用到了 UPDATE 请求。
DELETE 请求 - DELETE 请求会删除对应的内容。当你按了删除按钮删除 Facebook 或 Twitter 消息的时候,其实是调用了 DELETE 请求。
以上这四个请求类型是基于 REST 协议的。Internet 能运行就是由这些请求组成的。你可能也听说过 CRUD 这个缩写词,CRUD 是由 C reate, R ead, U pdate 和 D elete 的首字母组成的。很显然,这些单词就和 POST,GET,UPDATE 和 DELETE 是一一对应的。
帅气!现在我们已经对 HTTP 协议有一定的理解了,我们可以进入到这次教程的核心部分了。
在我们使用 MongoLab 或 Heroku 之前,我们应该要确保 Node.js 能正常使用。
打开 Node.js官网 ,根据简单的引导下载 Node 到你的电脑上。
然后,到 npm 官网 下载 npm。
为了正确配置我们的后端,我们需要分别在 Heroku 和 MongoLab 上注册帐号。我们先从 MongoLab 开始吧,去 MongoLab 官网 注册帐号。
确保选择的是 single-node(免费),填上你数据库的名字。我这边取名为 alamofire-db (以 db 为后缀表示是一个数据库,这是比较普遍的命名规范)。
接下来,登录你的数据库,定位好 MongoDB 数据库的 URI。
马上就让你添加一个新的数据库帐号,输入用户名和密码。不要忘记密码。
现在返回到你设置 URI 的页面,修改成新的地址。比如:
mongodb://<dbuser>:<dbpassword>@ds057954.mongolab.com:57954/alamofire-db
替换成:
mongodb://gregg:test@ds057954.mongolab.com:57954/alamofire-db
MongoLab 搞定!
现在去 Heroku.com ,免费注册后,打开 heroku toolbelt 页面 。
跟随指南,成功安装后,打开终端并登录 heroku。如果你之前从未使用过终端,不用担心。本教程会多次使用终端,这样你最终就会对终端的使用有一个清晰的认识。
一旦你在终端上登录 heroku,可以使用 cd 命令(cd 代表改变目录)进到对应目录,将之前从 dropbox 下载的工程文件夹移动进去。
按下回车键就可以执行这行命令了。干的不错,现在我们可以用 git 提交(Push) 东西到 heroku 了。
在终端中键入以下命令:
git init
git add .
git commit -m "First Commit"
这三行命令,初始化了一个仓库(repository,简写为 repo),并添加了当前目录下的所有文件到这个仓库,最终提交并保存。
git 是一款很流行的版本控制软件。
现在你可以看终端里应该和下图的内容差不多:
因为你之前已经成功安装了 heroku toolbelt,所以你现在可以在终端里键入 heroku login ,并输入帐号密码。敲回车后继续,如果帐号密码没问题的话,你的 Email 会以蓝绿色高亮显示。
现在,键入 heroku create 来创建一个新的 heroku 应用。Heroku 会创建一个新的带有域名的应用给你。比如,我的就是 https://whispering-plains-1537.herokuapp.com/。
现在,键入 git push heroku master 来把你新建的应用发送到 heroku。
如果一切顺利的话,会显示如下图(其中的某一些设置可能会不同)。
让我们从下载示例工程开始, 链接在这里 。打开你最喜欢的文本编辑器(我这边用的是 Sublime Text 2;可以在 这里 下载免费版,如果你支持的话也可以购买),然后继续。
Javascript 在很大程度上是和 Swift 很相似的。我们之后会使用 express 和 mongoose 两个著名 node 包。请确保你已经在系统上安装 npm 和 node 包管理器。
Express 是 Node.js 中的一个「快速、强大而又轻量级」的网络框架,它可以轻松解决路由(Route)问题。你问什么是路由?路由就是你与网络交互的方式。每次你打开 google.com 的时候,其实你访问的是根主页,即 google.com/。假如你访问 google.com/hello,那就是另外一个路由了。我们接下来将要定义一个能访问我们数据库的路由。
你可以从 expressjs.org 官网上学习更多关于 express 的知识。
下面是示例代码:
var express = require('express'); // 1
var app = express(); // 2
// 当一个 GET 请求访问主页的时候,会返回 hello world
app.get('/', function(req, res) { // 3
res.send('hello world'); // 4
});
第一行代码设置了一个叫 express 的变量。第二行代码,把 express 初始化后赋值给一个叫 app 的变量。在第三行代码,app 这个变量代表了 express 环境,调用它的 get() 方法(形式类似 Swift)。当一个用户访问 / 根主页的时候,就会显示「hello world」。这是 express 作为路由的一个例子。如果需要更多信息,可以访问 express 官网 查看。
现在,我们已经配置好了 mongo 数据库的环境,接下来让我们来使用 cURL 请求测试一下功能。cURL 是一款命令行程序,它可以发送 HTTP 请求。我们将会先使用 cURL 做一下实验,然后再迁移到 Alamofire 去。
打开你的文本编辑器(再次顺便说一下,我用的是 Sublime),同时打开 app.js 文件。正如你看到的,应用被分割成了一个 model 和路由文件(就是你刚打开的 app.js 文件)。model 文件可以建立模式(schema)或数据库结构。让我们先来简单看看这个文件吧。
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var TodoSchema = new Schema(
{
name: String
});
mongoose.model('employees', TodoSchema);
我们可以使用 mongoose,它是一个用在应用与 mongo 之间作为接口的 npm 包。我起初在构建一个雇工跟踪应用,并把 model 命名为 employee,但是可能会随时修改这个 model。我保留着它,是因为这个教程的接下来部分可能会用到。
Mongoose 能很方便的提供与 mongoLab 的 heroku node 应用连接并提供相应的接口。这的确非常方便。
路由文件里存的是我们将会 输出 到 app.js 文件的内容。不用太担心这个输出——它是 node 中一个比较先进的特性,也超出了本教程的范围。
注意第 26 行的 newTodo 。正如你可能猜到的,这行代码创建了一个新的 todo。
var emp = new Todo(req.body);
emp.save(function(err){
if (err) {
res.send('Error occurred');
return console.log(err);
}
res.send(emp);
});
我们把 Todo 对象(在第四行定义了一个与 mongoose 连接的对象)赋值给一个叫 emp 的变量,并设置 req.body(req 代表请求,它会发送给我们数据,同时,res 代表回复,它会返回我们的要返回的东西)。
随意浏览一下文件中剩下的方法。
现在回到 app.js 文件,这里是整个应用的主要部分。接下来列出来一些这个文件里的重点部分(译者注:对照下图看):
以上这些,能让你了解到一些 Javascript 应用的基本运作知识。但是,毕竟这篇教程不是主讲 Javascript 的,我不会继续深究。当然,我还是鼓励你们去研究一下 express 和 mongoose。
在我们的 node 应用开启状态下,我们可以执行一些 cURL 请求来做测试。一旦我们做完测试,就可以迁移到 Alamofire 上去了。
在终端里执行下面的代码(记得将 url 修改成你自己对应的 heroku url)。
curl -i -H "Accept: application/json" "https://rocky-meadow-1164.herokuapp.com/todo"
命令行中的 -i 和 -H 参数,表示我们将要接收什么东西。我们会接收 JSON 并将 JSON url 追加到请求的末尾。
你应该能看到有数据返回了。和下图差不多。
正如你看到的,返回的数据就是我们想要得到的。如果你已经将 url 替换成你自己的,你可能什么也看不到,因为你的 mongodb 里现在还没数据。
加入你想要加一些数据到数据库里,你需要的就是下面的 POST 命令。
curl -H "Content-Type: application/json" -X POST -d '{"name":"Buy Presents"}' https://rocky-meadow-1164.herokuapp.com/todo
然后,你使用之前讲过的 GET 请求,就可以看到你刚才添加的「Buy Presents」的内容了。
curl -X DELETE 'https://rocky-meadow-1164.herokuapp.com/todo/5657901fee93910900cc54ed'
很棒!接下去我们不会讲 PUT 请求,因为在我们这个应用里暂时还用不上。但是它和其他的请求使用起来是差不多的。
让我们从新建一个名叫 TodoApp 的 Xcode 工程开始吧。因为假期就要到来,我们应该有一种方式来跟踪这件事情。幸运的是,我们有我们的 node 应用可以帮忙。
虽然你可以手动安装 Alamofire(通过拖拽源文件到对应工程的方法),但是我们选择使用 Cocoapods。Cocoapods 是一款为 iOS 工程提供依赖管理的工具。在使用 Cocoapods 的时候,开发者可以轻松的添加框架或第三方类库。如果你之前没有使用 Cocoapods,强烈推荐你去使用。
接下来,在终端里运行以下命令可以确保你在接下来的步骤后成功安装 Cocoapods。
$ gem install cocoapods
然后,通过 cd 命令进入你工程所在的目录,键入以下命令。
vim Podfile
Vim 是一款系统自带的命令行编辑器,与 Sublime Text 或 TextMate 类似。我们现在要新建一个 Podfile 的文件,Cocoapods 每次都会去这个文件里查询是否需要更新工程的 pod(包括各种的依赖)。
在 Podfile 这个文件里键入如下内容:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!
pod 'Alamofire', '~> 3.0'
然后,按 ESC 键,并输入 :wq
,再敲回车。其中,wq 表示保存并退出。
我们现在已经成功创建 Podfile 并且保存了,为了安装 CocoaPods,在终端里输入以下命令:
pod install
敲了回车后,如果一切都设置好的话,大概会呈现下图显示的内容。
这时候,你可以看到命令行里要求你关闭当前打开的 Xcode 并且以后都用 .xcworkspace 为后缀的文件来打开工程。
下面这个命令能够非常方便地打开当前目录的 finder 界面。到此为止我们在 Terminal 中的操作就那么多,看上去一天之内有那么多就够了!
open .
打开 ViewController.swift,让我们继续吧。
在打开的 ViewController.swift 里,输入以下代码来导入 Alamofire:
import Alamofire
在 viewDidLoad() 方法里键入以下代码来使用 Alamofire。
Alamofire.request(.GET, "https://rocky-meadow-1164.herokuapp.com/todo") .responseJSON { response in // 1
print(response.request) // original URL request
print(response.response) // URL response
print(response.data) // server data
print(response.result) // result of response serialization
if let JSON = response.result.value {
print("JSON: /(JSON)")
}
}
在第一行代码中,我们声明了一个 GET 请求,并且传入了一个我们需要的 URL。运行当前的应用,看看返回的是什么。如果一切都设置正确的话,你会看到返回的是 JSON 数据。
现在,打开 Main.storyboard,添加一个 tableview 到 view controller,并将视图控制器嵌入到 navigation controller。你的 storyboard 现在看起来应该跟我的一样,如下图(值得注意的是,现在返回的 JSON 数据还只是显示在控制台上,我们要将其显示出来。)。
将以下代码复制并粘帖到你的 Viewcontroller.swift 文件里。
import UIKit
import Alamofire
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var jsonArray:NSMutableArray?
var newArray: Array<String> = []
override func viewDidLoad() {
super.viewDidLoad()
Alamofire.request(.GET, "https://rocky-meadow-1164.herokuapp.com/todo") .responseJSON { response in
print(response.request) // original URL request
print(response.response) // URL response
print(response.data) // server data
print(response.result) // result of response serialization
if let JSON = response.result.value {
self.jsonArray = JSON as? NSMutableArray
for item in self.jsonArray! {
print(item["name"]!)
let string = item["name"]!
print("String is /(string!)")
self.newArray.append(string! as! String)
}
print("New array is /(self.newArray)")
self.tableView.reloadData()
}
}
// Do any additional setup after loading the view, typically from a nib.
}
}
我初始化了两个数组 jsonArray 和 newArray,用 for 循环遍历了返回数据的那个 jsonArray,将其中的每个数据保存到 newArray 中。
我使用 POST cURL 请求在数据库里多添加了一些数据。用法类似,不再赘述。
你可以试试下面代码演示的 GET 请求的极致精简写法。
Alamofire.request(.GET, "https://rocky-meadow-1164.herokuapp.com/todo").responseJSON { response in debugPrint(response) }
接下来,在文件顶部的 UIViewController 定义后面添加 UITableViewDelegate 和 UITableViewDataSource。并且,在 viewDidLoad() 方法里键入如下代码:
self.tableView.dataSource = self
self.tableView.delegate = self
最后,添加 UITableView 的 delegate 方法。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.newArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = self.newArray[indexPath.row]
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
正如你看到的,我们的 tableview 已经成功显示数据了。
现在,让我们来添加一个按钮,用来添加数据到列表中。首先,先在 storyboard 里添加一个叫 AddViewController 的类,并用 segue 的方式连接起来。你的 storyboard 应该和下图差不多。
在你的 AddViewController.swift 文件里,为 textfield 建立一个 IBOutlet(命名为 textView)和为 Save 按钮建立一个 IBAction。在 Save 按钮代码下面键入如下代码:
Alamofire.request(.POST, "https://rocky-meadow-1164.herokuapp.com/todo", parameters: ["name": self.textView.text!])
self.navigationController!.popViewControllerAnimated(true)
正如你看到的,Alamofire 大大简化了发送 POST 请求的过程。
接下来,我们来对 ViewController.swift 文件进行重构,确保我们在保存数据后能及时更新列表。删除 viewDidLoad() 方法里 GET Alamofire 的代码,用以下的 downloadAndUpdate 方法代替。
func downloadAndUpdate() {
Alamofire.request(.GET, "https://rocky-meadow-1164.herokuapp.com/todo") .responseJSON { response in
print(response.request) // original URL request
print(response.response) // URL response
print(response.data) // server data
print(response.result) // result of response serialization
if let JSON = response.result.value {
self.jsonArray = JSON as? NSMutableArray
for item in self.jsonArray! {
print(item["name"]!)
let string = item["name"]!
print("String is /(string!)")
self.newArray.append(string! as! String)
}
print("New array is /(self.newArray)")
self.tableView.reloadData()
}
}
}
现在,在 viewWillAppear() 方法里调用这个方法,如下。
override func viewWillAppear(animated: Bool) {
self.downloadAndUpdate()
}
如果你再次编译并运行这个应用,就会发现每次添加新的 todo 后都会重新加载。但是,这是为什么呢?
这就关系到 view controller 的生命周期,这里我就简短讨论一下。viewDidLoad() 会在 view 初始化后并且所有控件都结束加载后被调用。问题就出在,当你从已经加载的 ViewController 上加载另外一个 view(比如 AppViewController)时,viewDidLoad 方法不会被调用(之前已经初始化过)。viewWillAppear 方法会在每次 view 在屏幕上显示时调用。因为我们需要在再次显示 ViewController.swift 时候显示,所以这个方法刚好可用。
现在在刚才的 newArray 下面添加一个 IDArray。
var IDArray: Array<String> = []
接下来,更新 downloadAndUpdate 方法的相应部分,代码如下。
self.newArray.removeAll() // NEW
self.IDArray.removeAll() // NEW
Alamofire.request(.GET, "https://rocky-meadow-1164.herokuapp.com/todo") .responseJSON { response in
print(response.request) // original URL request
print(response.response) // URL response
print(response.data) // server data
print(response.result) // result of response serialization
if let JSON = response.result.value {
self.jsonArray = JSON as? NSMutableArray
for item in self.jsonArray! {
print(item["name"]!)
let string = item["name"]!
let ID = item["_id"]! // NEW
self.newArray.append(string! as! String)
self.IDArray.append(ID! as! String) // NEW
}
print("New array is /(self.newArray)")
self.tableView.reloadData()
}
}
两行带有 NEW 注释的代码是新添加的。从代码的本质上来说,我们在循环中获得对应的 ID 并保存到数组 IDArray 中。同样,我们也需要将不需要的数据从列表中删除并重置。
添加 commitEditingStyle 方法,以调用 DELETE 请求来删除对应的不需要数据。
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
print("ID is /(self.IDArray[indexPath.row])")
Alamofire.request(.DELETE, "https://rocky-meadow-1164.herokuapp.com/todo//(self.IDArray[indexPath.row])")
self.downloadAndUpdate()
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
正如你看到的,以上代码遵循了我们应用的 API,即通过传入 /todo/ID 来调用 DELETE 请求删除对应的数据。
同时,我们用比较简单的 Alamofire 方法来调用 DELETE 请求并删除了对应的数据。
至此,你现在已经拥有了一个功能完备的 todo 应用了。因此,让我们来总结一下本次教程吧。
本教程探索了很多东西。从 Javascript 的 node 到 express,从 MongoDB 到 cURL,从终端到 Cocoapods,以及最后的 Alamofire,我们深入了解了 REST API 的创建过程和网络的工作流程。你通过本次教程应该已经坚实的掌握了以下内容:
这真是一个大教程,我感谢你坚持和我走到这里。所有的源代码可以在 这里 下载,其中包含了 node 应用和 iOS 应用。
有任何问题和想法都可以在教程下面留言评论。下次见!
本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问http://swift.gg。