转载请注明出处:http://leejunhui.com/2017/01/23/Alamofire-Tutorial/html
注:已更新到Alamofire 4
, Xcode 8.2
, iOS 10
以及Swift 3
.git
Alamofire
是一个为iOS
和macOS
打造的并基于Swift
的网络库.它在Apple的基础网络架构上提供了更加优雅的接口来简化繁重而经常使用的网络请求任务。Alamofire
提供了链式的request
/response
方法,JSON
的传参和响应序列化,身份认证和其余特性。在这篇Alamofire
教程中,你将使用Alamofire
来执行像从第三方提供的RESTful
api接口上传文件,请求数据等基本网络操做。Alamofire
的优雅之处在于它完彻底全是由Swift
写成的,而且没有从它的Objective-C
版本-AFNetworking
那继承任何特性。github
你应该对于HTTP
网络有一个概念性的理解,而且还应该接触过Apple的网络类好比URLSession
.固然Alamofire
中的一些细节会有点晦涩难懂,若是你有曾经解决过网络请求方面的经验也是极好的。你还须要使用CocoaPods
来将Alamofire
集成到项目中。web
下载项目代码 摸我json
下面介绍的项目名为PhotoTagger
,当咱们完成该项目后,能够实现:从相册中选择一张照片(若是你用的是真机测试的话,能够拍取一张照片),而后上传这张照片到一个第三方平台,平台会对该照片进行图像识别,而后返回一个tag的列表和这张图片的主色。swift
编译并运行该项目,你就能够看到以下界面:api
点击Select Photo
,而后选择一张照片,接着界面背景图就会变成你选择的那张照片。打开Main.stroyboard
,显示tags和colors的界面已经为你准备好了。剩下的工做就是上传照片和获取tags以及colors了。数组
Imagga是一个为开发者和企业提供构建可伸缩的,以图片为主的云产品的图像识别服务的网站。你能够先尝试下官方的自动标记服务demo:地址.浏览器
你须要再Imagga为本文的项目建立一个免费的开发者帐号。由于Imagga对于每一个HTTP
请求都进行了权限认证,因此只有拥有该网站帐号的用户才可使用他们的服务。来到 https://imagga.com/auth/signup/hacker,而后注册。完成注册后,检查下是否以下图所示:
在Authorization
区域列出了一个等会你将用到的token。该token将会被包含在每一个发往Imagga的请求的头部。
提示:请确认你拷贝了整个token字符串,确认滑动了最右边并检查是否拷贝完整。
稍后你将会用到Imagga的api来上传图片,tagging
api实现图像识别,colors
api用来颜色识别。你能够在http://docs.imagga.com上查看全部的api。
在项目主目录里建立Podfile
文件,内容以下:
platform :ios, '10.0' inhibit_all_warnings! use_frameworks! target 'PhotoTagger' do pod 'Alamofire', '~> 4.2.0' end
而后打开终端,来到项目主目录下,执行pod install
命令。若是你的Mac上没有安装过CocoaPods
的话,能够查看How to Use CocoaPods with Swift教程。
确保你使用的是
CocoaPods
的最新版本,不然可能会在安装第三方库时报错。本文撰写时,最新版本为1.1.1。
关闭Xcode项目,而后打开新生成的PhotoTagger.xcworkspace
文件。编译而后运行,跑起来的效果应该和以前保持一致。你下一步的任务是添加一些HTTP请求来从RESTful服务获取一些JSON数据。
若是你对使用HTTP不是特别有经验的话,那么你可能会很好奇这些缩写词究竟是什么含义。HTTP
是一种应用层协议,或者能够理解为一套网站用来从web服务器传输数据到你的电脑屏幕上的规范。你应该看到了你在浏览器中所输入的每一个URL前面都会有HTTP
或HTTPS
做为前缀。你可能也听过其它的应用层协议,好比FTP
,Telnet
和SSH
。HTTP
定义了一些客户端(你的浏览器或者app)用来指明所需的行为,请求的方法或者说动做:GET
: 获取数据,好比一个WEB页面。可是不更改服务器上的任何内容。HEAD
: 与GET
相同,可是服务器只会返回头部,并不会返回实际的数据。POST
: 发送数据到服务器,一般用于表单提交。PUT
: 发送数据到指定的路径。DELETE
: 删除指定路径的数据。
REST, 或者表征状态转移,是用来设计可持续的,易于使用的以及易于维护的WEB API的一套规范。REST有几个体系结构规则,用于强制执行某些操做,例如不在请求之间保持状态,使请求可缓存,并提供统一的接口。这样,像您这样的应用开发者就能够轻松地将API集成到您的应用中,而无需跟踪请求之间的数据状态。
JSON表明JavaScript Object Notation; 它提供了一个用于在两个系统之间用于传输数据的直接的,人类可读的和便携的机制。JSON的数据类型有:string,boolean,array,object / dictionary,null和number; 整数和小数之间没有区别。Apple提供JSONSerialization
类来帮助将内存中的对象转换为JSON,反之亦然。
HTTP
,REST
和JSON
的组合构成了做为开发人员可用的Web服务的很好的一部分。试图理解每一个细枝末节如何工做多是使人难以应对的。 像Alamofire这样的库能够帮助减小使用这些服务的复杂性,而且在没有帮助的状况下让您的运行速度更快。
你为何须要Alamofire
?苹果已经提供了URLSession
和其余类来经过HTTP获取内容,因此为何要使用一个第三方库来增长复杂度呢?
简单的答案是Alamofire
是基于URLSession
开发而成的,但它可让你免于编写样板代码,使编写网络代码更容易。你能够花费不多的精力就可让从网络上获取数据的操做代码变得更加简洁和易于阅读。Alamofire
有几个主要功能:
.upload:以multipart,流,文件或数据方法上传文件。
.download:下载文件或恢复正在进行的下载。
.request:每一个不与文件传输相关联的HTTP请求。
这些Alamofire
函数做用于模块,而不是类或结构体。 Alamofire
的基础部分是类和结构体,如SessionManager
,DataRequest
和DataResponse
; 可是,您不须要彻底了解Alamofire
的整个结构便可开始使用它。
下面是使用Apple的URLSession和Alamofire的请求函数进行相同网络操做的示例:
// With URLSession public func fetchAllRooms(completion: @escaping ([RemoteRoom]?) -> Void) { let url = URL(string: "http://localhost:5984/rooms/_all_docs?include_docs=true")! var urlRequest = URLRequest( url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10.0 * 1000) urlRequest.httpMethod = "GET" urlRequest.addValue("application/json", forHTTPHeaderField: "Accept") let task = urlSession.dataTask(with: urlRequest) { (data, response, error) -> Void in guard error == nil else { print("Error while fetching remote rooms: \(error)") completion(nil) return } guard let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { print("Nil data received from fetchAllRooms service") completion(nil) return } guard let rows = json["rows"] as? [[String: Any]] else { print("Malformed data received from fetchAllRooms service") completion(nil) return } let rooms = rows.flatMap({ (roomDict) -> RemoteRoom? in return RemoteRoom(jsonData: roomDict) }) completion(rooms) } task.resume() }
对比:
// With Alamofire func fetchAllRooms(completion: @escaping ([RemoteRoom]?) -> Void) { Alamofire.request( URL(string: "http://localhost:5984/rooms/_all_docs")!, method: .get, parameters: ["include_docs": "true"]) .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.value as? [String: Any], let rows = value["rows"] as? [[String: Any]] else { print("Malformed data received from fetchAllRooms service") completion(nil) return } let rooms = rows.flatMap({ (roomDict) -> RemoteRoom? in return RemoteRoom(jsonData: roomDict) }) completion(rooms) } }
你能够看到Alamofire
所须要的设置更加简单,函数可读性也更高。你使用responseJSON(options:completionHandler:)
来反序列化相应结果,而后调用validate()
方法来简化错误处理。如今是时候来实践使用Alamofire
了。
打开ViewController.swift文件,而后在顶部添加如下代码:
import Alamofire
这使你能够在代码中使用Alamofire
模块提供的功能,而后,在该文件中末尾处添加如下代码:
// Networking calls extension ViewController { func upload(image: UIImage, progressCompletion: @escaping (_ percent: Float) -> Void, completion: @escaping (_ tags: [String], _ colors: [PhotoColor]) -> Void) { guard let imageData = UIImageJPEGRepresentation(image, 0.5) else { print("Could not get JPEG representation of UIImage") return } } }
将图片上传到Imagga的第一步是将图片转换为适用于API的正确格式。上面的图片选择方法会返回一个转化成JPEG格式的图片实例。
而后,来到imagePickerController(_:didFinishPickingMediaWithInfo:)方法,而后在你设置imageView的后面添加如下代码:
// 1 takePictureButton.isHidden = true progressView.progress = 0.0 progressView.isHidden = false activityIndicatorView.startAnimating() upload( image: image, progressCompletion: { [unowned self] percent in // 2 self.progressView.setProgress(percent, animated: true) }, completion: { [unowned self] tags, colors in // 3 self.takePictureButton.isHidden = false self.progressView.isHidden = true self.activityIndicatorView.stopAnimating() self.tags = tags self.colors = colors // 4 self.performSegue(withIdentifier: "ShowResults", sender: self) })
Alamofire
的一切行为都是异步的,这意味着你将以异步的方式更新UI。
1.隐藏上传按钮,并显示进度条和活动视图。
2.当文件上传时,你能够调用进度回调来获取实时的进度百分比,而后能够更新进度条上的进度。
3.当上传完成后将执行完成处理回调,控件的状态也将会恢复到初始状态。
4.最后,在一个成功或者失败的上传后进入结果界面,用户界面不会根据错误状况变化。
而后,咱们回到upload(image:progressCompletion:completion:)方法,而后在转化UIImage实例代码后面添加以下代码:
Alamofire.upload( multipartFormData: { multipartFormData in multipartFormData.append(imageData, withName: "imagefile", fileName: "image.jpg", mimeType: "image/jpeg") }, to: "http://api.imagga.com/v1/content", headers: ["Authorization": "Basic xxx"], encodingCompletion: { encodingResult in } )
请确保从Imagga网站上获取到你本身的Basic
受权令牌,而后替换掉代码中Basic xxx
的部分。这里你将JPEG数据转换为MIME multipart请求并发送到Imagga内容服务api。
下一步,添加以下代码:
switch encodingResult { case .success(let upload, _, _): upload.uploadProgress { progress in progressCompletion(Float(progress.fractionCompleted)) } upload.validate() upload.responseJSON { response in } case .failure(let encodingError): print(encodingError) }
这两段代码经过调用Alamofire
的upload
方法,而后随着文件上传,经过一个简单的计算来更新进度条UI。以后验证响应的状态代码是否在默承认接受的范围内(在200和299之间)。
注意:在Alamofire 4以前,不能保证老是在主队列上调用进度回调。而从Alamofire 4开始,新的进度回调API则老是在主队列上调用。
接下来,在upload.responseJSON
中添加如下代码:
// 1. guard response.result.isSuccess else { print("Error while uploading file: \(response.result.error)") completion([String](), [PhotoColor]()) return } // 2. guard let responseJSON = response.result.value as? [String: Any], let uploadedFiles = responseJSON["uploaded"] as? [[String: Any]], let firstFile = uploadedFiles.first, let firstFileID = firstFile["id"] as? String else { print("Invalid information received from service") completion([String](), [PhotoColor]()) return } print("Content uploaded with ID: \(firstFileID)") // 3. completion([String](), [PhotoColor]())
下面是上面代码每一个步骤的解释:
1.检查响应是否成功,若是失败,打印错误并调用完成回调函数。
2.检查响应的每一个部分,并验证预期的类型是否就是接收到的实际类型,而后从响应中检索firstFileID
,若是firstFileID
没法解析,则输出错误信息并调用完成回调函数。
3.调用完成回调函数来更新UI。此时,你并无下载到任何标签和颜色数据,因此只须要传入空数据便可。
注意:每一个响应都有一个带有值和类型的Result枚举。 使用自动验证,当结果返回200到299之间的有效HTTP代码而且内容类型是在Accept HTTP header字段中指定的有效类型时,将认为结果成功。
你能够经过以下所示的添加.validate参数来使用手动验证响应结果:
Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]) .validate(statusCode: 200..<300) .validate(contentType: ["application/json"]) .response { response in // response handling code }若是你在上传中发生错误的话,UI界面是不会提示错误信息的,它仅仅告诉用户没有颜色和标签返回。这不是最好的用户体验,可是对于这篇教程来讲已经足够了。
编译并运行整个项目,选择一张图片而后静观其变。进度条渐进式的前进,你应该能够在控制台看到以下输出:
恭喜你,你已经成功的经过网络上传了一张图片了。
将图片上传到Imagga以后,下一步则是从Imagga获取到它分析以后的标签数据了。在ViewController文件的扩展方法upload(image:progress:completion:):下添加如下代码:
func downloadTags(contentID: String, completion: @escaping ([String]) -> Void) { Alamofire.request( "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: Any] else { print("Invalid tag information received from the service") completion([String]()) return } print(responseJSON) completion([String]()) } }
请再一次确认替换的Basic xxx是你本身帐号的受权token。上述代码执行了一个对标签服务的GET请求,参数是经过上传成功后返回的一个ID。接着,咱们回到upload(image:progress:completion:)方法,而后替换成功条件下的完成回调方法代码以下:
self.downloadTags(contentID: firstFileID) { tags in completion(tags, [PhotoColor]()) }
上述代码将得到到的标签数据经过完成回调函数返回给上级调用者。
编译而后运行整个项目,上传照片而后控制台会打印以下结果:
本教程里你不用在乎返回结果里的confidence score是什么意思,只须要关心标签名称的数组便可。下一步,回到downloadTags(contentID: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: Any], let results = responseJSON["results"] as? [[String: Any]], let firstObject = results.first, let tagsAndConfidences = firstObject["tags"] as? [[String: Any]] 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)
咱们来逐步分解上面的代码:
1.检查返回结果是否成功,若是失败,打印错误信息,而后调用完成回调函数。
2.检查返回结果的每一个部分,验证数据类型是否正确,从返回结果中获取tagsAndConfidences,若是解析失败,打印错误信息,而后调用完成回调函数。
3.遍历tagsAndConfidences数组里的每一个字典对象,从中以tag
做为key来提取对应的值。
4.将从服务器获取的标签数据返回给上层调用者。
注意:代码里使用Swift的
flatMap
方法来遍历数组里面的每一个字典。这个方法在遍历到nil的值得时候并不会让程序crash掉,而是直接将这些nil值移除掉,而后只返回正确的结果。因此经过使用可选解包(as?)来验证字典中的值是否能够被转化成为字符串。
编译而后运行整个项目,上传一张图片,而后你能够在界面上看到以下效果:
纵享丝滑,Imagga不愧是一个智能的api。下一步,你将要获取的是图片的颜色。添加以下代码到ViewController中的downloadTags(contentID:completion:):方法下面:
func downloadColors(contentID: String, completion: @escaping ([PhotoColor]) -> Void) { Alamofire.request( "http://api.imagga.com/v1/colors", parameters: ["content": contentID], // 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: Any], let results = responseJSON["results"] as? [[String: Any]], let firstResult = results.first, let info = firstResult["info"] as? [String: Any], let imageColors = info["image_colors"] as? [[String: Any]] else { print("Invalid color information received from service") completion([PhotoColor]()) return } // 4. let photoColors = imageColors.flatMap({ (dict) -> PhotoColor? in guard let r = dict["r"] as? String, let g = dict["g"] as? String, let b = dict["b"] as? String, let 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) } }
下面按照编号来依次解析:
1.确保Basic xxx是你本身帐号所属的认证token。
2.检查返回结果是否成功,若是失败,打印错误信息,而后调用完成回调函数。
3.检查返回结果的每一个部分,验证数据类型是否正确。从返回结果中获取imageColors,若是解析失败,打印错误信息,而后调用完成回调函数。
4.再次使用flatMap
方法,对服务器返回的PhotoColors
对象进行遍历,将里面的符合RGB格式的数据转换字符串,而后封装成PhotoColor
对象。
5.调用完成回调函数,传入服务器返回的图片颜色数据。
最后,回到upload(image:progress:completion:)方法,而后替换掉成功条件下的调用完成回调函数:
self.downloadTags(contentID: firstFileID) { tags in self.downloadColors(contentID: firstFileID) { colors in completion(tags, colors) } }
上述代码嵌套了上传图片,获取标签以及获取颜色的操做。
编译而后运行整个项目,这一次当你选择Colors按钮后,界面会有如下效果:
这块主要使用了映射到PhotoColor结构体的RGB颜色来渲染UI。你已经成功的往Imagga上传了一张图片,以及调用了2个不一样的api来获取数据。你已经作得很不错了,可是在如何使用Alamofire
上,咱们还有改进的余地。
你应该注意到了咱们前面的代码中有许多重复代码。若是Imagga官方宣布废除掉v1版本的api,并推出v2版本的api。咱们的应用将再也不可用直到你将每一个方法里面的URL都修改过来。一样的道理,若是你的Basic认证token发生了变化,那么你须要在全部用到的地方作出修改。Alamofire
提供了一个简单的方法来消除代码重复的问题并提供了集中式的配置方法。该技术涉及建立符合URLRequestConvertible
协议的结构体,而后更新的上传和请求的方法。
建立一个新的Swfit
文件,命名为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) var method: HTTPMethod { switch self { case .content: return .post case .tags, .colors: return .get } } var path: String { switch self { case .content: return "/content" case .tags: return "/tagging" case .colors: return "/colors" } } public func asURLRequest() throws -> URLRequest { let parameters: [String: Any] = { switch self { case .tags(let contentID): return ["content": contentID] case .colors(let contentID): return ["content": contentID, "extract_object_colors": 0] default: return [:] } }() let url = try ImaggaRouter.baseURLPath.asURL() var request = URLRequest(url: url.appendingPathComponent(path)) request.httpMethod = method.rawValue request.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField: "Authorization") request.timeoutInterval = TimeInterval(10 * 1000) return try URLEncoding.default.encode(request, with: parameters) } }
替换Basic xxx为你本身帐号的认证Token。这个路由类经过提供三个不一样的分类:.content
, .tags(String)
,.colors(String)
来实现建立多个URLRequest
实例。如今你的重复代码都集中到了一个地方,若是有须要在这里更改便可。
回到ViewController.swift文件,而后替换upload(image:progress:completion:)方法:
Alamofire.upload( multipartFormData: { multipartFormData in multipartFormData.append(imageData, withName: "imagefile", fileName: "image.jpg", mimeType: "image/jpeg") }, to: "http://api.imagga.com/v1/content", headers: ["Authorization": "Basic xxx"],
替换为:
Alamofire.upload( multipartFormData: { multipartFormData in multipartFormData.append(imageData, withName: "imagefile", fileName: "image.jpg", mimeType: "image/jpeg") }, with: ImaggaRouter.content,
而后替换掉downloadTags(contentID:completion:)方法里的代码:
Alamofire.request(ImaggaRouter.tags(contentID))
最后,替换掉downloadColors(contentID:completion:)方法里的代码:
Alamofire.request(ImaggaRouter.colors(contentID))
编译而后运行项目,结果应与以前的效果保持一致。这意味着你已经在不破坏你的app的前提下完成了重构,干得漂亮!
全部的代码文件都上传到了github上,不要忘了替换你本身的Basic认证token。
这篇教程只涵盖到了很是基础的知识点。你能够去Alamofire
的官方网站https://github.com/Alamofire/Alamofire上进行更深刻的学习。
进一步的话,你能够花一些时间来学习Alamofire
底层使用的Apple
的URLSession
类的内容: