注意:更新iOS 9.3, and Swift 2.2- 2016年4月2日
原文 https://www.raywenderlich.com/121540/alamofire-tutorial-getting-started
Alamofire 是一个为 iOS 和 Mac OS,基于 Swift 的 HTTP 网络库。它在 Apple 的基础网络库上提供了一个优雅的接口,简化了许多常见的网络任务。
Alamofire 提供链式地请求与响应方法,JSON 参数以及响应的序列化和授权,等等。在本教程中,你将使用 Alamofire 来完成基本的网络任务,包括上传文件以及使用第三方库 RESTful API 来请求数据。
Alamofire 的优雅是一个事实,它完全由 Swift 编写,没有任何 Objective-C 的代码,也不是继承自 AFNetworking。
你需要理解一些 HTTP 的基本概念,以及了解苹果的网络类,比如NSURLSession和NSURLConnection。
虽然 Alamofire 掩盖了一些实现细节,但是如果你需要解决你的网络请求,有一些背景知识也是不错的。你也需要使用 CocoaPods 把 Alamofire 安装到你的教程工程中。
入门指南
下载示例初始工程.
这个Alamofire应用程序名为PhotoTagger;下载完成后,打开工程运行,会发现中间有一个按钮可以选择图片,点击按钮会访问系统的相册,随便选择一张图片,背景就会被这张图片代替。但是现在的工程是一个初始工程,好多功能没有完成,如下图:
在Xcode中构建和运行这个项目,您将看到以下信息:
点击选择照片,然后选择一个照片。背景图像将被替换为你选择的图片。
打开的Main.storyboard,你就会看到另外的用于显示标签和颜色的页面已经添加好了。剩下的工作就是上传图像和获取标签和颜色。
Imagga API
Imagga是一个图像识别平台,它提供了图像标签api为开发人员和企业构建可延展,密集型云应用程序。你可以看一下demo.
您将需要创建一个免费的Imagga开发者账户。Imagga需要一个授权添加在每个HTTP的授权头,所以只有拥有帐户的人才可以使用他们的服务。访问https://imagga.com/auth/signup/hacker并填写表单。在您创建完您的帐户后,请查看dashboard:
在Authorization选项是一个 token,后面你会用到。
注意:确保你拷贝了整个 token,因为这个 token 很长。
你将会使用 Imagga 作为上传图片的服务器,给图片标注和设置颜色。你可以在http://docs.imagga.com了解到所有的 API。
安装库文件
在项目的主目录下创建一个名为Podfile的文件的并添加以下内容:
platform:ios,'9.0'
inhibit_all_warnings!
use_frameworks!
target'PhotoTagger'do
pod'Alamofire','~> 3.1.2'
end
接下来,用pod install命令安装CocoaPods中的第三方框架。如果你没有CocoaPods安装在你的机器上,看看如何使用CocoaPods迅速教程中获得更多信息How to Use CocoaPods with Swift。
关闭项目并打开新创建的PhotoTagger.xcworkspace。编译并运行您的项目,你不会发现应用程序运行的任何视觉的变化。这很好——你的下一个任务是添加一些HTTP调用RESTful服务来检索JSON。
REST, HTTP, JSON — 都是什么?
如果你来到这个Alamofire教程并且对于如何使用第三方服务用于互联网没有太多经验,你可能想知道这些缩写是什么意思!
HTTP:是应用协议,或一组规则,网站使用从web服务器传输数据到你的屏幕上。你应该见过HTTP(或HTTPS)中排列在每个你输入到web浏览器的URL面前。你可能听说过的其他应用程序协议,如FTP、Telnet和SSH。HTTP方法定义了几个请求,或动词,客户端(web浏览器或应用程序)使用来表明所需的行动:
GET: 用于获取数据,比如一个网页,但不改变任何服务器上的数据。
HEAD: 和GET相同,但只有返回头部信息并没有实际的数据。
POST: 用于发送数据到服务器,常用当填充表单并单击submit。
PUT: 用于发送数据到特定位置。
DELETE: 从提供的特定位置删除数据。
REST,或具象状态传输,是一组规则设计一致、易于使用和维护的web api。REST有几个架构规则,执行诸如不跨请求持久化状态,发出请求缓存,并提供统一的接口。这使得像你一样的应用程序开发人员很容易将API集成到应用程序中,而不需要跟踪数据在请求的状态。
JSON支持JavaScript对象表示法,它提供了一个简单的、人类可读的和轻便的机制在两个系统之间传输数据。JSON有数量有限的数据类型:字符串、布尔值、数组、对象/字典,空和数字;没有整数与小数之间的区别。苹果供应NSJSONSerialization类来帮助你将内存中对象转换为JSON,反之亦然。
HTTP、REST和JSON的结合可为一个开发人员提供的相当一部分可用的web服务。试图了解每一个小模块是如果工作的。像Alamofire这样的库可以帮助减少使用这些服务的复杂性,让你更快地启动和运行比你可能没有帮助。
Alamofire有什么好处?
你为什么需要Alamofire?苹果已经提供了NSURLSession和其他类通过HTTP下载内容,那么为什么还要另一个第三方库将问题变得复杂?
简短的回答是,Alamofire基于NSURLSession,但它使你免受写作样板代码使得编写网络代码更加容易。你可以很轻松的在互联网上访问数据,并且您的代码将更干净、更容易阅读。
Alamofire有几个主要功能:
.upload: 上传文件多部分、流、文件或数据的方法。
.download: 下载文件或继续一个已经在进程中的下载任务。
.request: 每一个HTTP请求与文件传输无关。
这些Alamofire函数是模块范畴的,而不是一个类或结构体。有底层块Alamofire类和结构体,像Manager,Request, 和Response;然而,你不需要完全理解整个结构就可以开始使用它。
这里有一个用苹果NSURLSession和Alamofire处理相同的网络操作的例子:
// With NSURLSession
public func fetchAllRooms(completion:([RemoteRoom]?)->Void){
let url = NSURL(string:"http://localhost:5984/rooms/_all_docs?include_docs=true")!
let urlRequest = NSMutableURLRequest(
URL:url,
cachePolicy:.ReloadIgnoringLocalAndRemoteCacheData,
timeoutInterval:10.0*1000)
urlRequest.HTTPMethod="GET"
urlRequest.addValue("application/json", forHTTPHeaderField:"Accept")
let task = urlSession.dataTaskWithRequest(urlRequest){(data, response, error)->Void in
guard error == nil else {
print("Error while fetching remote rooms:\(error)")
completion(nil)
return
}
guard let json = try ? NSJSONSerialization.JSONObjectWithData(data!, options:[])as?[String:AnyObject] else {
print("Nil data received from fetchAllRooms service")
completion(nil)
return
}
guard let rows = json["rows"]as?[[String:AnyObject]]{
print("Malformed data received from fetchAllRooms service")
completion(nil)
return
}
var rooms = [RemoteRoom()
for roomDict in rows{
rooms.append(RemoteRoom(jsonData:roomDict))
}
completion(rooms)
}
task.resume()
}
与此相反:
// With Alamofire
func fetchAllRooms(completion:([RemoteRoom]?)->Void){
Alamofire.request(
.GET,
"http://localhost:5984/rooms/_all_docs",
parameters:["include_docs":"true"],
encoding:.URL)
.validate()
.responseJSON{(response)->Void in
guard response.result.isSuccess else {
print("Error while fetching remote rooms:\(response.result.error)")
completion(nil)
return
}
guard let value = response.result.valueas?[String:AnyObject],
rows=value["rows"]as?[[String:AnyObject]] else {
print("Malformed data received from fetchAllRooms service")
completion(nil)
return
}
var rooms=[RemoteRoom]()
for roomDict in rows{
rooms.append(RemoteRoom(jsonData:roomDict))
}
completion(rooms)
}
}
你可以看到,该请求使用Alamofire来设置相关方法时更短且更清晰。你可调用responseJSON(options:completionHandler:)并在响应对象上调用validate()简化了错误处理。
上传文件
打开ViewController.swift。并添加下面的类扩展的文件:
// Networking calls
extension ViewController{
func uploadImage(image:UIImage, progress:(percent:Float)->Void,
completion:(tags:[String], colors:[PhotoColor])->Void){
guard let imageData=UIImageJPEGRepresentation(image,0.5) else {
print("Could not get JPEG representation of UIImage")
return
}
}
}
上传图片到Imagga的第一步是获取正确格式的API。上图,图片选择器API返回一个UIImage实例并将其转换成JPEGNSData实例实例。
接下来,去到imagePickerController(_:didFinishPickingMediaWithInfo)并在imageView添加以下的设置:
// 1
takePictureButton.hidden=true
progressView.progress=0.0
progressView.hidden=false
activityIndicatorView.startAnimating()
uploadImage(
image,
progress:{ [unowned self] percent in
// 2
self.progressView.setProgress(percent, animated:true)
},
completion:{ [unowned self]tags, colors in
// 3
self.takePictureButton.hidden=false
self.progressView.hidden=true
self.activityIndicatorView.stopAnimating()
self.tags=tags
self.colors=colors
// 4
self.performSegueWithIdentifier("ShowResults", sender:self)
})
Alamofire的所有处理都是异步的,这意味着你的UI更新也会是以异步的方式:
隐藏上传按钮,显示进度视图和活动视图。
文件上传,你调用process处理程序更新的百分比。这个进度条显示的数量变化。
completion将在上传完成时执行。这里设置控件的状态回到初始的状态。
最后Storyboard在上传成功(或失败)结束后刷新结果页面。如果发生错误用户界面将不会改变。
接下来,在ViewController.swift之上添加以下:
import Alamofire
这允许您在代码中使用Alamofire模块提供的功能。
接下来,回到uploadImage(_:progress:completion:),并添加以下代码在你的UIImage实例中:
Alamofire.upload(
.POST,
"http://api.imagga.com/v1/content",
headers:["Authorization":"Basic xxx"],
multipartFormData:{multipartFormData in
multipartFormData.appendBodyPart(data:imageData, name:"imagefile",
fileName:"image.jpg", mimeType:"image/jpeg")
},
encodingCompletion:{encodingResult in
}
)
确保将替换Basic xxx标题取自Imagga仪表板。在这里你将JPEG数据blob(imageData)转换成一个MIME 分段请求Imagga contentendpoint。
确保将Basic xxx替换从Imaggadashboard获取的真实的认证头。你将JPEG数据包(imageData)转换成一个MIME 分段请求Imagga 的content 端点。
接下来,添加以下encodingCompletion闭包中:
switch encodingResult{
case.Success(letupload, _, _):
upload.progress{bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
dispatch_async(dispatch_get_main_queue()){
let percent = (Float(totalBytesWritten)/Float(totalBytesExpectedToWrite))
progress(percent:percent)
}
}
upload.validate()
upload.responseJSON{ response in
}
case.Failure(let encodingError):
print(encodingError)
}
这段代码调用 Alamofire 上传功能,通过在计算来更新进度条。
注意:Alamofire 并不会保证调用过程会在主队列上回调;因此你必须通过向主队列调度来更新用户界面。有一些 Alamofire 的回调,比如 responseJSON ,都会默认在主队列调用。例如:
dispatch_async(queue ?? dispatch_get_main_queue()){
let response = ...
completionHandler(response)
}
为了改变默认的做法,你需要给 Alamofire 提供一个 dispatch_queue_t 。
接下来,添加下面的代码到upload.responseJSON:
// 1.
guard response.result.isSuccess else {
print("Error while uploading file:\(response.result.error)")
completion(tags:[String](), colors:[PhotoColor]())
return
}
// 2.
guard let responseJSON=response.result.value as?[String:AnyObject],
uploadedFiles = responseJSON["uploaded"]as?[AnyObject],
firstFile = uploadedFiles.first as?[String:AnyObject],
firstFileID = firstFile["id"] as?String else {
print("Invalid information received from service")
completion(tags:[String](), colors:[PhotoColor]())
return
}
print("Content uploaded with ID:\(firstFileID)")
// 3.
completion(tags:[String](), colors:[PhotoColor]())
这边将逐行解释上面的代码:
检查响应是否成功;如果不成功,输出错误信息并调用completion。
检查响应的每个部分,验证所期望的类型是收到的实际类型,如果firstFileID没有被解析,那么输出错误信息,并调用completion。
调用completion来更新 UI。你没有任何下载的标志或颜色,所以简化调用数据为空的情况。
注意:每一个响应都有一个结果,包括枚举值和类型。使用自动验证,当它返回一个合法的 HTTP 码(在200-299之间的内容类型都被认为是可以接受的)。
您可以通过添加 .validate 手动执行验证,代码如下:
Alamofire.request(.GET,"https://httpbin.org/get", parameters:["foo":"bar"])
.validate(statusCode:200..<300)
.validate(contentType:["application/json"])
.response{response in
// response handling code
}
如果你在上传期间发生错误,UI 并不会显示错误;它仅仅返回没有标记或颜色给用户。这不是最好的用户体验,但对于这个Alamofire教程也算可以了。
编译并运行您的项目,选择一个图片,看文件上传进度条的变化,上传完成后,你会看到下面的信息:
你已经成功的上传图片!
获取数据
上传图像Imagga之后,下一步是获取Imagga分析产生的照片标签。
在ViewController的 extension 里面,uploadImage(_:progress:completion:)的下面添加如下的代码::
func downloadTags(contentID:String, completion:([String])->Void){
Alamofire.request(
.GET,
"http://api.imagga.com/v1/tagging",
parameters:["content":contentID],
headers:["Authorization":"Basic xxx"]
)
.responseJSON{ response in
guard response.result.isSuccess else {
print("Error while fetching tags:\(response.result.error)")
completion([String]())
return
}
guard let responseJSON = response.result.value as?[String:AnyObject] else {
print("Invalid tag information received from service")
completion([String]())
return
}
print(responseJSON)
completion([String]())
}
}
再次强调,一定要将Basic xxx替换成实际授权xxx头。这里你将执行HTTP GET请求,设置好 URL 以及对应的参数。
接下来,回到uploadImage(_:progress:completion:)并用以下代码替换成功后的回调处理:
self.downloadTags(firstFileID){ tags in
completion(tags:tags, colors:[PhotoColor]())
}
编译运行你的工程,上传一个文件,之后你在控制台就会看见返回的数据:
你不必在意这个教程中给出confidence分数,只需用到tag的名称。
接下来,回到downloadTags(_:completion:)并用以下代码替换.responseJSON里面的代码:
// 1.
guard response.result.isSuccess else {
print("Error while fetching tags:\(response.result.error)")
completion([String]())
return
}
// 2.
guard let responseJSON = response.result.value as?[String:AnyObject],
results = responseJSON["results"] as?[AnyObject],
firstResult = results.first,
tagsAndConfidences = firstResult["tags"]as?[[String:AnyObject]] else {
print("Invalid tag information received from the service")
completion([String]())
return
}
// 3.
let tags = tagsAndConfidences.flatMap({ dict in
return dict["tag"]as?String
})
// 4.
completion(tags)
下面是每步的代码:
检查响应是否成功;如果不成功,输出错误信息并调用 completion
对返回 json 数据进行解析。
迭代 tagsAndConfidences 数组,检索 tag.
调用 completion。
注意:你使用Swift的flatMap方法来进行迭代,这个方法在遇到值为nil的情况不会崩溃,并且会从返回结果中移除为nil的值。这可以让你使用条件解包(as?)来验证字典的值是否可以转换为一个字符串。
再一次编译运行你的工程,选择一涨图片,然后你会看到下面的界面:
非常流畅!Immaga是聪明的API。:]接下来,您将获取图像的相关颜色。
下面在ViewController的downloadTags(_:completion:)中添加以下扩展方法
func downloadColors(contentID:String, completion:([PhotoColor])->Void){
Alamofire.request(
.GET,
"http://api.imagga.com/v1/colors",
parameters:["content":contentID,"extract_object_colors":NSNumber(int:0)],
// 1.
headers:["Authorization":"Basic xxx"]
)
.responseJSON{ response in
// 2.
guard response.result.isSuccess else {
print("Error while fetching colors:\(response.result.error)")
completion([PhotoColor]())
return
}
// 3.
guard let responseJSON = response.result.value as? [String:AnyObject],
results = responseJSON["results"] as?[AnyObject],
firstResult=results.first as?[String:AnyObject],
info=firstResult["info"] as?[String:AnyObject],
imageColors=info["image_colors"] as?[[String:AnyObject]] else {
print("Invalid color information received from service")
completion([PhotoColor]())
return
}
// 4.
let photoColors = imageColors.flatMap({(dict)-> PhotoColor? in
guardletr=dict["r"] as?String,
g=dict["g"] as?String,
b=dict["b"] as?String,
closestPaletteColor=dict["closest_palette_color"] as?String else{
return nil
}
return PhotoColor(red:Int(r),
green:Int(g),
blue:Int(b),
colorName:closestPaletteColor)
})
// 5.completion(photoColors)
}
}
最后,返回uploadImage(_:progress:completion:)方法,在completion里面的success的情况,添加下面的代码:
self.downloadTags(firstFileID) { tags in
self.downloadColors(firstFileID ){ colors in
completion(tags:tags, colors:colors)
}
}
再一次编译运行你的工程,选择一涨图片,然后你会看到下面的界面:
你使用RGB颜色映射到PhotoColor结构改变视图的背景颜色。您已经成功上传图像Imagga并获取数据。你已经走了很长的路,但还有一些改进的空间。
优化PhotoTagger
你可能已经注意到了,在PhotoTagger里面有重复代码。
Alamofire 提供了一个简单的方法来排除重复的代码并且提供集中配置。这就需要创建一个结构体,遵循URLRequestConvertible协议,并且更新你的上传和请求调用。
创建一个 Swift 文件,点击File\New\File…,然后在 iOS 下面选择 Swift 文件,点击下一步,文件命名为 ImaggaRouter.swift,然后点击创建。
在你新建的文件中添加下面的代码:
import Foundation
import Alamofire
public enum ImaggaRouter:URLRequestConvertible {
static let baseURLPath = "http://api.imagga.com/v1"
static let authenticationToken = "Basic xxx"
case Content
case Tags(String)
case Colors(String)
public var URLRequest:NSMutableURLRequest{
let result:(path:String, method:Alamofire.Method, parameters:[String:AnyObject]) = {
switch self {
case.Content:
return("/content", .POST,[String:AnyObject]())
case.Tags(letcontentID):
let params=["content":contentID]
return("/tagging", .GET, params)
case.Colors(letcontentID):
let params=["content":contentID,"extract_object_colors":NSNumber(int:0)]
return("/colors", .GET, params)
}
}()
let URL = NSURL(string:ImaggaRouter.baseURLPath)!
let URLRequest = NSMutableURLRequest(URL:URL.URLByAppendingPathComponent(result.path))
URLRequest.HTTPMethod=result.method.rawValue
URLRequest.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField:"Authorization")
URLRequest.timeoutInterval=NSTimeInterval(10*1000)
let encoding = Alamofire.ParameterEncoding.URL
return encoding.encode(URLRequest, parameters:result.parameters).0
}
}
把Basic xxx替换为实际授权头,设置好 URL 以及对应的参数。这个router会帮助我们创建NSMutableURLRequest实例,并且提公布了三种情况:.Content,.Tags(String), or.Colors(String)。现在所有的模板代码都在这里,如果你需要更新它的话。
返回uploadImage(_:progress:completion:方法,并且把Alamofire.upload替换成下面的代码:
Alamofire.upload(
ImaggaRouter.Content,
multipartFormData:{ multipartFormData in
multipartFormData.appendBodyPart(data:imageData, name:"imagefile",
fileName:"image.jpg", mimeType:"image/jpeg")
},
/// original code continues...
然后替换downloadTags(_:completion:)方法里的Alamofire.request:
Alamofire.request(ImaggaRouter.Tags(contentID))
最后,更新downloadColors(_:completion:)代码里的Alamofire.request:
Alamofire.request(ImaggaRouter.Colors(contentID))
最后一次编译运行,所有的功能都像之前一样,也就意味着没有破坏你的 app,进行了代码重构。不错的工作!
深入学习
您可以下载完整的版本的这个Alamofire教程的项目。别忘了代替你的token
这Alamofire教程覆盖了非常基本的。你可以查看Alamofire官方文档深入学习https://github.com/Alamofire/Alamofire.
同样,你可以花些时间来了解更多关于苹果的NSURLSession:
Apple WWDC 2015 – 711 – Networking with NSURLSession
Apple URL Session Programming Guide
Ray Wenderlich – NSURLSession Tutorial