在Swift中编写网络层:面向协议的方法



现在,几乎100%的应用程序都使用网络,因此每个人都面对网络层的组织和使用。 解决此问题的方法主要有两种,要么使用第三方库,要么使用您自己的网络层实现。 在本文中,我们将考虑第二个选项,并尝试使用该语言的所有最新功能,使用协议和枚举来实现网络层。 这将以其他库的形式保存项目,避免不必要的依赖。 曾经看过Moya的人会立即意识到实现和使用过程中的许多类似细节,只是这次,我们将自己动手做,而无需接触Moya和Alamofire。


在本指南中,我们将研究如何在不使用任何第三方库的情况下在纯Swift上实现网络层。 阅读本文后,您的代码将变为

  • 面向协议
  • 使用方便
  • 使用方便
  • 输入安全
  • 对于端点,将使用枚举


以下示例说明了网络层实现后的使用情况:



通过简单地编写router.request(。并使用枚举的所有功能,我们将看到所有可能的查询选项及其参数。

首先,关于项目的结构

每当您创建新内容时,为了将来能够轻松理解所有内容,正确组织和组织所有内容非常重要。 我相信正确组织的文件夹结构是构建应用程序体系结构时的重要细节。 为了使所有内容正确地排列在文件夹中,让我们提前创建它们。 这看起来像项目中的常规文件夹结构:



端点类型协议

首先,我们需要定义EndPointType协议。 该协议将包含配置请求的所有必要信息。 什么是请求(端点)? 本质上,它是具有所有相关组件(例如标头,请求参数,请求主体)的URLRequest。 EndPointType协议是我们网络层实现中最重要的部分。 让我们创建一个文件并将其命名为EndPointType 。 将此文件放在Service文件夹中(而不是EndPoint文件夹中,为什么-稍后会清除)



HTTP协议

我们的EndPointType包含创建请求所需的几种协议。 让我们看看这些协议是什么。

HTTP方法

创建一个文件,将其命名为HTTPMethod并将其放在Service文件夹中。 此清单将用于设置我们请求的HTTP方法。



HTTP任务
创建一个文件,将其命名为HTTPTask并将其放在Service文件夹中。 HTTPTask负责配置特定请求的参数。 您可以根据需要向其添加尽可能多的不同查询选项,但是我将依次进行常规查询,带参数的查询,带参数和标头的查询,因此,我将仅执行这三种类型的查询。



在下一节中,我们将讨论参数以及如何使用它们

HTTP头

HTTPHeaders只是字典的别名。 您可以在HTTPTask文件的顶部创建它。

public typealias HTTPHeaders = [String:String] 


参数与编码

创建一个文件,将其命名为ParameterEncoding并将其放在Encoding文件夹中。 为Parameters创建typealias,它将再次成为常规词典。 我们这样做是为了使代码看起来更易于理解和可读。

 public typealias Parameters = [String:Any] 


接下来,使用单个编码功能定义ParameterEncoder协议。 encode方法具有两个参数: inout URLRequestParametersINOUT是一个Swift关键字,它将功能参数定义为引用。 通常,参数作为值传递给函数。 在调用中的函数参数之前写出inout时 ,可以将此参数定义为引用类型。 要了解有关inout参数的更多信息,可以单击此链接。 简而言之, inout允许您更改传递给函数的变量本身的值,而不仅仅是在参数中获取其值并在函数内部使用它。 ParameterEncoder协议将在JSONParameterEncoderURLPameterEncoder中实现

 public protocol ParameterEncoder { static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws } 


ParameterEncoder包含一个函数,其功能是对参数进行编码。 此方法可能会抛出需要处理的错误,因此我们使用throw。

产生标准错误而不是定制错误也可能很有用。 解密Xcode给您的东西总是很困难。 当您对所有错误进行了自定义和描述后,您始终会确切知道发生了什么。 为此,让我们定义一个继承自Error的枚举。



创建一个文件,将其命名为URLParameterEncoder并将其放在Encoding文件夹中。



此代码获取参数列表,对其进行转换和格式化以用作URL参数。 如您所知,URL中不允许使用某些字符。 参数也由“&”符号分隔,因此我们必须注意这一点。 如果没有在请求中设置标头,我们还必须设置标头的默认值。

这是单元测试应该涵盖的代码部分。 构建URL请求是关键,否则我们会引发许多不必要的错误。 如果您使用开放式API,则显然您不希望将全部请求量用于失败的测试。 如果您想了解有关单元测试的更多信息,可以从本文开始。

JSONParameterEncoder

创建一个文件,将其命名为JSONParameterEncoder并将其放在Encoding文件夹中。



一切都与URLParameter的情况相同 ,只是在这里,我们将转换JSON的参数,然后再次将定义编码“ application / json”的参数添加到标头中。

网络路由器

创建一个文件,将其命名为NetworkRouter并将其放在Service文件夹中。 让我们开始定义闭包的类型别名。

 public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->() 


接下来,我们定义NetworkRouter协议。



NetworkRouter有一个用于请求的端点 ,并且在请求完成后,此请求的结果将传递到NetworkRouterCompletion闭包。 该协议还具有取消功能,可用于中断长期加载和卸载请求。 我们在这里也使用了associatedtype ,因为我们希望路由器支持任何类型的EndPointType 。 如果不使用associatedtype,则路由器必须具有实现EndPointType的某些特定类型。 如果您想了解有关associatedtype的更多信息,可以阅读本文

路由器

创建一个文件,将其命名为Router并将其放在Service文件夹中。 我们声明一个URLSessionTask类型的私有变量。 所有工作都在上面。 我们将其设为私有,因为我们不希望外部任何人都可以对其进行更改。



索取

在这里,我们使用URLSession.shared创建URLSession ,这是最简单的创建方法。 但是请记住,这种方法并非唯一。 您可以使用可以更改其行为的更复杂的URLSession配置。 本文将对此进行更多介绍

该请求是通过调用buildRequest函数创建的,该函数调用包装在do-try-catch中,因为buildRequest中的编码函数可能会引发异常。 Responsedataerror传递到完成。



建立要求

我们使用buildRequest函数创建请求。 此功能负责我们网络层中的所有重要工作。 本质上将EndPointType转换为URLRequest 。 当EndPoint变成一个请求时,我们可以将其传递给session 。 这里发生了很多事情,所以让我们看一下这些方法。 首先, 让我们检查buildRequest方法:

1.我们初始化URLRequest请求变量。 我们在其中设置基本URL,并添加将用于它的特定请求的路径。

2.从我们的EndPoint分配request.httpMethod http方法。

3.我们创建了一个do-try-catch块,因为我们的编码器可能会抛出错误。 通过创建一个大型的do-try-catch块,我们无需为每次尝试创建一个单独的块。

4.在switch中,检查route.task

5.根据任务的类型,我们称为相应的编码器。



配置参数

在路由器中创建configureParameters函数。



该函数负责转换我们的查询参数。 由于我们的API假定使用JSON形式的bodyParameters和将URLParameters转换为URL格式,因此我们只需将适当的参数传递给相应的转换函数,我们将在本文开头进行介绍。 如果使用包含各种编码类型的API,那么在这种情况下,我建议添加HTTPTask以及编码类型的其他枚举。 此清单应包含所有可能的编码类型。 之后,在configureParameters中再添加一个带有此枚举的参数。 根据其值,使用switch进行切换并进行所需的编码。

添加其他标题

在路由器中创建addAdditionalHeaders函数。



只需将所有必要的标头添加到请求中即可。

取消

取消功能看起来非常简单:



使用范例

现在,让我们尝试在一个真实示例中使用我们的网络层。 我们将连接到TheMovieDB以接收应用程序的数据。

电影终点

创建一个MovieEndPoint文件,并将其放置在EndPoint文件夹中。 MovieEndPoint与
和Moya中的TargetType。 在这里,我们改为实现自己的EndPointType。 在此链接中可以找到一篇文章,介绍如何使用Moya作为类似示例。

 import Foundation enum NetworkEnvironment { case qa case production case staging } public enum MovieApi { case recommended(id:Int) case popular(page:Int) case newMovies(page:Int) case video(id:Int) } extension MovieApi: EndPointType { var environmentBaseURL : String { switch NetworkManager.environment { case .production: return "https://api.themoviedb.org/3/movie/" case .qa: return "https://qa.themoviedb.org/3/movie/" case .staging: return "https://staging.themoviedb.org/3/movie/" } } var baseURL: URL { guard let url = URL(string: environmentBaseURL) else { fatalError("baseURL could not be configured.")} return url } var path: String { switch self { case .recommended(let id): return "\(id)/recommendations" case .popular: return "popular" case .newMovies: return "now_playing" case .video(let id): return "\(id)/videos" } } var httpMethod: HTTPMethod { return .get } var task: HTTPTask { switch self { case .newMovies(let page): return .requestParameters(bodyParameters: nil, urlParameters: ["page":page, "api_key":NetworkManager.MovieAPIKey]) default: return .request } } var headers: HTTPHeaders? { return nil } } 


电影模特

要将MovieModel和JSON数据模型解析为模型,使用了Decodable协议。 将此文件放在模型文件夹中。

注意 :要更详尽地了解Codable,Decodable和Encodable协议,可以阅读我的另一篇文章 ,其中详细介绍了使用它们的所有功能。

 import Foundation struct MovieApiResponse { let page: Int let numberOfResults: Int let numberOfPages: Int let movies: [Movie] } extension MovieApiResponse: Decodable { private enum MovieApiResponseCodingKeys: String, CodingKey { case page case numberOfResults = "total_results" case numberOfPages = "total_pages" case movies = "results" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: MovieApiResponseCodingKeys.self) page = try container.decode(Int.self, forKey: .page) numberOfResults = try container.decode(Int.self, forKey: .numberOfResults) numberOfPages = try container.decode(Int.self, forKey: .numberOfPages) movies = try container.decode([Movie].self, forKey: .movies) } } struct Movie { let id: Int let posterPath: String let backdrop: String let title: String let releaseDate: String let rating: Double let overview: String } extension Movie: Decodable { enum MovieCodingKeys: String, CodingKey { case id case posterPath = "poster_path" case backdrop = "backdrop_path" case title case releaseDate = "release_date" case rating = "vote_average" case overview } init(from decoder: Decoder) throws { let movieContainer = try decoder.container(keyedBy: MovieCodingKeys.self) id = try movieContainer.decode(Int.self, forKey: .id) posterPath = try movieContainer.decode(String.self, forKey: .posterPath) backdrop = try movieContainer.decode(String.self, forKey: .backdrop) title = try movieContainer.decode(String.self, forKey: .title) releaseDate = try movieContainer.decode(String.self, forKey: .releaseDate) rating = try movieContainer.decode(Double.self, forKey: .rating) overview = try movieContainer.decode(String.self, forKey: .overview) } } 


