使用Swift 5结果类型构建简单的异步API请求

Apple终于在2019年3月底将Swift 5发布到稳定版频道。它捆绑在macOS的Xcode 10.2中。 Swift 5为Apple平台中的Swift标准库提供了ABI稳定性。 这意味着,所有未来的Swift版本将与使用Swift 5代码编写的应用二进制兼容。 它还引入了App Thinning,缩小了应用程序的大小,因为ABI的稳定性意味着二进制文件不必将所有Swift标准库嵌入应用程序包中,从而减小了应用程序的大小。 从iOS 12.2开始,操作系统将包括Swift运行时和标准库。

您可以在下面的链接中阅读与Swift的未来有关的所有有关模块稳定性和库演变的信息。

ABI稳定性和更多
在MacOS,iOS,watchOS和tvOS上稳定Swift的ABI是一个长期的目标。 swift.org 是一个稳定的ABI。

在本文中,我们将讨论Swift 5的新Result类型,以及如何利用它来创建异步API请求并简化对完成处理程序闭包的处理。

在开始之前,让我们看看我们通常如何在Swift中创建一个异步函数。 我们通常使用几种方法。

1. Objective-C风格

使用Objective-C样式,我们创建一个带有多个参数的单个回调closure ,其中包含异步函数中的可选result value和可选error

  func fetchMovies(URL:URL,completionHandler:@ 转义 ([Movie] ?, Error?)-> Void){ 
...
}

2. Swift风格

在这种样式中,我们创建2个完成closures 。 一种是将result value作为参数处理成功,另一种是the error作为参数处理失败。

  func fetchMovies(URL:URL,successHandler:@ 逃逸 ([Movie])->无效,errorHandler:@ 逃避 (Error?)->无效){ 
...
}

介绍Swift 5结果类型

Swift 5最后引入了新的Result类型,以使用enum来处理异步函数的结果。 只有两种情况都使用带有关联值的Swift Generic

1. Success与结果的价值。

2.实现Error协议的类型Failure

Result类型确实可以帮助我们简化异步函数结果的处理,因为只有两种情况需要处理, successfailure 。 让我们开始为URLSession数据任务创建一个简单的扩展,该扩展将Result类型用于完成处理程序的关闭。

使用结果类型扩展URL会话数据任务

