在Swift 4中使用JSONDecoder和Decodable的基于协议的通用网络

Swift 4已经发布了一段时间,本周(2017年12月)发布了XCode 9.2,对于Swift 4并没有太多戏剧性的更新,但是我们可以使用一些新工具来编写更简洁和可重用的代码,在本文中我们将主要关注JSONDecoder和Decodable协议,该协议有助于简化JSON的解析。

我们将创建可用于任何模型的通用API,因为我要在该项目上强调的是如何创建网络层,因此我们将仅解析JSON并将其作为模型打印在控制台中。 作为奖励,我将在该项目中添加第二部分,在该项目中,我们将使用协议扩展来使用XIBS创建UI。

对于此项目,我们将使用“电影数据库”中的API,您可以在此处查看其文档。

让我们从下载此入门项目开始,在该项目上您将找到一个名为Model的组和一个名为Networking的空组。 模型一包含两个模型,一个模型名为MovieFeedResult,具有电影类型数组的属性,另一个模型称为电影,我们将在完成网络层后重新访问这些文件。

我们将逐步创建网络层,创建一个新的Swift文件并将其命名为Result并将此枚举复制并粘贴到其中……

 枚举Result ,其中U:错误{ 
成功案例(T)
案例失败(U)
}

当我们发出URL请求时,我们可能会得到两种不同的响应,要么是成功的响应,要么是失败的响应。 通常,我们都会在完成处理程序设置中将两者都设为nil,但是通过此通用枚举,我们可以避免这种情况,并只传递每种情况所需的值。

接下来,我们将创建一个带有协议扩展名的协议,创建一个新文件并将其命名为Endpoint,复制并粘贴…

 协议端点{ 
var base:字符串{get}
var path:字符串{get}
}
扩展端点{
var apiKey:字符串{
返回“ api_key = 34a92f7d77a168fdcd9a46ee1863edf1”
}

var urlComponents:URLComponents {
var components = URLComponents(string:base)!
components.path =路径
components.query = apiKey
返回组件
}

var request:URLRequest {
让url = urlComponents.url!
返回URLRequest(url:url)
}
}

与常规端点一样,此协议具有两个必需的属性,分别称为“基础”和“路径”,它在扩展中还具有一些计算的属性,一个是能够发出请求所需的APIKey(我从电影数据库网站),它还具有urlComponents属性,该属性将构造url并最终构造返回URLRequest的请求。

电影API可以返回不同的电影供稿,例如正在播放的电影或收视率最高的电影,我们将创建一个枚举,负责管理不同类型的供稿,创建新的空文件并称之为MovieFeed…

 枚举MovieFeed { 
现在案例
案例顶部
}
扩展MovieFeed:端点{

var base:字符串{
返回“ https://api.themoviedb.org”
}

var path:字符串{
切换自我{
case .nowPlaying:返回“ / 3 / movie / now_playing”
case .topRated:返回“ / 3 / movie / top_rated”
}
}
}

在这里,我们使MovieFeed符合Endpoint,因此将需要提供两条信息,即基本路径和与相应案例关联的路径,这样我们将为每种类型的feed构建端点。

如果网址请求中出现错误,可能是由于多种原因并提供了有关错误的详细信息,对于我们作为开发人员以及对于用户而言都是非常有用的,我们将创建一个枚举,该枚举将包含多种错误并返回它们的描述,创建一个文件并将其命名为APIError…

 枚举APIError:错误{ 
案例请求失败
案例jsonConversionFailure
案例invalidData
案件回应不成功
案例jsonParsingFailure
var localizedDescription:字符串{
切换自我{
case .requestFailed:返回“请求失败”
case .invalidData:返回“无效数据”
case .responseUnsuccessful:返回“响应失败”
case .jsonParsingFailure:返回“ JSON解析失败”
case .jsonConversionFailure:返回“ JSON转换失败”
}
}
}

在这里,事情变得很有趣,我们将使用JSONDecoder和Decodable协议创建一个通用APICLient,您可以在任何项目中使用它并与任何类型的对象或它们的集合一起重用,创建一个新的空项目并将其命名为APIClient,让我们从创建协议开始…

 协议APIClient { 
var session:URLSession {get}
func fetch (带有请求:URLRequest,解码:@转义(Decodable)-> T ?,完成:@转义(Result )->无效)
}

