Moya是一个网络库,其灵感来自以类型安全的方式封装网络请求(通常使用枚举)的概念,该概念为使用网络层提供了信心。成为Moya的网络超级英雄!
注意:本教程使用Xcode 10和Swift 4.2。它依赖的库尚未针对Swift 4.2进行更新,但可以正常使用。您需要忽略单个警告,告诉您Swift 4.2转换可用。
制作精美而高性能的iOS应用程序涉及许多动人的事。其中最重要的部分,如果不是最适合现代应用重要,是网络。作为iOS开发人员,您可以采用许多不同的方式来构建网络层-无论是使用URLSession
还是使用第三方库。
在本教程中,您将学习名为Moya的第三方网络库,该库旨在为网络服务和请求创建类型安全的结构。
您可能会问自己:“这是什么Moya?我已经知道并喜欢Alamofire!” 而且,如果您不了解和喜欢它,现在将是查看我们关于该主题的出色教程的绝佳时机。
好吧,这是最重要的部分:Moya实际上使用Alamofire,同时提供了另一种方法来构建网络层。在本教程的后面,您将学到更多有关Moya和Alamofire之间关系的信息。
在本教程中,您将构建一个名为ComicCards的简洁小应用程序,在其中您将使用Marvel API向用户显示在给定的一周内发行的漫画列表,以及其封面图像和其他有趣的信息。当用户选择漫画时,您的应用将生成包含漫画信息和图像的可共享卡的图像,让用户将其上传到Imgur服务并共享:
哇-在一个应用程序中有两种不同的API服务?不用担心 它并不像听起来那样难。让我们开始吧!
注意:本教程假定您具有HTTP API的基本知识,尽管即使您只有很少的知识,您也可以轻松地遵循本教程。但是,如果您想了解更多有关HTTP API的信息,请参考前面提到的Alamofire教程,或者参考这个有趣的站点,以获取有关REST API基础的更多信息。
https://koenig-media.raywenderlich.com/uploads/2018/07/ComicCards.zip
使用本教程顶部或底部的“ 下载材料”按钮,可下载已捆绑Moya 的ComicCards入门项目。打开ComicCards.xcworkspace而不是项目文件-这很重要。
在项目打开的情况下,签出Main.storyboard以大致了解应用程序的结构:
该ComicCards应用程序由两个不同的屏幕:
什么是Moya?
Moya是一个网络库,专注于以类型安全的方式封装网络请求,通常通过使用枚举(例如enum)在与您的网络层一起使用时提供编译时保证和信心以及增加的可发现性。
它是由Ash Furrow和Orta Therox为Artsy的Eidolon应用程序构建的,并迅速流行。如今,它完全由热情的开源贡献者社区维护。
如本教程的简介中所述,Moya和Alamofire之间的紧密联系只是因为Moya本身并没有真正进行任何联网。它使用Alamofire经过实战检验的网络功能,并且仅提供其他功能,类型和概念来进一步抽象Alamofire。
实际上,您正在使用Alamofire!而不是直接使用它,而是使用Moya,后者在引擎盖下使用Alamofire。
查看启动项目的Podfile.lock可以发现,Alamofire是Moya的依赖项:
Moya引入了一些独特的概念和构建块,在开始编写代码之前,您应该了解这些概念和构建块。它使用以下构建块来描述整个网络链:
在奇迹API是世界上最大的漫画API,由Marvel本身创建和维护。
首先创建一个免费帐户。设置完毕后,请返回“ 我的开发者帐户”页面,在该页面中,您将找到新的公共和私有密钥:
保持两个键都在手;几分钟后您将需要它们。
返回到ComicCards Xcode项目。在您的项目导航器中,右键单击ComicCards / Network
文件夹,然后选择New File…
创建一个新的Swift文件并将其命名为Marvel.swift
:
之后import Foundation,添加以下代码:
import Moya
public enum Marvel {
// 1
static private let publicKey = "YOUR PUBLIC KEY"
static private let privateKey = "YOUR PRIVATE KEY"
// 2
case comics
}
您刚刚创建了一个非常简单的枚举,描述了将要使用的API服务:
这些是您的Marvel公钥和私钥。您将它们与服务定义一起存储,以确保可以轻松地将密钥作为服务配置的一部分进行访问。确保将占位符替换为上一步中生成的实际密钥。
一个名为的枚举案例comics,它代表您将在Marvel API中使用的唯一端点-GET / v1 / public / comics。
现在,您已经配置了基本的枚举,是时候通过遵循使其实际成为目标TargetType。
将以下代码添加到文件末尾(大括号后):
extension Marvel: TargetType {
// 1
public var baseURL: URL {
return URL(string: "https://gateway.marvel.com/v1/public")!
}
// 2
public var path: String {
switch self {
case .comics: return "/comics"
}
}
// 3
public var method: Moya.Method {
switch self {
case .comics: return .get
}
}
// 4
public var sampleData: Data {
return Data()
}
// 5
public var task: Task {
return .requestPlain // TODO
}
// 6
public var headers: [String: String]? {
return ["Content-Type": "application/json"]
}
// 7
public var validationType: ValidationType {
return .successCodes
}
}
这看起来可能像是大量的代码,但这仅仅是为了符合TargetType。让我们分解一下:
注意:请注意,switch即使只有一个大小写(.comics),您也在所有属性中使用语句。这是一般的最佳做法,因为您的目标可能会轻松演变并添加更多端点。对于不同的目标属性,任何新端点都将需要其自己的值。
哇,这是很多知识!考虑到这是以Moya的最基本形式工作所需要了解的大部分知识,您应该感到非常自豪!
新Marvel目标中仅缺少一件事-代码中剩下的“要做”,即返回Task。
Marvel API使用自定义授权方案,在该方案中,您将使用唯一的标识符(例如时间戳),私钥和公钥创建一个“哈希”,这些标识符全部串联在一起并使用MD5进行哈希处理。您可以在API参考中的“服务器端应用程序的身份验证”下阅读完整规范。
在Marvel.swift中,替换task为以下内容:
public var task: Task {
let ts = "\(Date().timeIntervalSince1970)"
// 1
let hash = (ts + Marvel.privateKey + Marvel.publicKey).md5
// 2
let authParams = ["apikey": Marvel.publicKey, "ts": ts, "hash": hash]
switch self {
case .comics:
// 3
return .requestParameters(
parameters: [
"format": "comic",
"formatType": "comic",
"orderBy": "-onsaleDate",
"dateDescriptor": "lastWeek",
"limit": 50] + authParams,
encoding: URLEncoding.default)
}
}
您的任务已准备就绪!这是做什么的:
转到ComicsViewController.swift并在视图控制器类的开头添加以下内容:
let provider = MoyaProvider<Marvel>()
如前所述,您将用于与Moya目标进行交互的主要类是MoyaProvider,因此您首先创建一个MoyaProvider使用新Marvel目标的实例。
接下来,在您的内viewDidLoad(),替换为:
state = .error
带有:
// 1
state = .loading
// 2
provider.request(.comics) {
[weak self] result in
guard let self = self else {
return }
// 3
switch result {
case .success(let response):
do {
// 4
print(try response.mapJSON())
} catch {
self.state = .error
}
case .failure:
// 5
self.state = .error
}
}
新代码执行以下操作:
{
attributionHTML = "Data provided by Marvel. \U00a9 2018 MARVEL";
attributionText = "Data provided by Marvel. \U00a9 2018 MARVEL";
code = 200;
copyright = "\U00a9 2018 MARVEL";
data = {
count = 19;
limit = 50;
offset = 0;
results = (
{
comic object},
{
comic object},
{
comic object},
...
)
}
很棒的工作,您使用Moya和新Marvel目标从后端获得了有效的JSON响应!
注意:结果可能需要几秒钟才能出现在调试控制台中。
完成此视图控制器的最后一步实际上是将JSON响应映射到适当的数据模型—在您的情况下,是预配置的Comic结构。
这是使用其他Moya响应映射器的最佳时机,该映射器将响应映射到Decodable而不是原始JSON。
您可能已经注意到JSON响应的结构类似于:
data ->
results ->
[ Array of Comics ]
在到达对象本身之前data,意味着两个层次的嵌套(,results)。入门项目已经包括Decodable负责解码的适当对象。
替换以下内容:
print(try response.mapJSON())
带有:
self.state = .ready(try response.map(MarvelResponse<Comic>.self).data.results)
而不是将对象映射到原始JSON响应,而是使用将MarvelResponse通用对象Decodable与Comic结构一起使用的映射器。这还将解决两个嵌套层次的解析,使您可以通过访问来访问漫画数组data.results。
您可以将视图的状态设置为,.ready并将其关联的值为Comic从Decodable映射返回的对象数组。
生成并运行项目。您应该会看到第一个屏幕功能齐全!
然后进入细节视图!
当您点击漫画时,入门项目已经具有用于显示a CardViewController并将所选内容传递Comic给它的代码。但是,您可能会注意到,点击漫画只会显示一张空牌,而没有任何漫画细节。让我们来照顾它!
切换到CardViewController.swift并找到layoutCard(comic:)方法。在方法内部,添加:
// 1
lblTitle.text = comic.title
lblDesc.text = comic.description ?? "Not available"
// 2
if comic.characters.items.isEmpty {
lblChars.text = "No characters"
} else {
lblChars.text = comic.characters.items
.map {
$0.name }
.joined(separator: ", ")
}
// 3
lblDate.text = dateFormatter.string(from: comic.onsaleDate)
// 4
image.kf.setImage(with: comic.thumbnail.url)
此代码Comic通过以下方式使用提供的结构中的信息更新屏幕:
生成并运行您的应用程序,然后点击列表中的漫画之一-您应该会看到一张漂亮的信息卡:
您还需要添加两个功能:将卡上传到Imgur,并允许用户删除卡。
为此,您将创建另一个名为Moya的目标Imgur,该目标可让您与两个不同的终结点进行交互以进行图像处理:一个终结点用于上传,另一个终结点用于删除。
与Marvel API相似,您需要在Imgur 上注册一个免费帐户。
之后,您需要创建一个Imgur Application。您可以使用任何伪造的URL进行回调,因为这里不会使用OAuth。您也可以简单地选择没有回调URL的OAuth 2授权。
提交表单后,Imgur将为您提供新的Imgur 客户ID和客户密码。保存这些以进行下一步。
右键单击ComicCards / Network
文件夹,然后选择New File…
,然后创建一个新的Swift
文件,并将其命名为Imgur.swift
。
添加以下代码以定义将要实现和使用的Imgur端点:
import UIKit
import Moya
public enum Imgur {
// 1
static private let clientId = "YOUR CLIENT ID"
// 2
case upload(UIImage)
case delete(String)
}
与Marvel API相似,您可以:
extension Imgur: TargetType {
// 1
public var baseURL: URL {
return URL(string: "https://api.imgur.com/3")!
}
// 2
public var path: String {
switch self {
case .upload: return "/image"
case .delete(let deletehash): return "/image/\(deletehash)"
}
}
// 3
public var method: Moya.Method {
switch self {
case .upload: return .post
case .delete: return .delete
}
}
// 4
public var sampleData: Data {
return Data()
}
// 5
public var task: Task {
switch self {
case .upload(let image):
let imageData = image.jpegData(compressionQuality: 1.0)!
return .uploadMultipart([MultipartFormData(provider: .data(imageData),
name: "image",
fileName: "card.jpg",
mimeType: "image/jpg")])
case .delete:
return .requestPlain
}
}
// 6
public var headers: [String: String]? {
return [
"Authorization": "Client-ID \(Imgur.clientId)",
"Content-Type": "application/json"
]
}
// 7
public var validationType: ValidationType {
return .successCodes
}
}
现在您应该已经熟悉了。让我们浏览一下新Imgur目标的七个协议属性。
Imgur API的基本URL设置为https://api.imgur.com/3。
您path根据情况返回适当的端点。/image为.upload和/image/{deletehash}为.delete。
method不同的情况也因情况而不同:.postfor .upload和.deletefor .delete。
与之前一样,您为返回一个空Data结构sampleData。
这task是事情变得有趣的地方。您为每个端点返回一个不同的 Task值。该.delete案例不需要任何参数或内容,因为它是一个简单的DELETE请求,但是该.upload案例需要更多的工作。
要上传文件,您将使用.uploadMultipart任务类型,该类型需要一个MultipartFormData结构数组。然后,您MultipartFormData使用适当的图像数据,字段名称,文件名和图像mime类型创建的实例。
与Marvel API一样,该headers属性返回一个Content-Type: application/json标头和一个附加标头。Imgur API使用标头授权,因此您需要在每个请求的标头中以形式提供您的客户ID Authorization: Client-ID (YOUR CLIENT ID)。
该.validationType是和以前一样-适用于200和299之间的任何状态代码。
您的Imgur目标完成了!到此,ComicCards应用程序的Moya相关代码结束。恭喜您!
最后一步是CardViewController使其使用您新创建的Moya目标。
返回CardViewController.swift并在CardViewController类的开头在comic属性下方添加以下行:
private let provider = MoyaProvider<Imgur>()
private var uploadResult: UploadResult?
像以前一样,您MoyaProvider这次使用Imgur目标创建实例。您还定义uploadResult了一个可选UploadResult属性,用于存储上传结果,删除图像时将需要该属性。
您有两种实现方法:uploadCard()和deleteCard()。
在的末尾uploadCard(),添加以下代码:
// 1
let card = snapCard()
// 2
provider.request(.upload(card),
// 3
callbackQueue: DispatchQueue.main,
progress: {
[weak self] progress in
// 4
self?.progressBar.setProgress(Float(progress.progress), animated: true)
},
completion: {
[weak self] response in
guard let self = self else {
return }
// 5
UIView.animate(withDuration: 0.15) {
self.viewUpload.alpha = 0.0
self.btnShare.alpha = 0.0
}
// 6
switch response {
case .success(let result):
do {
let upload = try result.map(ImgurResponse<UploadResult>.self)
self.uploadResult = upload.data
self.btnDelete.alpha = 1.0
self.presentShare(image: card, url: upload.data.link)
} catch {
self.presentError()
}
case .failure:
self.presentError()
}
})
这段大代码肯定需要一些解释,但不必担心-大多数应该相对熟悉。
对于当天的最后一段代码:在下面添加以下代码deleteCard():
// 1
guard let uploadResult = uploadResult else {
return }
btnDelete.isEnabled = false
// 2
provider.request(.delete(uploadResult.deletehash)) {
[weak self] response in
guard let self = self else {
return }
let message: String
// 3
switch response {
case .success:
message = "Deleted successfully!"
self.btnDelete.alpha = 0.0
case .failure:
message = "Failed deleting card! Try again later."
self.btnDelete.isEnabled = true
}
let alert = UIAlertController(title: message, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Done", style: .cancel))
self.present(alert, animated: true, completion: nil)
}
此方法非常简单,其工作方式如下:
这就对了!最后一次构建并运行您的应用。选择漫画并将您的图像分享给Imgur。完成后,您可以点击从Imgur删除按钮将其删除。
注意:您可能会注意到,只有在卡片视图控制器中,您才能删除上传的图像。离开后,视图控制器uploadResult将被清除,并且deletehash将丢失。在不同的会话中持久保存所有生成的图像的哈希是一个不错的挑战,您可能想解决:]。
Moya是一个极其通用的网络库,具有太多的附加功能,无法在本教程中全面介绍,但是绝对值得一提:
您可以使用本教程顶部或底部的“ 下载材料”按钮下载项目的完整版本。不要忘记在项目中设置您的Imgur客户ID和Marvel公共和私有密钥!
在本教程中,您已经学习了使用Moya的基础知识,然后再学习一些!您拥有将网络层提升到新层次所需的一切。
继续探索Moya的最佳地方是它的官方文档页面,该页面内容丰富,深入探讨Moya的各个方面,甚至保留中文翻译。
同时,如果您对本教程或一般网络有任何疑问或意见,请加入下面的论坛讨论。
https://www.raywenderlich.com/5121-moya-tutorial-for-ios-getting-started