众所周知,使用URLSession进行数据任务时,我们需要传递带有URLResponseDataError参数的完成处理程序闭包。 它们的类型都是可选的,我们需要使用let或guard语句来检查它们,以检查http响应状态代码,检索数据并将数据解码到模型中。

   url = URL(string:“ https://exampleapi.com/data.json”)! 
  URLSession.shared.dataTask(with:url){(data:Data ?,响应:URLResponse ?,错误:Error?)  
如果 错误=错误{
//处理错误
返回
}
  警卫队 回应=回应否则 { 
//处理空响应
返回
}
  保护数据=数据其他 { 
//处理空数据
返回
}
  //处理将数据解码为模型 
}

每次都很难处理这种方法。 我们可以使用“ Result类型”创建更好的解决方案。 因此,让我们为URLSession做一个简单的扩展,以处理带有Result类型的数据任务。

  扩展 URLSession { 
  func dataTask(使用url:URL,结果: @转义 (结果)->无效)-> URLSessionDataTask { 
return dataTask(with:url){(data,response,error) in
如果 错误=错误{
结果(。失败(错误))
返回
}
  保护 let response =响应, let data = data else { 
let error = NSError(domain:“ error”,code:0,userInfo: nil
结果(。失败(错误))
返回
}
结果(.success((响应,数据)))
}
}
}

我们可以像通常调用传递URL的dataTask一样调用此函数,但是这次我们将以Result Type作为参数传递完成闭包。

  URLSession.shared.dataTask(with:url){(结果)  
切换结果{
case .success( 响应, 数据):
//处理数据和响应
打破
  情况 .failure( 错误): 
//处理错误
打破
}
}

与使用以前没有Result类型的完成句柄相比,此方法看起来非常简单干净。

为MovieDB(TMDb)API创建API服务

让我们使用我们的新知识来构建一个简单的MovieServiceAPI类,该类使用电影数据库(TMDb)API来按类别检索电影,按ID检索单个电影。

您可以在下面的链接中注册API密钥。

API文件
针对每个OAS(Swagger)和RAML规范的托管API文档。 由Stop​​light.io驱动。 文档,模拟…… developers.themoviedb.org

首先,让我们创建一个实现CodableModel ,以处理将JSON数据解码为Swift Model

  public struct MoviesResponse:Codable { 

公开 页:Int
public let totalResults:整数
public let totalPages:Int
公开 让步结果:[电影]
}
  公共 结构电影:可编码{ 

公开 ID:Int
公开 租借标题:字符串
公共 概述:字符串
公开发布日期:日期
公众 票平均:双倍
公众 票数:整数
公众 成年人:布尔
}

接下来,让我们创建具有所有属性的类,例如baseURLapiKeyjsonDecoder和由enum表示的endpoint

  MovieServiceAPI  { 

公共 静态 共享= MovieServiceAPI ()
  私人 初始化 (){} 
私人 urlSession = URLSession.shared
私有 let baseURL = URL(string:“ https://api.themoviedb.org/3”)!
私人 let apiKey =“ PUT_API KEY HERE”
  私人  jsonDecoder:JSONDecoder = { 
让jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
让dateFormatter = DateFormatter()
dateFormatter.dateFormat =“ yyyy-mm-dd”
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
返回jsonDecoder
}()
  //枚举端点 
枚举端点:String,CustomStringConvertible,CaseIterable {
case nowPlaying =“ now_playing”
案件即将到来
案例受欢迎
case topRated =“ top_rated”
}
}

我们还需要创建一个Error enum以表示我们的API请求中的几种错误情况。

  MovieServiceAPI  { 
...
公共 枚举 APIServiceError:错误{
案例 apiError
大小写无效
大小写无效
案例
案例解码错误
}
}

接下来,让我们创建一个私有函数,该函数使用decodable类型的Swift Generic Constraint获取资源。 该函数将接受将用于以Result类型启动URLsession数据任务的URL

  私人 功能 fetchResources <T:可解码的(URL:URL,完成: @转义 (Result )->无效){ 
  保护 var urlComponents = URLComponents(url:url,resolvingAgainstBaseURL: trueelse { 
完成(.failure(.invalidEndpoint))
返回
}
   queryItems = [URLQueryItem(name:“ api_key”,value:apiKey)] 
urlComponents.queryItems = queryItems
  警卫  url = urlComponents.url else { 
完成(.failure(.invalidEndpoint))
返回
}

urlSession.dataTask(with:url){(结果)
切换结果{
case .success( let (响应,数据)):
guard let statusCode =(响应 ?HTTPURLResponse)?. statusCode,200。.<299〜= statusCode 其他 {
完成(.failure(.invalidResponse))
返回
}
{
值= 尝试 self .jsonDecoder.decode(T。self,from:data)
完成(。成功(值))
} {
完成(.failure(.decodeError))
}
情况 .failure( 错误):
完成(.failure(.apiError))
}
}。恢复()
}

在具有完成关闭功能的URLSession dataTask中,我们检查result enum 。 如果成功,我们将检索数据,然后检查响应HTTP状态代码以确保其范围在200到299之间。然后,使用jsonDecoder我们使用来自函数参数的模型的通用decodable约束对json数据进行decodable

从端点获取电影列表

接下来,让我们创建第一个方法来使用特定endpoint检索电影列表。 首先,我们通过将baseURL附加传递的端点开始定义URL 。 然后,让我们使用之前创建的fetchResource方法来检索已经解码的电影数组。

  MovieServiceAPI  { 
  ... 
  公共 函数 fetchMovies(来自端点:端点,结果: @转义 (Result )->无效){ 
   movieURL = baseURL 
.appendingPathComponent(“ movie”)
.appendingPathComponent(endpoint.rawValue)
fetchResources(URL:movieURL,完成:result)
}

要使用该方法,我们可以像这样调用它:

  MovieAPIService.shared.fetchMovies(来自:.nowPlaying){(结果:Result )  
  切换结果{ 
case .success( movieResponse):
打印(movieResponse.results)
  情况 .failure( 错误): 
打印(error.localizedDescription)
}
}

使用电影ID提取单个电影

为了使用电影ID检索单个电影,我们将通过附加电影ID来构造url ,然后使用相同的fetchResource方法为结果检索单个解码的电影。

  MovieServiceAPI  { 
  ... 
public func fetchMovie(movieId:Int,result: @escaping (Result )-> Void){

movieURL = baseURL
.appendingPathComponent(“ movie”)
.appendingPathComponent(String(movieId))
  fetchResources(URL:movieURL,完成:result) 
}

要使用该方法,我们可以像这样调用它:

  MovieAPIService.shared.fetchMovie(movieId:1234){(结果:Result )  
切换结果{
案例。成功( 电影):
打印(电影)
情况 .failure( 错误):
打印(error.localizedDescription)
}
}

结论

就是这样,我们的API服务看起来很简单且干净,使用Result Type作为完成处理程序。 Result类型还有许多其他功能,例如mapflatMap Result到另一个ResultResult类型确实将回调完成处理程序简化为成功和失败的情况。

Swift 5非常出色,我相信还有更多令人惊叹的新功能,我们可以期待Swift的下一个发展。 继续进行终身学习,让Swifting happy开心。