Slide 1

Slide 1 text

Networking shouldn't be so hard LINE Developer Meetup #53 Kyoto | 2019.4.24 By @onevcat

Slide 2

Slide 2 text

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 [email protected] 2 https:"#github.com/onevcat 1 https:"#onevcat.com

Slide 3

Slide 3 text

Agenda — Networking is not hard. — Networking is hard. — Networking should not be hard.

Slide 4

Slide 4 text

A story of myself

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

My toolkit (2010, iOS 4) — NSURLConnection — stig/json-framework (SBJson) — UITableView

Slide 8

Slide 8 text

Is it Hard?

Slide 9

Slide 9 text

Is it Hard? No

Slide 10

Slide 10 text

Very soon later I found something wrong...

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

With help of a networking wrapper/ framework — NSURLSession — AFNetworking — Alamofire — !"#

Slide 13

Slide 13 text

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()

Slide 14

Slide 14 text

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()

Slide 15

Slide 15 text

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()

Slide 16

Slide 16 text

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()

Slide 17

Slide 17 text

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()

Slide 18

Slide 18 text

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()

Slide 19

Slide 19 text

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()

Slide 20

Slide 20 text

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()

Slide 21

Slide 21 text

Is it Hard?

Slide 22

Slide 22 text

Is it Hard? No

Slide 23

Slide 23 text

Creating a Client To make things easier! — Request — Response

Slide 24

Slide 24 text

What is a Client — Create and setup Requests. — Send the request with a session. — Handle/process the Responses.

Slide 25

Slide 25 text

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 } }

Slide 26

Slide 26 text

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 } }

Slide 27

Slide 27 text

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 } }

Slide 28

Slide 28 text

Response (Data?, URLResponse?, Error?)

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Response let decoder = JSONDecoder() struct HTTPResponse { 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 } }

Slide 32

Slide 32 text

Client struct HTTPClient { let session: URLSession func send( _ request: HTTPRequest, handler: @escaping (HTTPResponse?) !" 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() } }

Slide 33

Slide 33 text

Client struct HTTPClient { let session: URLSession func send( _ request: HTTPRequest, handler: @escaping (HTTPResponse?) !" 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() } }

Slide 34

Slide 34 text

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?) in print(res#$value#$form.foo %& "") !" "bar" }

Slide 35

Slide 35 text

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?) in print(res#$value#$form.foo %& "") !" "bar" }

Slide 36

Slide 36 text

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?) in print(res#$value#$form.foo %& "") !" "bar" }

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Improvement - Response func send( _ request: HTTPRequest, handler: @escaping (Result) !" 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() }

Slide 41

Slide 41 text

Improvement - Response func send( _ request: HTTPRequest, handler: @escaping (Result) !" 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() }

Slide 42

Slide 42 text

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?) in print(res#$value#$form.foo %& "") !" "bar" }

Slide 43

Slide 43 text

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) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }

Slide 44

Slide 44 text

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) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }

Slide 45

Slide 45 text

Improvement - Request protocol Request { associatedtype Response: Decodable var url: URL { get } var method: String { get } var parameters: [String: Any] { get } }

Slide 46

Slide 46 text

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] } }

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }

Slide 49

Slide 49 text

Is it Hard?

Slide 50

Slide 50 text

Is it Hard? No

Slide 51

Slide 51 text

But

Slide 52

Slide 52 text

It is not the real life

Slide 53

Slide 53 text

NOT the Real Life

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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 }

Slide 57

Slide 57 text

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 }

Slide 58

Slide 58 text

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 }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Case 2 - Response struct HTTPBinPostResponse: Codable { struct Form: Codable { let foo: String } let form: Form } !" 403 Forbidden { "error": true, "code": 999, "reason": "Invalid Token." }

Slide 62

Slide 62 text

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)) } }

Slide 63

Slide 63 text

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)) } }

Slide 64

Slide 64 text

Case 3 - Client — Retry a request. — Refresh token. — Mapping data.

Slide 65

Slide 65 text

Case 3 - Client: Retry func send( _ request: HTTPRequest, leftRetryCount: Int = 2, handler: @escaping (Result) !" Void)

Slide 66

Slide 66 text

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)) } }

Slide 67