网络经理

在管理器文件夹中创建一个NetworkManager文件。 目前,NetworkManager仅包含两个静态属性:API密钥和描述要连接到的服务器类型的枚举。 NetworkManager还包含类型为MovieApi路由器



网络响应

在NetworkManager中创建NetworkResponse枚举。



在处理对请求的响应时,我们将使用此枚举,并显示相应的消息。

结果

在NetworkManager中创建一个结果枚举。



我们使用Result来确定请求是否成功。 如果不是,那么我们将返回一条错误消息并说明原因。

请求响应处理

创建handleNetworkResponse函数。 此函数采用一个参数(例如HTTPResponse),并返回Result。



在此函数中,根据从HTTPResponse接收到的statusCode,我们返回错误消息或请求成功的标志。 通常,范围为200..299的代码表示成功。

发出网络请求

因此,我们已经完成了一切工作,开始使用我们的网络层,让我们尝试发出一个请求。

我们将索取新电影的清单。 创建一个函数并将其命名为getNewMovies



让我们逐步进行:

1.我们用两个参数定义getNewMovies方法:分页页码和完成处理程序,它返回一个可选的Movie模型数组,或者一个可选的错误。

2.呼叫路由器 。 我们在关闭处传递页码和处理完成

3.如果没有网络或由于任何原因无法发出请求, URLSession将返回错误。 请注意,这不是API错误,此类错误在客户端上发生,通常是由于Internet连接质量差而发生的。

