Networking shouldn’t be so hard

Networking shouldn’t be so hard

2019/4/24に行われたLINE Developer meetup #53 in KYOTOでの登壇資料です

53850955f15249a1a9dc49df6113e400?s=128

LINE Developers

April 24, 2019
Tweet

Transcript

  1. Networking shouldn't be so hard LINE Developer Meetup #53 Kyoto

    | 2019.4.24 By @onevcat
  2. About me Wei Wang (@onevcat) iOS developer from 2010 Now

    working on LINE SDK Created some libraries like Kingfisher, APNGKit, etc. Find me in my blog1, GitHub2 or email3. 3 wei.wang@linecorp.com 2 https:"#github.com/onevcat 1 https:"#onevcat.com
  3. Agenda — Networking is not hard. — Networking is hard.

    — Networking should not be hard.
  4. A story of myself

  5. Once upon a time When I was just starting iOS...

  6. None
  7. My toolkit (2010, iOS 4) — NSURLConnection — stig/json-framework (SBJson)

    — UITableView
  8. Is it Hard?

  9. Is it Hard? No

  10. Very soon later I found something wrong...

  11. None
  12. With help of a networking wrapper/ framework — NSURLSession —

    AFNetworking — Alamofire — !"#
  13. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) } task.resume()
  14. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) } task.resume()
  15. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) } task.resume()
  16. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) } task.resume()
  17. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) !" Or whatever you want to do. } task.resume()
  18. Post Request !" let request = URLRequest(url: URL(string: "https:!"httpbin.org/get")!) let

    request = URLRequest(url: URL(string: "https:!"httpbin.org/post")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } !" "bar" print((dic["form"] as! [String: Any])["foo"] as! String) } task.resume()
  19. Post Request !" let request = URLRequest(url: URL(string: "https:!"httpbin.org/get")!) var

    request = URLRequest(url: URL(string: "https:!"httpbin.org/post")!) request.httpMethod = "POST" request.httpBody = "foo=bar".data(using: .utf8) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } !" "bar" print((dic["form"] as! [String: Any])["foo"] as! String) } task.resume()
  20. Post Request !" let request = URLRequest(url: URL(string: "https:!"httpbin.org/get")!) var

    request = URLRequest(url: URL(string: "https:!"httpbin.org/post")!) request.httpMethod = "POST" request.httpBody = "foo=bar".data(using: .utf8) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } !" "bar" print((dic["form"] as! [String: Any])["foo"] as! String) } task.resume()
  21. Is it Hard?

  22. Is it Hard? No

  23. Creating a Client To make things easier! — Request —

    Response
  24. What is a Client — Create and setup Requests. —

    Send the request with a session. — Handle/process the Responses.
  25. Request let url = URL(string: "https:!"httpbin.org/post")! var request = URLRequest(url:

    ) request.httpMethod = "POST" request.httpBody = "foo=bar".data(using: .utf8) struct HTTPRequest { let url: URL let method: String let parameters: [String: Any] func buildRequest() #$ URLRequest { var request = URLRequest(url: url) request.httpMethod = method request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) return request } }
  26. Request !" let url = URL(string: "https:!"httpbin.org/post")! !" var request

    = URLRequest(url: ) !" request.httpMethod = "POST" !" request.httpBody = "foo=bar".data(using: .utf8) struct HTTPRequest { let url: URL let method: String let parameters: [String: Any] func buildRequest() #$ URLRequest { var request = URLRequest(url: url) request.httpMethod = method request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) return request } }
  27. Request !" let url = URL(string: "https:!"httpbin.org/post")! !" var request

    = URLRequest(url: ) !" request.httpMethod = "POST" !" request.httpBody = "foo=bar".data(using: .utf8) struct HTTPRequest { let url: URL let method: String let parameters: [String: Any] func buildRequest() #$ URLRequest { var request = URLRequest(url: url) request.httpMethod = method request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) return request } }
  28. Response (Data?, URLResponse?, Error?)

  29. Response (Data?, URLResponse?, Error?) Let's consider HTTP response and make

    the context to API networking.
  30. Response (Data?, URLResponse?, Error?) Let's consider HTTP response and make

    the context to API networking. (Codable?, HTTPURLResponse?, Error?)
  31. Response let decoder = JSONDecoder() struct HTTPResponse<T: Codable> { let

    value: T? let response: HTTPURLResponse? let error: Error? init(data: Data?, response: URLResponse?, error: Error?) throws { self.value = try data.map { try decoder.decode(T.self, from: $0) } self.response = response as? HTTPURLResponse self.error = error } }
  32. Client struct HTTPClient { let session: URLSession func send<T: Codable>(

    _ request: HTTPRequest, handler: @escaping (HTTPResponse<T>?) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in handler(try? HTTPResponse( data: data, response: response, error: error)) } task.resume() } }
  33. Client struct HTTPClient { let session: URLSession func send<T: Codable>(

    _ request: HTTPRequest, handler: @escaping (HTTPResponse<T>?) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in handler(try? HTTPResponse( data: data, response: response, error: error)) } task.resume() } }
  34. Using the Client struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: HTTPResponse<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  35. Using the Client struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: HTTPResponse<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  36. Improvement - Response struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: HTTPResponse<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  37. Improvement - Response !" (data: Data?, response: URLResponse?, error: Error?)

    struct HTTPResponse<T: Codable> { let value: T? let response: HTTPURLResponse? let error: Error? !" #$% } func send<T: Codable>( _ request: HTTPRequest, handler: @escaping (HTTPResponse<T>?) &' Void) { !" #$% }
  38. Improvement - Response !" (data: Data?, response: URLResponse?, error: Error?)

    struct HTTPResponse<T: Codable> { let value: T? let response: HTTPURLResponse? let error: Error? !" #$% } func send<T: Codable>( _ request: HTTPRequest, handler: @escaping (HTTPResponse<T>?) &' Void) { !" #$% }
  39. Improvement - Response !" (data: Data?, response: URLResponse?, error: Error?)

    struct HTTPResponse<T: Codable> { let value: T? let response: HTTPURLResponse? let error: Error? !" #$% } func send<T: Codable>( _ request: HTTPRequest, handler: @escaping (Result<T, Error>) &' Void) { !" #$% }
  40. Improvement - Response func send<T: Codable>( _ request: HTTPRequest, handler:

    @escaping (Result<T, Error>) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in guard let data = data else { handler(.failure(error #$ ResponseError.nilData)) return } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() }
  41. Improvement - Response func send<T: Codable>( _ request: HTTPRequest, handler:

    @escaping (Result<T, Error>) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in guard let data = data else { handler(.failure(error #$ ResponseError.nilData)) return } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() }
  42. Improvement - Response struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: HTTPResponse<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  43. Improvement - Response struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: Result<HTTPBinPostResponse, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  44. Improvement - Request struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: Result<HTTPBinPostResponse, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  45. Improvement - Request protocol Request { associatedtype Response: Decodable var

    url: URL { get } var method: String { get } var parameters: [String: Any] { get } }
  46. Improvement - Request struct HTTPBinPostRequest: Request { typealias Response =

    HTTPBinPostResponse let url = URL(string: "https:!"httpbin.org/post")! let method = HTTPMethod.POST let foo: String var parameters: [String : Any] { return ["foo": foo] } }
  47. Improvement - Request let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!,

    method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: Result<HTTPBinPostResponse, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  48. Improvement - Request !" let request = HTTPRequest( !" url:

    URL(string: "https:!"httpbin.org/post")!, !" method: "POST", !" parameters: ["foo": "bar"] !" ) let request = HTTPBinPostRequest(foo: "bar") client.send(request) { (res: Result<HTTPBinPostResponse, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  49. Is it Hard?

  50. Is it Hard? No

  51. But

  52. It is not the real life

  53. NOT the Real Life

  54. Case 1 - Request request.httpBody = parameters .map { "\($0.key)=\($0.value)"

    } .joined(separator: "&") !" foo=bar&flag=1 .data(using: .utf8)
  55. Case 1 - Request request.httpBody = parameters .map { "\($0.key)=\($0.value)"

    } .joined(separator: "&") !" foo=bar&flag=1 .data(using: .utf8) JSON Body? URL query?
  56. Case 1 - Request func buildRequest() !" URLRequest { #$%&'

    if method () "GET" { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else { if headers["Content-Type"] () "application/json" { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if headers["Content-Type"] () "application/x-*+,-form-urlencoded" { #$ TODO: Read RFC 3986. How to encode something like array? request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else { #$%&' } } return request }
  57. Case 1 - Request func buildRequest() !" URLRequest { #$%&'

    if method () "GET" { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else *+ POST, PUT, DELET, etc. ,- { if headers["Content-Type"] () "application/json" { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if headers["Content-Type"] () "application/x-./0-form-urlencoded" { #$ TODO: Read RFC 3986. How to encode something like array? request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else *+ multipart/form-data, text/xml, etc ,- { #$%&' } } return request }
  58. Case 1 - Request func buildRequest() !" URLRequest { #$%&'

    if method () "GET" { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else *+ POST, PUT, DELET, etc. ,- { if headers["Content-Type"] () "application/json" { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if headers["Content-Type"] () "application/x-./0-form-urlencoded" { #$ TODO: Read RFC 3986. How to encode something like array? request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else *+ multipart/form-data, text/xml, etc ,- { #$%&' } } return request }
  59. Case 2 - Response struct HTTPBinPostResponse: Codable { struct Form:

    Codable { let foo: String } let form: Form }
  60. Case 2 - Response struct HTTPBinPostResponse: Codable { struct Form:

    Codable { let foo: String } let form: Form } !" 200 OK { "form": { "foo": "bar" } }
  61. Case 2 - Response struct HTTPBinPostResponse: Codable { struct Form:

    Codable { let foo: String } let form: Form } !" 403 Forbidden { "error": true, "code": 999, "reason": "Invalid Token." }
  62. Case 2 - Response session.dataTask(with: urlRequest) { data, response, error

    in guard let data = data else { handler(.failure(error !" ResponseError.nilData)) return } do { let value = try decoder.decode(Req.Response.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } }
  63. Case 2 - Response session.dataTask(with: urlRequest) { data, response, error

    in guard let data = data else { handler(.failure(error !" ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode #$ 300 { do { let error = try decoder.decode(APIError.self, from: data) handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } catch { handler(.failure(error)) } } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } }
  64. Case 3 - Client — Retry a request. — Refresh

    token. — Mapping data.
  65. Case 3 - Client: Retry func send<T: Codable>( _ request:

    HTTPRequest, leftRetryCount: Int = 2, handler: @escaping (Result<T, Error>) !" Void)
  66. Case 3 - Client: Retry session.dataTask(with: urlRequest) { data, response,

    error in if let error = error { handler(.failure(error)) return } guard let data = data else { handler(.failure(ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode !" 300 { if leftRetryCount > 0 { self.send(request, leftRetryCount: leftRetryCount - 1, handler: handler) } else { do { let error = try decoder.decode(APIError.self, from: data) handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } catch { handler(.failure(error)) } } } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } }
  67. Case 3 - Client: Refresh token !" If a response

    contains 403 and error code 999, !" send this to refresh token, then retry the original request. struct RefreshTokenRequest: Request { #$ %& }
  68. Case 3 - Client: Refresh token session.dataTask(with: urlRequest) { data,

    response, error in if let error = error { handler(.failure(error)) return } guard let data = data else { handler(.failure(ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode !" 300 { do { let error = try decoder.decode(APIError.self, from: data) if response.statusCode #$ 403 %& error.code #$ 999 { let freshTokenRequest = RefreshTokenRequest(refreshToken: "token123") self.send(freshTokenRequest) { result in switch result { case .success(let token): keyChain.saveToken(result) '( Send current request again. self.send(request, handler: handler) case .failure(let error): handler(.failure(ResponseError.tokenError)) } } return } else { handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } } catch { handler(.failure(error)) } } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } }
  69. Case 3 - Client: Mapping Data Response: Status Code: 200

    Content-Type: application/json Body: { }
  70. Case 3 - Client: Mapping Data Response: Status Code: 200

    Content-Type: application/json Body: <Empty>
  71. Case 3 - Client: Mapping Data session.dataTask(with: urlRequest) { data,

    response, error in if let error = error { handler(.failure(error)) return } guard let data = data else { handler(.failure(ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode !" 300 { do { let error = try decoder.decode(APIError.self, from: data) if response.statusCode #$ 403 %& error.code #$ 999 { let freshTokenRequest = RefreshTokenRequest(refreshToken: "token123") self.send(freshTokenRequest) { result in switch result { case .success(let token): keyChain.saveToken(result) '( Send current request again. self.send(request, handler: handler) case .failure(let error): handler(.failure(ResponseError.tokenError)) } } return } else { handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } } catch { handler(.failure(error)) } } do { let realData = data.isEmpty ? "{}".data(using: .utf8)! : data let value = try decoder.decode(T.self, from: realData) handler(.success(value)) } catch { handler(.failure(error)) } }
  72. Ideal World struct HTTPClient { func send<T: Codable>( _ request:

    HTTPRequest, handler: @escaping (Result<T, Error>) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in guard let data = data else { handler(.failure(error #$ ResponseError.nilData)) return } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() } }
  73. Real World protocol Request { associatedtype Response: Decodable var url:

    URL { get } var method: HTTPMethod { get } var parameters: [String: Any] { get } var contentType: ContentType { get } } extension Request { func buildRequest() !" URLRequest { var request = URLRequest(url: url) request.httpMethod = method.rawValue if method #$ .GET { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else { if contentType.rawValue.contains("application/json") { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if contentType.rawValue.contains("application/x-%&'-form-urlencoded") { request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else { ()*+, } } return request } } struct APIError: Decodable { let code: Int let reason: String } struct RefreshTokenRequest: Request { struct Response: Decodable { let token: String } let url = URL(string: "someurl")! let method: HTTPMethod = .POST let contentType: ContentType = .json var parameters: [String : Any] { return ["refreshToken": refreshToken] } let refreshToken: String } class IndicatorManager { static var currentCount = 0 static func increase() { currentCount += 1 if currentCount -. 0 { UIApplication.shared .isNetworkActivityIndicatorVisible = true } } static func decrease() { currentCount = max(0, currentCount - 1) if currentCount #$ 0 { UIApplication.shared .isNetworkActivityIndicatorVisible = false } } } struct HTTPClient { let session: URLSession init(session: URLSession) { self.session = session } func send<T: Codable>( _ request: HTTPRequest, handler: @escaping (Result<T, Error>) !" Void) { let urlRequest = request.buildRequest() IndicatorManager.increase() let task = session.dataTask(with: urlRequest) { data, response, error in DispatchQueue.main.async { IndicatorManager.decrease() } if let error = error { handler(.failure(error)) return } guard let data = data else { handler(.failure(ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode -. 300 { do { let error = try decoder.decode(APIError.self, from: data) if response.statusCode #$ 403 /0 error.code #$ 999 { let freshTokenRequest = RefreshTokenRequest(refreshToken: "token123") self.send(freshTokenRequest) { result in switch result { case .success(let token): () keyChain.saveToken(result) () Send current request again. self.send(request, handler: handler) case .failure: handler(.failure(ResponseError.tokenError)) } } return } else { handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } } catch { handler(.failure(error)) } } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() } func send<Req: Request>( _ request: Req, handler: @escaping (Result<Req.Response, Error>) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in guard let data = data else { handler(.failure(error 12 ResponseError.nilData)) return } do { let value = try decoder.decode(Req.Response.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() } }
  74. Is it Hard?

  75. Is it Hard? YES

  76. Out of Control

  77. None
  78. Responsibility

  79. Responsibility 1. Setting up the request 2. Handling the response

    (data, error)
  80. Responsibility 1. Setting up the request 2. Handling the response

    (data, error)
  81. Setting up the request — GET, POST, !"# — httpMethod

    — JSON, Form, !"# — httpBody — Token, User agent — setValue(_:forHTTPHeaderField:)
  82. Abstract Request Adapter request.xxx = yyy

  83. Abstract Request Adapter request.xxx = yyy f: (URLRequest) !" URLRequest

  84. Abstract Request Adapter protocol RequestAdapter { func adapted(_ request: URLRequest)

    throws !" URLRequest }
  85. POST /post HTTP/1.1 Content-Type: application/json Host: httpbin.org User-Agent: Networking Demo/1.0.0

    (iPhone; OS 12_2) Content-Length: 13 {"foo":"bar"}
  86. POST /post HTTP/1.1 Content-Type: application/json Host: httpbin.org User-Agent: Networking Demo/1.0.0

    (iPhone; OS 12_2) Content-Length: 13 {"foo":"bar"}
  87. Adapter for HTTP method struct AnyAdapter: RequestAdapter { let block:

    (URLRequest) throws !" URLRequest func adapted(_ request: URLRequest) throws !" URLRequest { return try block(request) } } enum HTTPMethod: String { case GET, POST var adapter: AnyAdapter { return AnyAdapter { req in var req = req req.httpMethod = self.rawValue return req } } }
  88. Adapter for HTTP method struct AnyAdapter: RequestAdapter { let block:

    (URLRequest) throws !" URLRequest func adapted(_ request: URLRequest) throws !" URLRequest { return try block(request) } } enum HTTPMethod: String { case GET, POST var adapter: AnyAdapter { return AnyAdapter { req in var req = req req.httpMethod = self.rawValue return req } } }
  89. Adapter for HTTP method extension Request { func buildRequest() !"

    URLRequest { var request = URLRequest(url: url) request.httpMethod = method.rawValue #$%&' } }
  90. Adapter for HTTP method extension Request { func buildRequest() throws

    !" URLRequest { var request = URLRequest(url: url) request = try method.adapter.adapted(request) #$%&' } }
  91. POST /post HTTP/1.1 Content-Type: application/json Host: httpbin.org User-Agent: Networking Demo/1.0.0

    (iPhone; OS 12_2) Content-Length: 13 {"foo":"bar"}
  92. None
  93. Adapter for Content enum ContentType: String { case json =

    "application/json" case urlForm = "application/x-!"#-form-urlencoded; charset=utf-8" var headerAdapter: AnyAdapter { return AnyAdapter { req in var req = req req.setValue( self.rawValue, forHTTPHeaderField: "Content-Type") return req } } }
  94. Adapter for Content enum ContentType: String { case json =

    "application/json" case urlForm = "application/x-!"#-form-urlencoded; charset=utf-8" $% &'( func dataAdapter(for parameters: [String: Any]) )* RequestAdapter { switch self { case .json: return JSONRequestDataAdapter(parameters: parameters) case .urlForm: return URLFormRequestDataAdapter(parameters: parameters) } } }
  95. Adapter for Content extension Request { func buildRequest() throws !"

    URLRequest { #$ %&' if method () .GET { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else { if contentType.rawValue.contains("application/json") { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if contentType.rawValue.contains("application/x-*+,-form-urlencoded") { request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else { #$%&' } } } }
  96. Adapter for Content extension Request { func buildRequest() throws !"

    URLRequest { #$ %&' request = try RequestContentAdapter( method: method, contentType: contentType, content: parameters) .adapted(request) } }
  97. Adapters extension Request { func buildRequest() throws !" URLRequest {

    var request = URLRequest(url: url) request = try method.adapter .adapted(request) request = try RequestContentAdapter( method: method, contentType: contentType, content: parameters) .adapted(request) #$ More adapters%&' return request } }
  98. Adapters in protocol protocol Request { associatedtype Response: Decodable var

    url: URL { get } var method: HTTPMethod { get } var parameters: [String: Any] { get } var contentType: ContentType { get } var adapters: [RequestAdapter] { get } }
  99. Adapters in protocol extension Request { var adapters: [RequestAdapter] {

    return [ method.adapter, RequestContentAdapter( method: method, contentType: contentType, content: parameters) !" More adapters#$% ] } func buildRequest() throws &' URLRequest { let request = URLRequest(url: url) return try adapters.reduce(request) { try $1.adapted($0) } } }
  100. Adapters in protocol extension Request { var adapters: [RequestAdapter] {

    return [ method.adapter, RequestContentAdapter( method: method, contentType: contentType, content: parameters) !" More adapters#$% ] } func buildRequest() throws &' URLRequest { let request = URLRequest(url: url) return try adapters.reduce(request) { try $1.adapted($0) } } }
  101. Responsibility 1. Setting up the request 2. Handling the response

    (data, error)
  102. Response Decision When we receive a response:

  103. None
  104. Abstract Response Decision Treat every step a decision, it decides

    what to do next.
  105. Abstract Response Decision We can: — continue next decision with

    mapped data/ response. — restart the current request with some decisions. — errored an error happens and request fails. — done expected value is parsed and request is done.
  106. Decision Action enum DecisionAction<Req: Request> { case continueWith(Data, HTTPURLResponse) case

    restartWith([Decision]) case errored(Error) case done(Req.Response) }
  107. Decision Protocol protocol Decision { func shouldApply<Req: Request>( request: Req,

    data: Data, response: HTTPURLResponse) !" Bool func apply<Req: Request>( request: Req, data: Data, response: HTTPURLResponse, done closure: @escaping (DecisionAction<Req>) !" Void) }
  108. Parsing Decision

  109. Parsing Decision struct ParseResultDecision: Decision { func shouldApply<Req: Request>( request:

    Req, data: Data, response: HTTPURLResponse) !" Bool { return true } func apply<Req: Request>( request: Req, data: Data, response: HTTPURLResponse, done closure: @escaping (DecisionAction<Req>) !" Void) { do { let value = try decoder.decode(Req.Response.self, from: data) closure(.done(value)) } catch { closure(.errored(error)) } } }
  110. Parsing Decision struct ParseResultDecision: Decision { func shouldApply<Req: Request>( request:

    Req, data: Data, response: HTTPURLResponse) !" Bool { return true } func apply<Req: Request>( request: Req, data: Data, response: HTTPURLResponse, done closure: @escaping (DecisionAction<Req>) !" Void) { do { let value = try decoder.decode(Req.Response.self, from: data) closure(.done(value)) } catch { closure(.errored(error)) } } }
  111. More examples struct BadResponseStatusCodeDecision: Decision { func shouldApply !" #$

    { return !(200%&'300).contains(response.statusCode) } func apply !" #$ { do { let value = try decoder.decode(APIError.self, from: data) let error = ResponseError.apiError( error: value, statusCode: response.statusCode) closure(.errored(error)) } catch { closure(.errored(error)) } } }
  112. More examples struct BadResponseStatusCodeDecision: Decision { func shouldApply !" #$

    { return !(200%&'300).contains(response.statusCode) } func apply !" #$ { do { let value = try decoder.decode(APIError.self, from: data) let error = ResponseError.apiError( error: value, statusCode: response.statusCode) closure(.errored(error)) } catch { closure(.errored(error)) } } }
  113. More examples struct DataMappingDecision: Decision { let condition: (Data) !"

    Bool let transform: (Data) !" Data func shouldApply #$ %& { return condition(data) } func apply #$ %& { closure(.continueWith(transform(data), response)) } }
  114. More examples DataMappingDecision(condition: { $0.isEmpty }) { _ in return

    "{}".data(using: .utf8)! } <empty> !" "{}"
  115. More examples #if SERVER_NOT_READY DataMappingDecision(condition: { _ in return true

    }) { _ in return #"{"dummy": "data"}"# } #end
  116. Decisions in protocol protocol Request { associatedtype Response: Decodable var

    url: URL { get } var method: HTTPMethod { get } var parameters: [String: Any] { get } var contentType: ContentType { get } var adapters: [RequestAdapter] { get } var decisions: [Decision] { get } }
  117. Use Decisions extension Request { !" #$% var decisions: [Decision]

    { return [ RefreshTokenDecision(), RetryDecision(leftCount: 2), BadResponseStatusCodeDecision(), DataMappingDecision(condition: { $0.isEmpty }) { _ in return "{}".data(using: .utf8)! }, ParseResultDecision() ] } }
  118. Handle Decision Actions if currentDecision.shouldApply(!" #$) { currentDecision.apply(!" #$) {

    action in switch action { case .continueWith(let data, let response): self.handleDecision(!" #$) case .restartWith(let decisions): self.send(!" #$) case .errored(let error): handler(.failure(error)) case .done(let value): handler(.success(value)) } } } else { handleDecision(!" #$) }
  119. Handle Decision Actions if currentDecision.shouldApply(!" #$) { currentDecision.apply(!" #$) {

    action in switch action { case .continueWith(let data, let response): self.handleDecision(!" #$) case .restartWith(let decisions): self.send(!" #$) case .errored(let error): handler(.failure(error)) case .done(let value): handler(.success(value)) } } } else { handleDecision(!" #$) %& Next decision }
  120. Handle Decision Actions if currentDecision.shouldApply(!" #$) { currentDecision.apply(!" #$) {

    action in switch action { case .continueWith(let data, let response): self.handleDecision(!" #$) %& Next decision case .restartWith(let decisions): self.send(!" #$) %& Restart request case .errored(let error): handler(.failure(error)) %& Finish with error case .done(let value): handler(.success(value)) %& Finish with value } } } else { handleDecision(!" #$) }
  121. Finally struct HTTPBinPostRequest: Request { struct Response: Codable { struct

    Form: Codable { let foo: String } let form: Form } let url = URL(string: "https:!"httpbin.org/post")! let method = HTTPMethod.POST let contentType = ContentType.urlForm var parameters: [String : Any] { return ["foo": foo] } let foo: String }
  122. Finally let request = HTTPBinPostRequest(foo: "bar") client.send(request) { res in

    switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  123. Driven by Request extension Request { var adapters: [RequestAdapter] {

    return [ method.adapter, RequestContentAdapter( method: method, contentType: contentType, content: parameters) ] } var decisions: [Decision] { return [ RefreshTokenDecision(), RetryDecision(leftCount: 2), BadResponseStatusCodeDecision(), DataMappingDecision(condition: { $0.isEmpty }) { _ in return "{}".data(using: .utf8)! }, ParseResultDecision() ] } }
  124. None
  125. Pros Small components Clearness Pure function Testability Protocol extension Flexibility

    More decision types Extensibility Control each request Operability
  126. Is it Hard?

  127. Networking should not be hard

  128. Not be hard in Demo4 4 https:"#github.com/onevcat/ComponentNetworking

  129. Even in real life

  130. real life

  131. real life LINE SDK Networking 5 5 https:"#github.com/line/line-sdk-ios-swift/tree/master/LineSDK/ LineSDK/Networking

  132. None
  133. Thank you for coming today onevcat@LINE, 2019.04.24, Kyoto