Slide 67 text

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 { #$ %& }

Slide 68

Slide 68 text

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)) } }

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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)) } }

Slide 72

Slide 72 text

Ideal World struct HTTPClient { func send( _ request: HTTPRequest, handler: @escaping (Result) !" 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() } }

Slide 73

Slide 73 text

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( _ request: HTTPRequest, handler: @escaping (Result) !" 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( _ request: Req, handler: @escaping (Result) !" 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() } }

Slide 74

Slide 74 text

Is it Hard?

Slide 75

Slide 75 text

Is it Hard? YES

Slide 76

Slide 76 text

Out of Control

Slide 77

Slide 77 text

No content

Slide 78

Slide 78 text

Responsibility

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

Setting up the request — GET, POST, !"# — httpMethod — JSON, Form, !"# — httpBody — Token, User agent — setValue(_:forHTTPHeaderField:)

Slide 82

Slide 82 text

Abstract Request Adapter request.xxx = yyy

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

Abstract Request Adapter protocol RequestAdapter { func adapted(_ request: URLRequest) throws !" URLRequest }

Slide 85

Slide 85 text

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"}

Slide 86

Slide 86 text

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"}

Slide 87

Slide 87 text

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 } } }

Slide 88

Slide 88 text

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 } } }

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

Adapter for HTTP method extension Request { func buildRequest() throws !" URLRequest { var request = URLRequest(url: url) request = try method.adapter.adapted(request) #$%&' } }

Slide 91

Slide 91 text

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"}

Slide 92

Slide 92 text

No content

Slide 93

Slide 93 text

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 } } }

Slide 94

Slide 94 text

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) } } }

Slide 95

Slide 95 text

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 { #$%&' } } } }

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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 } }

Slide 98

Slide 98 text

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 } }

Slide 99

Slide 99 text

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) } } }

Slide 100

Slide 100 text

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) } } }

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

Response Decision When we receive a response:

Slide 103

Slide 103 text

No content

Slide 104

Slide 104 text

Abstract Response Decision Treat every step a decision, it decides what to do next.

Slide 105

Slide 105 text

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.

Slide 106

Slide 106 text

Decision Action enum DecisionAction { case continueWith(Data, HTTPURLResponse) case restartWith([Decision]) case errored(Error) case done(Req.Response) }

Slide 107

Slide 107 text

Decision Protocol protocol Decision { func shouldApply( request: Req, data: Data, response: HTTPURLResponse) !" Bool func apply( request: Req, data: Data, response: HTTPURLResponse, done closure: @escaping (DecisionAction) !" Void) }

Slide 108

Slide 108 text

Parsing Decision

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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)) } } }

Slide 112

Slide 112 text

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)) } } }

Slide 113

Slide 113 text

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)) } }

Slide 114

Slide 114 text

More examples DataMappingDecision(condition: { $0.isEmpty }) { _ in return "{}".data(using: .utf8)! } !" "{}"

Slide 115

Slide 115 text

More examples #if SERVER_NOT_READY DataMappingDecision(condition: { _ in return true }) { _ in return #"{"dummy": "data"}"# } #end

Slide 116

Slide 116 text

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 } }

Slide 117

Slide 117 text

Use Decisions extension Request { !" #$% var decisions: [Decision] { return [ RefreshTokenDecision(), RetryDecision(leftCount: 2), BadResponseStatusCodeDecision(), DataMappingDecision(condition: { $0.isEmpty }) { _ in return "{}".data(using: .utf8)! }, ParseResultDecision() ] } }

Slide 118

Slide 118 text

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(!" #$) }

Slide 119

Slide 119 text

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 }

Slide 120

Slide 120 text

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(!" #$) }

Slide 121

Slide 121 text

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 }

Slide 122

Slide 122 text

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) } }

Slide 123

Slide 123 text

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() ] } }

Slide 124

Slide 124 text

No content

Slide 125

Slide 125 text

Pros Small components Clearness Pure function Testability Protocol extension Flexibility More decision types Extensibility Control each request Operability

Slide 126

Slide 126 text

Is it Hard?

Slide 127

Slide 127 text

Networking should not be hard

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

Even in real life

Slide 130

Slide 130 text

real life

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

No content

Slide 133

Slide 133 text

Thank you for coming today onevcat@LINE, 2019.04.24, Kyoto