4.我们需要将响应 强制转换HTTPURLResponse ,因为我们需要访问statusCode属性。

5.声明结果并使用handleNetworkResponse方法对其进行初始化

6. 成功表示请求成功,我们收到了预期的答复。 然后,我们检查数据是否带有答案,如果不是,则只需通过return结束方法即可。

7.如果答案与数据一起出现,则有必要将接收到的数据解析到模型中。 之后,我们将生成的模型数组传递给完成。

8.发生错误时,只需将错误传递给完成即可

就是这样,这就是我们自己的网络层在纯Swift上工作的方式,而无需使用任何第三方Pod和库形式的依赖项。 为了进行测试api请求以获取电影列表,请创建具有NetworkManager属性的MainViewController并通过它调用getNewMovies方法。

  class MainViewController: UIViewController { var networkManager: NetworkManager! init(networkManager: NetworkManager) { super.init(nibName: nil, bundle: nil) self.networkManager = networkManager } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .green networkManager.getNewMovies(page: 1) { movies, error in if let error = error { print(error) } if let movies = movies { print(movies) } } } } 


小奖金

当您不了解在特定位置使用哪种占位符时,您在Xcode中遇到了一些情况? 例如,查看我们刚刚为Router编写的代码。



我们自己确定了NetworkRouterCompletion ,但是即使在这种情况下,也很容易忘记它是什么类型以及如何使用它。 但是,我们钟爱的Xcode会处理所有事情,只需双击占位符就足够了,Xcode将替换所需的类型。



结论

现在,我们已经实现了面向协议的网络层,该层非常易于使用,您可以随时对其进行自定义。 我们了解了它的功能以及所有机制的工作原理。

您可以在此存储库中找到源代码。

Source: https://habr.com/ru/post/zh-CN443514/


All Articles