每个符合APIClient的对象都将有一个会话,并且将能够使用通用提取功能,现在让我们在扩展中为此协议添加一些功能…

 扩展APIClient {typealias JSONTaskCompletionHandler =(Decodable ?, APIError?)-> Void 
私有函数解码任务(带有请求:URLRequest,decodingType:T.Type,completionHandler完成:@转义JSONTaskCompletionHandler)-> URLSessionDataTask {

let task = session.dataTask(with:request){数据,响应,错误
守护让httpResponse =响应为? HTTPURLResponse else {
完成(无,.requestFailed)
返回
}
如果httpResponse.statusCode == 200 {
如果让数据=数据{
做{
让genericModel =尝试JSONDecoder()。decode(decodingType,来自:数据)
完成(genericModel,nil)
} {
完成(nil,.jsonConversionFailure)
}
}其他{
完成(nil,.invalidData)
}
}其他{
完成(无,.response不成功)
}
}
退货任务
}
}

该函数将负责解析或解码JSON数据,它以请求作为参数,遵循Decodable的对象类型和完成处理程序,最后返回URLSessionDataTask。 您可以看到,在其中我们只检查了响应和statusCode,然后根据它对响应中的数据进行解码,或者在出现错误的情况下提供相应的错误信息。

不过,最酷的部分是我们如何在此处使用JSONDecoder解析(或解码)任何类型的对象甚至它们的集合。 顺便说一句,看到我们在这里不叫简历,我们只是想返回任务。

为了完成该协议,让我们在扩展功能的复制和粘贴内部将逻辑添加到提取功能中……

  func fetch (带有请求:URLRequest,解码:@转义(Decodable)-> T ?,完成:@转义(Result )->无效){ 
let task = encodingTask(with:request,解码Type:T.self){(json,error)在

// MARK:更改为主队列
DispatchQueue.main.async {
警卫队让json = json else {
如果让错误=错误{
完成(Result.failure(错误))
}其他{
完成(Result.failure(.invalidData))
}
返回
}
如果让值=解码(json){
完成(。成功(值))
}其他{
完成(.failure(.jsonParsingFailure))
}
}
}
task.resume()
}

在此函数内部,我们从刚刚编写的helper方法返回一个任务,并将其作为解码类型的参数传递给该函数将解码的类型,在检查JSON是否为nil并且是否已解码后,我们传递了解码后的数据在完成处理程序的成功案例中。

最后一件,我们已经准备好了,现在我们有了通用的API,它将获取和解码任何类型的对象,让我们为Movies应用程序创建一个客户端,它将符合APIClient以获取其功能,并创建一个新文件并命名为MovieClient…

 类MovieClient:APIClient {让会话:URLSession 

初始化(配置:URLSessionConfiguration){
self.session = URLSession(配置:配置)
}

便利init(){
self.init(配置:.default)
}

//在成功的情况下,在函数的签名中,我们定义了在API中通用的Class类型
func getFeed(来自movieFeedType:MovieFeed,完成:@escaping(Result )->无效){
fetch(with:movieFeedType.request,解码:{json-> MovieFeedResult?in
守护让movieFeedResult = json为? MovieFeedResult else {return nil}
返回movieFeedResult
},完成:完成)
}
}

这有一个方便的初始化方法,因为为此我们总是希望将URLSession设置为默认值。

我们将创建一个接受MovieFeed作为参数的函数,因此当我们调用此函数时,我们将非常清楚我们需要的是哪种提要,其余的只是将对相应的MovieFeed的请求传递给通用的fetch方法APIClient协议。

那么这将如何工作? 好了,提取通用函数会将通用类型转换为MovieFeedResult吗? 键入,这样当执行解码完成处理程序json时,它现在是一个Decodable模型,然后,我们将其安全地向下转换为MovieFeedResult并返回它,最后,我们通过完成操作,我知道此解释可能会很冗长,所以我建议阅读该功能几次。

现在让我们转到视图控制器文件,并在viewDidLoad上测试我们的功能…

  client.getFeed(from:.nowPlaying){导致切换结果{ 
案例.success(let movieFeedResult):

警惕let movieResults = movieFeedResult?.results else {return}
打印(movieResults)
案例。失败(让错误):
print(“错误\(错误)”)
}
}

转到控制台,您应该可以看到一系列电影!

好的,现在您可能会问自己,在解析JSON时通常使用的所有键在哪里,那么让我们尝试在模型文件中找到它们吧…什么都没有? 那他们呢 这是什么巫术? 我们如何解析它?

好吧,这里没有魔术,如果您在此处检查此响应的JSON并检查键,这将是Decodable的工作方式,您将看到在MOVIE模型中其属性与这些键的编写方式完全匹配,其余部分由协议。

总之,您需要使模型符合Decodable,并且为了避免出现问题,使属性成为可选属性,以防万一这些键之一不存在。

您可以在这里找到这部分的完整实现,希望对您有所帮助!

PS:感谢Treehouse和Pasan Premaratne提供了基于协议的想法😉