{"id":335526,"date":"2022-07-10T03:00:06","date_gmt":"2022-07-10T03:00:06","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=335526"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=335526","title":{"rendered":"<span>Swift. \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432<\/span>"},"content":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9f8\/3e2\/0c7\/9f83e20c78d53d96069069dfb407ff79.png\" width=\"1124\" height=\"555\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/9f8\/3e2\/0c7\/9f83e20c78d53d96069069dfb407ff79.png\"\/><figcaption><\/figcaption><\/figure>\n<p>\u041f\u043e \u043a\u0440\u0430\u0439\u043d\u0435\u0439 \u043c\u0435\u0440\u0435 \u0432 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0434\u0435\u0441\u044f\u0442\u0438\u043b\u0435\u0442\u0438\u0435, \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043e\u0441\u0442\u0443\u043f \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442, \u043d\u0435\u0438\u043c\u043e\u0432\u0435\u0440\u043d\u043e \u0432\u043e\u0437\u0440\u043e\u0441\u043b\u043e. \u041f\u0440\u0438\u0447\u0435\u043c \u0434\u043b\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 REST \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439, \u0441 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043c preview. \u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0447\u0435\u043c, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0441\u0432\u043e\u0435\u0433\u043e \u0440\u043e\u0434\u0430 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0434\u043b\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0437 \u0441\u0435\u0442\u0438. \u0414\u0430\u043b\u0435\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d NetworkManager, \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f REST \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044e\u0442\u0441\u044f \u0444\u0430\u0439\u043b\u044b, \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430<\/h3>\n<p>\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f REST \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0438 \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f preview \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e, \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u043f\u043e\u044f\u0432\u043b\u0435\u043d\u0438\u0435\/\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435. \u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443 concurrency, \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u0443\u044e \u0432 iOS 13.<\/p>\n<h3>REST \u0437\u0430\u043f\u0440\u043e\u0441\u044b<\/h3>\n<p>\u041d\u0438\u0436\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043b\u0430\u0441\u0441, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0430 \u043b\u043e\u0433\u0438\u043a\u0430 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043e\u0442\u0432\u0435\u0442\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (URLResponse), \u043e\u0442 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0447\u0438\u043a\u0438 (\u043c\u043e\u0434\u0443\u043b\u0438) \u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442 Foundation framework.<\/p>\n<pre><code class=\"swift\">import Foundation  public class NetworkManager {  \/\/ MARK: - Properties  let session: URLSession let decoder: JSONDecoder  \/\/ MARK: - Lifecycle  public init(session: URLSession, decoder: JSONDecoder) { self.session = session self.decoder = decoder }  \/\/ MARK: - Methods  func handle&lt;T>(response: URLResponse, content: T) throws -> T { guard let response = response as? HTTPURLResponse else { throw \"Unknown response received\" }  guard let httpStatusCode = HttpStatusCode(rawValue: response.statusCode) else { throw \"Unknown http status code\" }  if httpStatusCode.isSuccessStatusCode { return content } else if let content = content as? Data { throw try decoder.decode(Request.Error.self, from: content) } else { throw Request.Error(code: response.statusCode) } } } <\/code><\/pre>\n<p>\u041d\u0435 \u0441\u0442\u043e\u0438\u0442 \u0443\u0434\u0438\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u0430\u043b\u0438\u0447\u0438\u044e \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f HTTPStatusCode, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u044d\u0442\u043e \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442, \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u0430\u043c\u0438 \u0432 URLResponse.<\/p>\n<pre><code class=\"swift\">import Foundation  public enum HttpStatusCode: Int { case unknown = -1  \/\/ Informational response case `continue` = 100 case switchingProtocols = 101 case processing = 102 case earlyHints = 103  \/\/ Success case ok = 200 case created = 201 case accepted = 202 case nonAuthoritativeInformation = 203 case noContent = 204 case resetContent = 205 case partialContent = 206 case multiStatus = 207 case alreadyReported = 208 case used = 226  \/\/ Redirection case multipleChoices = 300 case movedPermanently = 301 case found = 302 case seeOther = 303 case notModified = 304 case useProxy = 305 case switchProxy = 306 case temporaryRedirect = 307 case permanentRedirect = 308  \/\/ Client errors case badRequest = 400 case unauthorized = 401 case paymentRequired = 402 case forbidden = 403 case notFound = 404 case methodNotAllowed = 405 case notAcceptable = 406 case proxyAuthenticationRequired = 407 case requestTimeout = 408 case conflict = 409 case gone = 410 case lengthRequired = 411 case preconditionFailed = 412 case payloadTooLarge = 413 case urlTooLong = 414 case unsupportedMediaType = 415 case rangeNotSatisfiable = 416 case expectationFailed = 417 case teapot = 418 case misdirectedRequest = 421 case unprocessableEntity = 422 case locked = 423 case failedDependency = 424 case tooEarly = 425 case upgradeRequired = 426 case preconditionRequired = 428 case tooManyRequests = 429 case requestHeaderFieldsTooLarge = 431 case unavailableForLegalReasons = 451  \/\/ Server errors case internalServerError = 500 case notImplemented = 501 case badGateway = 502 case serviceUnavailable = 503 case gatewayTimeout = 504 case httpVersionNotSupported = 505 case variantAlsoNegotiates = 506 case insufficientStorage = 507 case loopDetected = 508 case notExtended = 510 case networkAuthenticationRequired = 511  public var isSuccessStatusCode: Bool { switch self.rawValue { case 200..&lt;300: return true default: return false } } } <\/code><\/pre>\n<h2>DataLoader<\/h2>\n<p>\u041d\u0430\u0438\u0431\u043e\u043b\u0435\u0435 \u0447\u0430\u0441\u0442\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f. \u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0441 \u0435\u0433\u043e \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f REST \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u0442\u043e \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0438\u0442\u0435\u043b\u044c\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u0435\u0449\u0435 \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u043b \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e\/\u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.<\/p>\n<pre><code class=\"swift\">import Foundation  public final class DataLoader: NetworkManager { @discardableResult public func dataRequest&lt;T: Decodable>(url: UrlCreatable,   method: HTTPMethod,   headers: [HTTPHeader] = [],   parameters: Request.Parameters? = nil) async throws -> T { let data = try await dataRequest( url: url, method: method, headers: headers, parameters: parameters )  return try decoder.decode(T.self, from: data) }  @discardableResult public func dataRequest(url: UrlCreatable, method: HTTPMethod, headers: [HTTPHeader] = [], parameters: Request.Parameters? = nil) async throws -> Data { let url = try url.create()  let request = try Request( url: url, method: method, headers: headers, parameters: parameters )  let (data, response) = try await session.data(for: request.urlRequest) return try handle(response: response, content: data) } } <\/code><\/pre>\n<p>UrlCreatable \u2014 \u044d\u0442\u043e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f URL, \u043a\u043e\u0442\u0440\u044b\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0442 \u0442\u0438\u043f\u044b String \u0438 URL.<\/p>\n<pre><code class=\"swift\">import Foundation  public protocol UrlCreatable { func create() throws -> URL }  extension String: UrlCreatable { public func create() throws -> URL { if let url = URL(string: self) { return url } else { throw \"Cannot create url\" } } }  extension URL: UrlCreatable { public func create() throws -> URL { return self } } <\/code><\/pre>\n<p>\u0421 HTTPMethod \u0438 HTTPHeader \u0442\u043e\u0436\u0435 \u0432\u0441\u0435 \u043f\u043e\u043d\u044f\u0442\u043d\u043e \u2014 \u044d\u0442\u043e \u0442\u0430\u043a\u0436\u0435 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0445 \u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0435, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u043c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 (URLRequest).<\/p>\n<pre><code class=\"swift\">import Foundation  public struct HTTPHeader: Hashable { public let name: String public let value: String  public init(name: String, value: String) { self.name = name self.value = value } }  extension HTTPHeader: CustomStringConvertible { public var description: String { \"\\(name): \\(value)\" } }  public extension HTTPHeader { static func accept(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Accept\", value: value) }  static func acceptCharset(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Accept-Charset\", value: value) }  static func acceptLanguage(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Accept-Language\", value: value) }  static func acceptEncoding(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Accept-Encoding\", value: value) }  static func authorization(username: String, password: String) -> HTTPHeader { let credential = Data(\"\\(username):\\(password)\".utf8).base64EncodedString() return authorization(\"Basic \\(credential)\") }  static func authorization(bearerToken: String) -> HTTPHeader { authorization(\"Bearer \\(bearerToken)\") }  static func authorization(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Authorization\", value: value) }  static func contentDisposition(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Content-Disposition\", value: value) }  static func contentType(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Content-Type\", value: value) }  static func contentLength(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Content-Length\", value: value) }  static func userAgent(_ value: String) -> HTTPHeader { HTTPHeader(name: \"User-Agent\", value: value) } }  public extension HTTPHeader { static func qualityEncoded(_ encodings: [String]) -> String { return encodings.enumerated().map { index, encoding in let quality = 1.0 - (Double(index) * 0.1) return \"\\(encoding);q=\\(quality)\" } .joined(separator: \", \") }  static let defaultAcceptEncoding: HTTPHeader = { let encodings = [\"br\", \"gzip\", \"deflate\"] let value = qualityEncoded(encodings) return .acceptEncoding(value) }()  static let defaultAcceptLanguage: HTTPHeader = { let encodings = Array(Locale.preferredLanguages.prefix(6)) let value = qualityEncoded(encodings) return .acceptLanguage(value) }()  static let defaultUserAgent: HTTPHeader = { let info = Bundle.main.infoDictionary let executable = (info?[\"CFBundleExecutable\"] as? String) ?? (ProcessInfo.processInfo.arguments.first?.split(separator: \"\/\").last.map(String.init)) ?? \"Unknown\" let bundle = info?[\"CFBundleIdentifier\"] as? String ?? \"Unknown\" let appVersion = info?[\"CFBundleShortVersionString\"] as? String ?? \"Unknown\" let appBuild = info?[\"CFBundleVersion\"] as? String ?? \"Unknown\"  let osNameVersion: String = { let version = ProcessInfo.processInfo.operatingSystemVersion let versionString = \"\\(version.majorVersion).\\(version.minorVersion).\\(version.patchVersion)\" let osName: String = { #if os(iOS) #if targetEnvironment(macCatalyst) return \"macOS(Catalyst)\" #else return \"iOS\" #endif #elseif os(watchOS) return \"watchOS\" #elseif os(tvOS) return \"tvOS\" #elseif os(macOS) return \"macOS\" #elseif os(Linux) return \"Linux\" #elseif os(Windows) return \"Windows\" #else return \"Unknown\" #endif }()  return \"\\(osName) \\(versionString)\" }()  let userAgent = \"\\(executable)\/\\(appVersion) (\\(bundle); build:\\(appBuild); \\(osNameVersion))\" return .userAgent(userAgent) }() } <\/code><\/pre>\n<pre><code class=\"swift\">import Foundation  public struct HTTPMethod: RawRepresentable, Equatable, Hashable { public let rawValue: String  public init(rawValue: String) { self.rawValue = rawValue } }  public extension HTTPMethod { static let connect = HTTPMethod(rawValue: \"CONNECT\") static let delete = HTTPMethod(rawValue: \"DELETE\") static let get = HTTPMethod(rawValue: \"GET\") static let head = HTTPMethod(rawValue: \"HEAD\") static let options = HTTPMethod(rawValue: \"OPTIONS\") static let patch = HTTPMethod(rawValue: \"PATCH\") static let post = HTTPMethod(rawValue: \"POST\") static let put = HTTPMethod(rawValue: \"PUT\") static let trace = HTTPMethod(rawValue: \"TRACE\") } <\/code><\/pre>\n<p>\u0410 \u0432\u043e\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u0430\u043c\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0443\u0436\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u043b\u043e\u0436\u043d\u0435\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\u0443\u0436\u043d\u043e \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043b\u0438\u0431\u043e \u0432 query \u0441\u0442\u0440\u043e\u043a\u0443, \u043b\u0438\u0431\u043e \u0432 httpBody.<\/p>\n<pre><code class=\"swift\">import AceExtensions import Foundation  public final class Request {  \/\/ MARK: - Properties  private(set) var urlRequest: URLRequest  \/\/ MARK: - Lifecycle  init(url: URL,  method: HTTPMethod,  headers: [HTTPHeader],  parameters: Parameters?) throws { urlRequest = URLRequest(url: url) urlRequest.httpMethod = method.rawValue urlRequest.allHTTPHeaderFields = headers.reduce(into: [:]) { $0[$1.name] = $1.value }  if let parameters = parameters { try encode(parameters: parameters) } }  \/\/ MARK: - Methods  private func encode(parameters: Parameters) throws { switch parameters.configuration.destination { case .query(let arrayEncoding, let boolEncoding): try appendQuery(parameters: parameters, arrayEncoding: arrayEncoding, boolEncoding: boolEncoding) case .httpBody: try addHttpBody(parameters: parameters) } }  private func appendQuery(parameters: Parameters,  arrayEncoding: Parameters.Configuration.ArrayEncoding,  boolEncoding: Parameters.Configuration.BoolEncoding) throws { guard let dictionary = parameters.object as? [String: Any] else { throw \"Query parameters is not [String: Any] dictionary\" }  guard let url = urlRequest.url,   var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { throw \"Cannot create url components from url request\" }  for (name, value) in dictionary { let items = createQueryItems( name: name, value: value, arrayEncoding: arrayEncoding, boolEncoding: boolEncoding )  var queryItems = components.queryItems ?? [] queryItems.append(contentsOf: items) components.queryItems = queryItems }  if let url = components.url { urlRequest.url = url }  if parameters.configuration.updateHeaders { var headers = urlRequest.allHTTPHeaderFields ?? [:] headers[\"Content-Type\"] = \"application\/x-www-form-urlencoded; charset=utf-8\" urlRequest.allHTTPHeaderFields = headers } }  private func addHttpBody(parameters: Parameters) throws { if let data = parameters.object as? Data { urlRequest.httpBody = data } else if JSONSerialization.isValidJSONObject(parameters.object) { let data = try JSONSerialization.data(withJSONObject: parameters.object, options: .fragmentsAllowed) urlRequest.httpBody = data }  if parameters.configuration.updateHeaders { var headers = urlRequest.allHTTPHeaderFields ?? [:] headers[\"Content-Type\"] = \"application\/json; charset=utf-8\" urlRequest.allHTTPHeaderFields = headers } }  private func createQueryItems(name: String,   value: Any,   arrayEncoding: Parameters.Configuration.ArrayEncoding,   boolEncoding: Parameters.Configuration.BoolEncoding) -> [URLQueryItem] { switch value { case let boolean as Bool: let value = encodeQuery(boolean: boolean, encoding: boolEncoding) let queryItem = URLQueryItem(name: name, value: value) return [queryItem] case let number as NSNumber: let queryItem = URLQueryItem(name: name, value: number.stringValue) return [queryItem] case let array as [Any]: let name = encodeQuery(array: name, encoding: arrayEncoding) let queryItems = array.flatMap { value in createQueryItems(name: name, value: value, arrayEncoding: arrayEncoding, boolEncoding: boolEncoding) } return queryItems case let dictionary as [String: Any]: let queryItems: [[URLQueryItem]] = dictionary.map { key, value in let name = name + \"[\\(key)]\" return createQueryItems(name: name, value: value, arrayEncoding: arrayEncoding, boolEncoding: boolEncoding) } return queryItems.flatMap { $0 } default: let queryItem = URLQueryItem(name: name, value: \"\\(value)\") return [queryItem] } }  private func encodeQuery(array name: String, encoding: Parameters.Configuration.ArrayEncoding) -> String { switch encoding { case .brackets: return name + \"[]\" case .noBrackets: return name } }  func encodeQuery(boolean value: Bool, encoding: Parameters.Configuration.BoolEncoding) -> String { switch encoding { case .numeric: return (value as NSNumber).stringValue case .literal: return String(value) } } } <\/code><\/pre>\n<p>\u041c\u043e\u0436\u0435\u0442 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0443\u0442\u0430\u043d\u0430, \u043d\u043e \u043d\u0430 \u0434\u0435\u043b\u0435 \u044d\u0442\u043e \u043d\u0430\u0431\u043e\u0440 \u043a\u043b\u044e\u0447: \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435, \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u043c \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c, \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u043c \u0432 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0435 Parameters.<\/p>\n<pre><code class=\"swift\">import Foundation  public extension Request { struct Parameters {  \/\/ MARK: - Types  public struct Configuration { public enum Destination { case query(arrayEncoding: ArrayEncoding, boolEncoding: BoolEncoding) case httpBody }  public enum ArrayEncoding { case brackets case noBrackets }  public enum BoolEncoding { case numeric case literal }  public var destination: Destination public var updateHeaders: Bool  public static let `defaultQuery` = Configuration( destination: .query(arrayEncoding: .brackets, boolEncoding: .literal), updateHeaders: true )  public static let `defaultJson` = Configuration( destination: .httpBody, updateHeaders: true ) }  \/\/ MARK: - Properties  public let object: Any public let configuration: Configuration  \/\/ MARK: - Lifecycle  public init(object: [String: Any], configuration: Configuration = .defaultQuery) { self.object = object self.configuration = configuration }  public init&lt;T: Encodable>(object: T, configuration: Configuration = .defaultJson) throws { self.object = try JSONEncoder().encode(object) self.configuration = configuration }  public init(object: Data, configuration: Configuration) throws { self.object = object self.configuration = configuration } } }  \/\/ MARK: - ExpressibleByDictionaryLiteral  extension Request.Parameters: ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (String, Any)...) { let dictionary = Dictionary(uniqueKeysWithValues: elements) let object = (dictionary as [String: Any?]).compactMapValues { $0 } self.init(object: object, configuration: .defaultQuery) } } <\/code><\/pre>\n<p>\u041d\u0430 \u044d\u0442\u043e\u043c \u044f\u0434\u0440\u043e NetworkManager \u0433\u043e\u0442\u043e\u0432\u043e. \u0412 \u043e\u0447\u0435\u0440\u0435\u0434\u043d\u043e\u0439 \u0440\u0430\u0437 \u043e\u0431\u0440\u0430\u0449\u0430\u044e \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044f\u0434\u0440\u043e \u043d\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 UI \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u0432, \u0430 \u0437\u043d\u0430\u0447\u0438\u0442 \u043b\u0435\u0433\u0447\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.<\/p>\n<h2>FileLoader<\/h2>\n<p>\u0418\u043c\u0435\u044f \u0442\u0430\u043a\u043e\u0435 \u044f\u0434\u0440\u043e \u043d\u0435\u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u2014 FileLoader \u0434\u043b\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u0430\u0439\u043b\u043e\u0432.<\/p>\n<pre><code class=\"swift\">import Foundation  public final class FileLoader: NetworkManager {  public init(session: URLSession) { super.init(session: session, decoder: JSONDecoder()) }  @discardableResult public func downloadRequest(url: UrlCreatable, method: HTTPMethod, headers: [HTTPHeader] = [], parameters: Request.Parameters? = nil) async throws -> URL { let url = try url.create()  let request = try Request( url: url, method: method, headers: headers, parameters: parameters )  let (fileUrl, response) = try await session.download(for: request.urlRequest) return try handle(response: response, content: fileUrl) } } <\/code><\/pre>\n<h2>ImageLoader<\/h2>\n<p>\u0421 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u043e\u0439 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439 \u0432\u0441\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u043b\u043e\u0436\u043d\u0435\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f preview. \u041d\u043e, \u043e\u0447\u0435\u043d\u044c \u0432\u0430\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u044d\u0442\u0430 \u043b\u043e\u0433\u0438\u043a\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u043b\u0430\u0441\u044c \u0432 \u043f\u043e\u0442\u043e\u043a\u0435, \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e\u043c main, \u0438\u043d\u0430\u0447\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b \u0442\u043e\u0440\u043c\u043e\u0437\u0430 \u0432 \u0440\u0430\u0431\u043e\u0442\u0435 UI. \u041f\u043e \u044d\u0442\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435 \u0442\u0430\u043a \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u043c\u044b\u0439 ImageLoader \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f actor, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f Task \u0434\u0430\u043d\u043d\u043e\u0433\u043e actor, \u0430 \u043d\u0435 MainActor. \u0422\u0430\u043a\u0436\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0443\u0436\u0435 \u0438\u0434\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u043c\u0438, \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u043e\u0442 UI \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430 \u2014 SwiftUI.<\/p>\n<pre><code class=\"swift\">import SwiftUI  public actor ImageLoader {  \/\/ MARK: - Properties  private let fileLoader: FileLoader private let storage = ImagesStorage()  \/\/ MARK: - Lifecycle  public init(session: URLSession) { fileLoader = FileLoader(session: session) }  \/\/ MARK: - Methods  public func loadImage(at url: URL) async -> UIImage? { if let image = storage.loadObject(for: url) { return image } else { let image: UIImage? = try? await downloadImage(url: url) storage.save(object: image, for: url) return image } }  public func createThumb(of image: UIImage?, size: CGSize) async -> UIImage? { guard let image = image else { return nil }  let ratio = image.size.width \/ image.size.height let width: CGFloat let height: CGFloat  if image.size.width > image.size.height { width = size.width height = width \/ ratio } else { height = size.height width = ratio * height }  let size = CGSize(width: width, height: height) return await image.byPreparingThumbnail(ofSize: size) }  public func fileUrl(forImageAt url: URL) async -> URL? { return storage.fileUrl(for: url) }  @discardableResult public func downloadImage(url: UrlCreatable,   method: HTTPMethod = .get,   headers: [HTTPHeader] = [],   parameters: Request.Parameters? = nil) async throws -> Image? { let uiImage: UIImage? = try await downloadImage( url: url, method: method, headers: headers, parameters: parameters )  if let uiImage = uiImage { return Image(uiImage: uiImage) } else { return nil } }  @discardableResult public func downloadImage(url: UrlCreatable,   method: HTTPMethod = .get,   headers: [HTTPHeader] = [],   parameters: Request.Parameters? = nil) async throws -> UIImage? { let fileUrl = try await fileLoader.downloadRequest( url: url, method: method, headers: headers, parameters: parameters )  return UIImage(contentsOfFile: fileUrl.path) } } <\/code><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u0441 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043c preview \u0432\u0441\u0435 \u043f\u043e\u043d\u044f\u0442\u043d\u043e \u2014 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434\u00a0<strong>byPreparingThumbnail(ofSize:)<\/strong>, \u0442\u043e \u0441 \u043b\u043e\u0433\u0438\u043a\u043e\u0439 \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0443\u0436\u043d\u043e \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c\u0441\u044f. \u0412\u0435\u0434\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c NSCache&lt;KeyType, ObjectType> \u043e\u0448\u0438\u0431\u043e\u0447\u043d\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0438 \u0442\u0430\u043a \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043e \u0432 \u041e\u0417\u0423, \u043f\u0440\u0438 \u043d\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 \u043c\u043e\u0434\u0435\u043b\u0438. \u0410 \u0432\u043e\u0442 \u0434\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0431\u044b\u043b\u0438 \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u0432 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u0443\u044e \u043f\u0430\u043c\u044f\u0442\u044c, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0439 ImagesStorage \u043e\u0431\u044a\u0435\u043a\u0442, \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0449\u0438\u0439 \u0432 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u0443\u044e \u043f\u0430\u043c\u044f\u0442\u044c \u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044e\u0449\u0438\u0439 \u0438\u0437 \u043d\u0435\u0451.<\/p>\n<pre><code class=\"swift\">import Foundation  public protocol NetworkCache { associatedtype Item func save(object: Item?, for requestUrl: URL) func loadObject(for requestUrl: URL) -> Item? func hasCache(for requestUrl: URL) -> Bool } <\/code><\/pre>\n<pre><code class=\"swift\">import UIKit  public final class ImagesStorage: CachedStorage, NetworkCache {  \/\/ MARK: - Methods  public func save(object: UIImage?, for requestUrl: URL) { let data = object?.pngData() let url = fileUrl(for: requestUrl) save(data: data, at: url) }  public func loadObject(for requestUrl: URL) -> UIImage? { let url = fileUrl(for: requestUrl)  if let data = loadData(at: url) { return UIImage(data: data) } else { return nil } }  public func hasCache(for requestUrl: URL) -> Bool { let url = fileUrl(for: requestUrl) return hasCache(at: url) }  public func fileUrl(for requestUrl: URL) -> URL { let url = cachesUrl() let className = String(describing: Self.self) let hash = (requestUrl.absoluteString as NSString).hash  return url .appendingPathComponent(className) .appendingPathComponent(String(hash)) .appendingPathComponent(requestUrl.lastPathComponent) } }  <\/code><\/pre>\n<pre><code class=\"swift\">import Foundation  public class CachedStorage {  \/\/ MARK: - Properties  private let fileManager: FileManager = .default  \/\/ MARK: - Methods  public func save(data: Data?, at url: URL) { let directoryUrl = url.deletingLastPathComponent()  if !directoryExistsAtPath(directoryUrl.path) { createCacheDirectory(at: directoryUrl) }  fileManager.createFile(atPath: url.path, contents: data, attributes: nil) }  public func loadData(at url: URL) -> Data? { return try? Data(contentsOf: url) }  public func hasCache(at url: URL) -> Bool { return fileManager.fileExists(atPath: url.path) }  private func directoryExistsAtPath(_ path: String) -> Bool { var isDirectory = ObjCBool(true) let exists = fileManager.fileExists(atPath: path, isDirectory: &amp;isDirectory) return exists &amp;&amp; isDirectory.boolValue }  private func createCacheDirectory(at url: URL) { do { try fileManager.createDirectory(at: url, withIntermediateDirectories: true) } catch { assertionFailure(\"Cache directory not created: \\(error.localizedDescription)\") } }  public func cachesUrl() -> URL { let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)  if let url = urls.first { return url } else { return fileManager.temporaryDirectory } } } <\/code><\/pre>\n<p>\u0412\u043d\u0438\u043c\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0447\u0438\u0442\u0430\u0442\u0435\u043b\u044f \u043c\u043e\u0436\u0435\u0442 \u0443\u0434\u0438\u0432\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u00a0<strong>hash<\/strong>\u00a0\u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430\u00a0<strong>NSString<\/strong>\u00a0\u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e\u00a0<strong>md5<\/strong>\u00a0hash. \u0412 \u044d\u0442\u043e\u043c \u043c\u0435\u0441\u0442\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u043b \u0441\u0435\u0431\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043f\u0430\u0443\u0437\u0443 \u0438 \u0432\u044b\u043f\u0438\u0442\u044c \u0447\u0430\u0448\u0435\u0447\u043a\u0443 \u0445\u043e\u0440\u043e\u0448\u0435\u0433\u043e \u043a\u043e\u0444\u0435, \u043f\u043e\u0441\u043b\u0435 \u0447\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043b \u043d\u0430\u0438\u0431\u043e\u043b\u0435\u0435 \u043f\u0440\u043e\u0441\u0442\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435.<\/p>\n<p>\u042d\u0442\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442 \u0443\u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d \u043e\u0442 CachedStorage, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0444\u0430\u0439\u043b\u044b \u0432 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u0443\u044e \u043f\u0430\u043c\u044f\u0442\u044c \u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u0438\u0437 \u043d\u0435\u0451 \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u0442 NetworkCache \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.<\/p>\n<h3>\u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f<\/h3>\n<p>\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c URLSession.<\/p>\n<pre><code class=\"swift\">import Foundation  extension URLSession { static let `default`: URLSession = { let configuration = URLSessionConfiguration.default configuration.waitsForConnectivity = true configuration.timeoutIntervalForResource = configuration.timeoutIntervalForRequest return URLSession(configuration: configuration) }()  static let background: URLSession = { let configuration = URLSessionConfiguration.background(withIdentifier: \"background-session\") return URLSession(configuration: configuration) }() } <\/code><\/pre>\n<p>\u0414\u0430\u043b\u0435\u0435 \u043e\u0431\u044a\u044f\u0432\u0438\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0443\u0434\u043e\u0432\u043b\u0435\u0442\u0432\u043e\u0440\u044f\u044e\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0430.<\/p>\n<pre><code class=\"swift\">extension DataLoader { convenience init(session: URLSession = .default) { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601  self.init(session: session, decoder: decoder) }  private func baseUrl() throws -> URL { guard let host = Bundle.main.object(forInfoDictionaryKey: \"Host\") as? String else { throw \"Host variable is not secified\" }  guard let url = URL(string: host) else { throw \"Cannot create url\" }  return url.appendingPathComponent(\"swagger\") }  private var accessToken: String { let secureSettings = SecureSettings() return secureSettings.accessToken ?? \"\" }  @discardableResult public func dataRequest(path: String, method: HTTPMethod, headers: [HTTPHeader] = [], parameters: Request.Parameters? = nil) async throws -> Data { let url = try baseUrl().appendingPathComponent(path)  return try await dataRequest( url: url, method: method, headers: headers, parameters: parameters ) }  @discardableResult public func dataRequest&lt;T: Decodable>(path: String,   method: HTTPMethod,   headers: [HTTPHeader] = [],   parameters: Request.Parameters? = nil) async throws -> T { let url = try baseUrl().appendingPathComponent(path)  return try await dataRequest( url: url, method: method, headers: headers, parameters: parameters ) } }<\/code><\/pre>\n<p>\u0418, \u043d\u0430\u043a\u043e\u043d\u0435\u0446, \u043e\u0431\u044a\u044f\u0432\u0438\u0442\u044c REST \u043c\u0435\u0442\u043e\u0434\u044b.<\/p>\n<pre><code class=\"swift\">extension DataLoader {  func login(userName: String, password: String) async throws -> LoginResponse { return try await dataRequest( path: \"auth\/login\", method: .get, parameters: [\"userName\": username, \"password\": password] ) }  }<\/code><\/pre>\n<p>\u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u043a\u0430\u0436\u0443\u0449\u0443\u044e\u0441\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0442\u0443 \u0440\u0435\u0448\u0435\u043d\u0438\u044f, \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 \u043d\u0430 \u0440\u0443\u043a\u0430\u0445 \u043c\u043e\u0449\u043d\u044b\u0439 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u0430\u043a \u0434\u0430\u043d\u043d\u044b\u0445, \u0442\u0430\u043a \u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439.<\/p>\n<pre><code class=\"swift\">class TestViewController: UIViewController {   func loadPreviews(of news: [NewsViewModel]) async {     await withTaskGroup(of: Void.self) { group in       for news in news {         group.addTask { [weak self] in           await self?.loadImage(of: news)         }       }     }   }    private func loadImage(of news: NewsViewModel) async {     guard let url = news.underlyingModel.preview else {       return     }      if let image = await imageLoader.loadImage(at: url) {       news.preview = Image(uiImage: image)     }   }  }<\/code><\/pre>\n<h3>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h3>\n<p>\u0412\u043f\u043e\u043b\u043d\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0447\u0442\u043e Apple \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u044f\u0442 \u0441\u0432\u043e\u0435\u0433\u043e \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u0430 Alamofire. \u0422\u0435\u043c \u0431\u043e\u043b\u0435\u0435, \u0447\u0442\u043e Alamofire \u0438\u043c\u0435\u0435\u0442 \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043a\u0438 \u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u0430 \u0441\u0442\u043e\u0438\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u0441\u043f\u0430\u0433\u0435\u0442\u0442\u0438 \u043a\u043e\u0434. \u0412 \u043b\u044e\u0431\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0438\u043c\u0435\u0442\u044c \u043f\u043e\u0434 \u0440\u0443\u043a\u043e\u0439 \u0442\u0430\u043a\u043e\u0439 \u043d\u0435\u043c\u0430\u043b\u043e\u0432\u0430\u0436\u043d\u044b\u0439 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u0432\u043e\u0432\u0441\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u043b\u0438\u0448\u043d\u0438\u043c.<\/p>\n<ol>\n<li>\n<p><a href=\"https:\/\/developer.apple.com\/documentation\/foundation\/urlsession\" rel=\"noopener noreferrer nofollow\">URLSession<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/developer.apple.com\/documentation\/foundation\/urlrequest\" rel=\"noopener noreferrer nofollow\">URLRequest<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/developer.apple.com\/documentation\/foundation\/urlresponse\" rel=\"noopener noreferrer nofollow\">URLResponse<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/Alamofire\/Alamofire\" rel=\"noopener noreferrer nofollow\">Alamofire<\/a><\/p>\n<\/li>\n<\/ol>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"v-portal\" style=\"display:none;\"><\/div>\n<\/div>\n<p> <!----> <!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/676092\/\"> https:\/\/habr.com\/ru\/post\/676092\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>\u041f\u043e \u043a\u0440\u0430\u0439\u043d\u0435\u0439 \u043c\u0435\u0440\u0435 \u0432 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0434\u0435\u0441\u044f\u0442\u0438\u043b\u0435\u0442\u0438\u0435, \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043e\u0441\u0442\u0443\u043f \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442, \u043d\u0435\u0438\u043c\u043e\u0432\u0435\u0440\u043d\u043e \u0432\u043e\u0437\u0440\u043e\u0441\u043b\u043e. \u041f\u0440\u0438\u0447\u0435\u043c \u0434\u043b\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 REST \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439, \u0441 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435\u043c preview. \u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0447\u0435\u043c, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0441\u0432\u043e\u0435\u0433\u043e \u0440\u043e\u0434\u0430 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0434\u043b\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0437 \u0441\u0435\u0442\u0438. \u0414\u0430\u043b\u0435\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d NetworkManager, \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f REST \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044e\u0442\u0441\u044f \u0444\u0430\u0439\u043b\u044b, \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f.<\/p>\n<h3>\u0417\u0430\u0434\u0430\u0447\u0430<\/h3>\n<p>\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f REST \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0438 \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f preview \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e, \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u043f\u043e\u044f\u0432\u043b\u0435\u043d\u0438\u0435\/\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435. \u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443 concurrency, \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u0443\u044e \u0432 iOS 13.<\/p>\n<h3>REST \u0437\u0430\u043f\u0440\u043e\u0441\u044b<\/h3>\n<p>\u041d\u0438\u0436\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043b\u0430\u0441\u0441, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0430 \u043b\u043e\u0433\u0438\u043a\u0430 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043e\u0442\u0432\u0435\u0442\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (URLResponse), \u043e\u0442 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0447\u0438\u043a\u0438 (\u043c\u043e\u0434\u0443\u043b\u0438) \u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442 Foundation framework.<\/p>\n<pre><code class=\"swift\">import Foundation  public class NetworkManager {  \/\/ MARK: - Properties  let session: URLSession let decoder: JSONDecoder  \/\/ MARK: - Lifecycle  public init(session: URLSession, decoder: JSONDecoder) { self.session = session self.decoder = decoder }  \/\/ MARK: - Methods  func handle&lt;T>(response: URLResponse, content: T) throws -> T { guard let response = response as? HTTPURLResponse else { throw \"Unknown response received\" }  guard let httpStatusCode = HttpStatusCode(rawValue: response.statusCode) else { throw \"Unknown http status code\" }  if httpStatusCode.isSuccessStatusCode { return content } else if let content = content as? Data { throw try decoder.decode(Request.Error.self, from: content) } else { throw Request.Error(code: response.statusCode) } } } <\/code><\/pre>\n<p>\u041d\u0435 \u0441\u0442\u043e\u0438\u0442 \u0443\u0434\u0438\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u0430\u043b\u0438\u0447\u0438\u044e \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f HTTPStatusCode, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u044d\u0442\u043e \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442, \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u0430\u043c\u0438 \u0432 URLResponse.<\/p>\n<pre><code class=\"swift\">import Foundation  public enum HttpStatusCode: Int { case unknown = -1  \/\/ Informational response case `continue` = 100 case switchingProtocols = 101 case processing = 102 case earlyHints = 103  \/\/ Success case ok = 200 case created = 201 case accepted = 202 case nonAuthoritativeInformation = 203 case noContent = 204 case resetContent = 205 case partialContent = 206 case multiStatus = 207 case alreadyReported = 208 case used = 226  \/\/ Redirection case multipleChoices = 300 case movedPermanently = 301 case found = 302 case seeOther = 303 case notModified = 304 case useProxy = 305 case switchProxy = 306 case temporaryRedirect = 307 case permanentRedirect = 308  \/\/ Client errors case badRequest = 400 case unauthorized = 401 case paymentRequired = 402 case forbidden = 403 case notFound = 404 case methodNotAllowed = 405 case notAcceptable = 406 case proxyAuthenticationRequired = 407 case requestTimeout = 408 case conflict = 409 case gone = 410 case lengthRequired = 411 case preconditionFailed = 412 case payloadTooLarge = 413 case urlTooLong = 414 case unsupportedMediaType = 415 case rangeNotSatisfiable = 416 case expectationFailed = 417 case teapot = 418 case misdirectedRequest = 421 case unprocessableEntity = 422 case locked = 423 case failedDependency = 424 case tooEarly = 425 case upgradeRequired = 426 case preconditionRequired = 428 case tooManyRequests = 429 case requestHeaderFieldsTooLarge = 431 case unavailableForLegalReasons = 451  \/\/ Server errors case internalServerError = 500 case notImplemented = 501 case badGateway = 502 case serviceUnavailable = 503 case gatewayTimeout = 504 case httpVersionNotSupported = 505 case variantAlsoNegotiates = 506 case insufficientStorage = 507 case loopDetected = 508 case notExtended = 510 case networkAuthenticationRequired = 511  public var isSuccessStatusCode: Bool { switch self.rawValue { case 200..&lt;300: return true default: return false } } } <\/code><\/pre>\n<h2>DataLoader<\/h2>\n<p>\u041d\u0430\u0438\u0431\u043e\u043b\u0435\u0435 \u0447\u0430\u0441\u0442\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f. \u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0441 \u0435\u0433\u043e \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f REST \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u0442\u043e \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0438\u0442\u0435\u043b\u044c\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u0435\u0449\u0435 \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u043b \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e\/\u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.<\/p>\n<pre><code class=\"swift\">import Foundation  public final class DataLoader: NetworkManager { @discardableResult public func dataRequest&lt;T: Decodable>(url: UrlCreatable,   method: HTTPMethod,   headers: [HTTPHeader] = [],   parameters: Request.Parameters? = nil) async throws -> T { let data = try await dataRequest( url: url, method: method, headers: headers, parameters: parameters )  return try decoder.decode(T.self, from: data) }  @discardableResult public func dataRequest(url: UrlCreatable, method: HTTPMethod, headers: [HTTPHeader] = [], parameters: Request.Parameters? = nil) async throws -> Data { let url = try url.create()  let request = try Request( url: url, method: method, headers: headers, parameters: parameters )  let (data, response) = try await session.data(for: request.urlRequest) return try handle(response: response, content: data) } } <\/code><\/pre>\n<p>UrlCreatable \u2014 \u044d\u0442\u043e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f URL, \u043a\u043e\u0442\u0440\u044b\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0442 \u0442\u0438\u043f\u044b String \u0438 URL.<\/p>\n<pre><code class=\"swift\">import Foundation  public protocol UrlCreatable { func create() throws -> URL }  extension String: UrlCreatable { public func create() throws -> URL { if let url = URL(string: self) { return url } else { throw \"Cannot create url\" } } }  extension URL: UrlCreatable { public func create() throws -> URL { return self } } <\/code><\/pre>\n<p>\u0421 HTTPMethod \u0438 HTTPHeader \u0442\u043e\u0436\u0435 \u0432\u0441\u0435 \u043f\u043e\u043d\u044f\u0442\u043d\u043e \u2014 \u044d\u0442\u043e \u0442\u0430\u043a\u0436\u0435 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0445 \u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0435, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u043e\u043c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 (URLRequest).<\/p>\n<pre><code class=\"swift\">import Foundation  public struct HTTPHeader: Hashable { public let name: String public let value: String  public init(name: String, value: String) { self.name = name self.value = value } }  extension HTTPHeader: CustomStringConvertible { public var description: String { \"\\(name): \\(value)\" } }  public extension HTTPHeader { static func accept(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Accept\", value: value) }  static func acceptCharset(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Accept-Charset\", value: value) }  static func acceptLanguage(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Accept-Language\", value: value) }  static func acceptEncoding(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Accept-Encoding\", value: value) }  static func authorization(username: String, password: String) -> HTTPHeader { let credential = Data(\"\\(username):\\(password)\".utf8).base64EncodedString() return authorization(\"Basic \\(credential)\") }  static func authorization(bearerToken: String) -> HTTPHeader { authorization(\"Bearer \\(bearerToken)\") }  static func authorization(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Authorization\", value: value) }  static func contentDisposition(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Content-Disposition\", value: value) }  static func contentType(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Content-Type\", value: value) }  static func contentLength(_ value: String) -> HTTPHeader { HTTPHeader(name: \"Content-Length\", value: value) }  static func userAgent(_ value: String) -> HTTPHeader { HTTPHeader(name: \"User-Agent\", value: value) } }  public extension HTTPHeader { static func qualityEncoded(_ encodings: [String]) -> String { return encodings.enumerated().map { index, encoding in let quality = 1.0 - (Double(index) * 0.1) return \"\\(encoding);q=\\(quality)\" } .joined(separator: \", \") }  static let defaultAcceptEncoding: HTTPHeader = { let encodings = [\"br\", \"gzip\", \"deflate\"] let value = qualityEncoded(encodings) return .acceptEncoding(value) }()  static let defaultAcceptLanguage: HTTPHeader = { let encodings = Array(Locale.preferredLanguages.prefix(6)) let value = qualityEncoded(encodings) return .acceptLanguage(value) }()  static let defaultUserAgent: HTTPHeader = { let info = Bundle.main.infoDictionary let executable = (info?[\"CFBundleExecutable\"] as? String) ?? (ProcessInfo.processInfo.arguments.first?.split(separator: \"\/\").last.map(String.init)) ?? \"Unknown\" let bundle = info?[\"CFBundleIdentifier\"] as? String ?? \"Unknown\" let appVersion = info?[\"CFBundleShortVersionString\"] as? String ?? \"Unknown\" let appBuild = info?[\"CFBundleVersion\"] as? String ?? \"Unknown\"  let osNameVersion: String = { let version = ProcessInfo.processInfo.operatingSystemVersion let versionString = \"\\(version.majorVersion).\\(version.minorVersion).\\(version.patchVersion)\" let osName: String = { #if os(iOS) #if targetEnvironment(macCatalyst) return \"macOS(Catalyst)\" #else return \"iOS\" #endif #elseif os(watchOS) return \"watchOS\" #elseif os(tvOS) return \"tvOS\" #elseif os(macOS) return \"macOS\" #elseif os(Linux) return \"Linux\" #elseif os(Windows) return \"Windows\" #else return \"Unknown\" #endif }()  return \"\\(osName) \\(versionString)\" }()  let userAgent = \"\\(executable)\/\\(appVersion) (\\(bundle); build:\\(appBuild); \\(osNameVersion))\" return .userAgent(userAgent) }() } <\/code><\/pre>\n<pre><code class=\"swift\">import Foundation  public struct HTTPMethod: RawRepresentable, Equatable, Hashable { public let rawValue: String  public init(rawValue: String) { self.rawValue = rawValue } }  public extension HTTPMethod { static let connect = HTTPMethod(rawValue: \"CONNECT\") static let delete = HTTPMethod(rawValue: \"DELETE\") static let get = HTTPMethod(rawValue: \"GET\") static let head = HTTPMethod(rawValue: \"HEAD\") static let options = HTTPMethod(rawValue: \"OPTIONS\") static let patch = HTTPMethod(rawValue: \"PATCH\") static let post = HTTPMethod(rawValue: \"POST\") static let put = HTTPMethod(rawValue: \"PUT\") static let trace = HTTPMethod(rawValue: \"TRACE\") } <\/code><\/pre>\n<p>\u0410 \u0432\u043e\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u0430\u043c\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0443\u0436\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u043b\u043e\u0436\u043d\u0435\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\u0443\u0436\u043d\u043e \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043b\u0438\u0431\u043e \u0432 query \u0441\u0442\u0440\u043e\u043a\u0443, \u043b\u0438\u0431\u043e \u0432 httpBody.<\/p>\n<pre><code class=\"swift\">import AceExtensions import Foundation  public final class Request {  \/\/ MARK: - Properties  private(set) var urlRequest: URLRequest  \/\/ MARK: - Lifecycle  init(url: URL,  method: HTTPMethod,  headers: [HTTPHeader],  parameters: Parameters?) throws { urlRequest = URLRequest(url: url) urlRequest.httpMethod = method.rawValue urlRequest.allHTTPHeaderFields = headers.reduce(into: [:]) { $0[$1.name] = $1.value }  if let parameters<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-335526","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/335526","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=335526"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/335526\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=335526"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=335526"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=335526"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}