Slide 1

Slide 1 text

姜᪠ԏ櫞 櫞ෝӤᶆॠ iPlayground 2019 @onevcat | 2019.9.22

Slide 2

Slide 2 text

橕ෝ౯ ሴ ૛ (@onevcat) iOS - 2010 LINE SDK, LINE Live Kingfisher, APNGKit, FengNiao etc.

Slide 3

Slide 3 text

扖氂 — 姜᪠ӧ櫞 — 姜᪠உ櫞 — 姜᪠ӧ䛑扗春䰬櫞

Slide 4

Slide 4 text

౯ጱඳԪ

Slide 5

Slide 5 text

้䓅֜䦒 吚౯㴄樄ত iOS 樄咳...

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

ૡٍ (2010, iOS 4) — NSURLConnection — stig/json-framework (SBJson) — UITableView

Slide 8

Slide 8 text

ૡٍ (2010, iOS 4) — ䷱ํ NSURLSession — ䷱ํ Codable ҅Ԟ䷱ํ NSJSONSerialization — ( 㫋ݘӧ捧ጱݝํ UITableView)

Slide 9

Slide 9 text

உ櫞㻟?

Slide 10

Slide 10 text

உ櫞㻟? No

Slide 11

Slide 11 text

֕ฎ҅உள ౯੊憽ํ焧ӧ䌘㵇...

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

姜᪠໛ຝ/੗愇 — AFNetworking — Alamofire — Moya — !"#

Slide 14

Slide 14 text

؉Ӟ㮆ᛔ૩ጱ姜᪠໛ຝމѺ Why? Because I can.

Slide 15

Slide 15 text

؉Ӟ㮆ᛔ૩ጱ姜᪠໛ຝމѺ Why? Learn from scratch.

Slide 16

Slide 16 text

ض፡፡姜᪠抬࿢Ҙ 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

Ӟ樄ত҅உ墋㻌 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 18

Slide 18 text

Ӟ樄ত҅உ墋㻌 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 19

Slide 19 text

Ӟ樄ত҅உ墋㻌 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 20

Slide 20 text

Ӟ樄ত҅உ墋㻌 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 21

Slide 21 text

Post 抬࿢ !" 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 22

Slide 22 text

Post 抬࿢ !" 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 23

Slide 23 text

Post 抬࿢ !" 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 24

Slide 24 text

உ櫞㻟?

Slide 25

Slide 25 text

உ櫞㻟? No

Slide 26

Slide 26 text

㴕ୌ HTTP Client ๜㬵੪ӧ櫞҅ݢզๅ墋㻌Ѻ — 抬࿢ Request — ࢧ䛑 Response

Slide 27

Slide 27 text

HTTP Client ฎՋ焒 — ୌᒈ޾ᯈᗝ抬࿢ — ᭐晃 session 咳ᭆ抬࿢ — ݐ஑޾归ቘࢧ䛑

Slide 28

Slide 28 text

抬࿢ 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 29

Slide 29 text

抬࿢ !" 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 30

Slide 30 text

抬࿢ !" 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 31

Slide 31 text

ࢧ䛑 (Data?, URLResponse?, Error?)

Slide 32

Slide 32 text

ࢧ䛑 (Data?, URLResponse?, Error?) Tuple অ服Ѻӧࡅ䴽Ѻ

Slide 33

Slide 33 text

ࢧ䛑 (Data?, URLResponse?, Error?) ᴴਧ Data !" Codable ጱ扖 HTTPResponse

Slide 34

Slide 34 text

ࢧ䛑 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 35

Slide 35 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 36

Slide 36 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 37

Slide 37 text

ֵአ Client struct HTTPBinPostResult: 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 38

Slide 38 text

ֵአ Client struct HTTPBinPostResult: 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 39

Slide 39 text

౯㮉؉ԧՋ焒 — HTTPRequest 䌔 url ҅ method ҅ body ੗愇 — Client ളݑ HTTPRequest ҅旉䟵 URLRequest ҅妔ࢧ Data — HTTPResponse 䌔 (Data?, URLResponse?, Error?) 旉捧凚 T: Codable

Slide 40

Slide 40 text

දᜉ - Response struct HTTPBinPostResult: 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 %& "") !" 㺔氂 1: অग़ ??? !" 㺔氂 2: HTTPBinPostResult 娒捌࢏כ挨Ҙ }

Slide 41

Slide 41 text

දᜉ - 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 42

Slide 42 text

දᜉ - 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 43

Slide 43 text

දᜉ - 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 44

Slide 44 text

දᜉ - 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 45

Slide 45 text

දᜉ - 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 46

Slide 46 text

දᜉ - Response struct HTTPBinPostResult: 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 47

Slide 47 text

දᜉ - Response struct HTTPBinPostResult: 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 48

Slide 48 text

දᜉ - Request struct HTTPBinPostResult: 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 49

Slide 49 text

දᜉ - Request protocol Request { associatedtype Response: Decodable var url: URL { get } var method: String { get } var parameters: [String: Any] { get } } extension Request { func buildRequest() !" URLRequest }

Slide 50

Slide 50 text

දᜉ - Request struct HTTPBinPostRequest: Request { typealias Response = HTTPBinPostResult let url = URL(string: "https:!"httpbin.org/post")! let method = HTTPMethod.POST let foo: String var parameters: [String : Any] { return ["foo": foo] } }

Slide 51

Slide 51 text

දᜉ - 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 52

Slide 52 text

දᜉ - 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 53

Slide 53 text

౯㮉؉ԧՋ焒 — Request protocol ು᨝ — Response Type አ associatedtype ޾ Request 昧ള᩸㬵 — አ Result 墋۸ Response

Slide 54

Slide 54 text

౯㮉஑کՋ焒 !" ਠᗦጱ抬࿢ොୗ 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 55

Slide 55 text

உ櫞㻟?

Slide 56

Slide 56 text

உ櫞㻟? No

Slide 57

Slide 57 text

֕ฎ

Slide 58

Slide 58 text

ቘమฎᗦঅጱ 匍䋿ฎ䵝ᯡጱ

Slide 59

Slide 59 text

匍䋿ฎ 䵝ᯡጱ

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Case 1 - Request request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") !" foo=bar&flag=1 .data(using: .utf8) !" JSON Body? {"foo": "bar", "flag" = 1} !" Query in URL? not HTTP body url = "https:!"example.com?foo=bar&flag=1"

Slide 62

Slide 62 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 63

Slide 63 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 64

Slide 64 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 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 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 69

Slide 69 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 70

Slide 70 text

Case 3 - Client — ᯿手抬࿢ — ๅෛ token — 揾ා旉䟵

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Case 3 - Client: ᯿手 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 73

Slide 73 text

Case 3 - Client: ๅෛ token !" ইຎ制䙪嘨ฎ 403 Ӭ error code ฎ 999, !" ڬෛ token҅ԏ஍᯿ෛ抬࿢ܻ API struct RefreshTokenRequest: Request { #$ %& }

Slide 74

Slide 74 text

Case 3 - Client: ๅෛ 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 75

Slide 75 text

Case 3 - Client: 揾ා旉䟵 Response: Status Code: 200 Content-Type: application/json Body: { }

Slide 76

Slide 76 text

Case 3 - Client: 揾ා旉䟵 Response: Status Code: 200 Content-Type: application/json Body:

Slide 77

Slide 77 text

Case 3 - Client: 揾ා旉䟵 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 78

Slide 78 text

ᗦঅጱቘమӮኴ 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 79

Slide 79 text

䵝ᯡጱ匍䋿Ӯኴ 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 80

Slide 80 text

உ櫞㻟?

Slide 81

Slide 81 text

உ櫞㻟? Yes

Slide 82

Slide 82 text

֦ݞ㾈፳ᥝ䌃ڊط斝冘凉 ک毣㬵㶴ݝ䵍ෝฦ᭗ଘٿ !"#$

Slide 83

Slide 83 text

SRP 㻌Ӟ实揣ܻ㳷 Single Responsibility Principle

Slide 84

Slide 84 text

ӧᥝقಒکӞ㮆ࣈො ֦ጱդ嘨䓚҅ӧฎ࣯࣍䁰

Slide 85

Slide 85 text

ӧᥝقಒکӞ㮆ࣈො ֦ጱդ嘨䓚҅ӧฎ࣯࣍䁰 ᘒӬ࣯࣍᮷ᵱᥝړ气҅ դ嘨...

Slide 86

Slide 86 text

Client ጱ实揣 1. ᯈᗝ抬࿢ 2. 归ቘࢧ䛑

Slide 87

Slide 87 text

Client ጱ实揣 1. ᯈᗝ抬࿢ 2. 归ቘࢧ䛑

Slide 88

Slide 88 text

ᯈᗝ抬࿢ — GET, POST, !"# — httpMethod — JSON, Form, !"# — httpBody — Token, User agent — setValue(_:forHTTPHeaderField:)

Slide 89

Slide 89 text

ು᨝ғRequest Adapter request.xxx = yyy

Slide 90

Slide 90 text

ು᨝ғRequest Adapter request.xxx = yyy f: (URLRequest) !" URLRequest

Slide 91

Slide 91 text

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

Slide 92

Slide 92 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 93

Slide 93 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 94

Slide 94 text

HTTP Method Adapter 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 95

Slide 95 text

HTTP Method Adapter 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 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 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 99

Slide 99 text

No content

Slide 100

Slide 100 text

Content Adapter enum ContentType: String { case json = "application/json" case urlForm = "application/x-!"#-form-urlencoded" var headerAdapter: AnyAdapter { return AnyAdapter { req in $% &'( } } }

Slide 101

Slide 101 text

Content Adapter enum ContentType: String { case json = "application/json" case urlForm = "application/x-!"#-form-urlencoded" $% &'( func dataAdapter( for parameters: [String: Any] ) )* RequestAdapter { switch self { case .json: return JSONRequestDataAdapter(parameters: parameters) case .urlForm: return URLFormRequestDataAdapter(parameters: parameters) } } }

Slide 102

Slide 102 text

Content Adapter 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 103

Slide 103 text

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

Slide 104

Slide 104 text

ग़㮆 Adapter 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 105

Slide 105 text

Request protocol Ӿጱ Adapter 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 106

Slide 106 text

Request protocol Ӿጱ Adapter 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 107

Slide 107 text

Request protocol Ӿጱ Adapter 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 108

Slide 108 text

౯㮉؉ԧՋ焒 — 㴕ୌ RequestAdapter protocol ( ਧ嬝᭗አጱ 䯤ୌ URLRequest ጱොୗ ) — ֵ HTTPMethod ޾ ContentType ඪൔ RequestAdapter — 凚 Request ᯈᗝ [RequestAdapter]

Slide 109

Slide 109 text

Client ጱ实揣 1. ᯈᗝ抬࿢ 2. 归ቘࢧ䛑

Slide 110

Slide 110 text

Response Decision 吚තکࢧ䛑䦒҅౯㮉ᚆ؉Ջ焒ғ

Slide 111

Slide 111 text

No content

Slide 112

Slide 112 text

ು᨝ғResponse Decision ᭐晃䷥ᒽ (decision)҅䷥ਧ䤖䢡ӥӞྍጱᩳݻ

Slide 113

Slide 113 text

ು᨝ғResponse Decision ౯㮉ํইӥ䷥ᒽғ — continue 媣媲䁆ᤈӥӞ㮆䷥ᒽ — restart ᯿ෛ昲ᤈ抬࿢ — errored 䝦ڊ梊抅ᤒ晄०䤂 — done ྋଉਠ౮抬࿢

Slide 114

Slide 114 text

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

Slide 115

Slide 115 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 116

Slide 116 text

Parsing Decision

Slide 117

Slide 117 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 118

Slide 118 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 119

Slide 119 text

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

Slide 120

Slide 120 text

ๅग़ໜֺ 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 121

Slide 121 text

ๅग़ໜֺ struct DataMappingDecision: Decision { let condition: (Data) !" Bool let transform: (Data) !" Data func shouldApply #$ %& { return condition(data) } func apply #$ %& { closure(.continueWith(transform(data), response)) } }

Slide 122

Slide 122 text

ๅग़ໜֺ DataMappingDecision( condition: { $0.isEmpty }, transform: { _ in "{}".data(using: .utf8)! } ) !" "{}"

Slide 123

Slide 123 text

ๅग़ໜֺ #if SERVER_NOT_READY DataMappingDecision( condition: { _ in true }, transform: { _ in #"{"dummy": "data"}"# } ) #end

Slide 124

Slide 124 text

Request protocol Ӿጱ Decision 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 125

Slide 125 text

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

Slide 126

Slide 126 text

归ቘ 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 127

Slide 127 text

归ቘ 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 128

Slide 128 text

౯㮉؉ԧՋ焒 — 㴕ୌ Decision protocol (ਧ嬝归ቘ Response ጱොୗ) — Ⴒےݱ圵 Decision 䋿֢ — 凚 Request ᯈᗝ [Decision]

Slide 129

Slide 129 text

๋஍... 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 130

Slide 130 text

๋஍... !" क़ࣁ䷱ํ捧҅㲌ࣁय़ӧݶ 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 131

Slide 131 text

դ嘨㲌ࣁጱ奈侜҅ ᚆ捰ኞၚ꧌佝ኝᢸ޾ࡅ䗫

Slide 132

Slide 132 text

᭐晃 protocol ཛྷ奲۸晁ᤈ 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 133

Slide 133 text

㱢㵟 زկ۸ Ⴔศ 奈ڍ䤖 ݢ介手 चෝ protocol 欥ၚ ๅग़䷥ᒽ ݢ䢷઀ 奞ᔉଶഴګ ݢ඙֢

Slide 134

Slide 134 text

No content

Slide 135

Slide 135 text

揾Ⴎૡᑕ䒍੪ฎ 春焒䰼䋿僻嶆Ӭນᆳ —— Nelson

Slide 136

Slide 136 text

ইຎ֦Ջ焒᮷䷱室ک...

Slide 137

Slide 137 text

ইຎ֦Ջ焒᮷䷱室ک... 1. դ嘨ړ气҅ℂ౯؉᩸Ѻ

Slide 138

Slide 138 text

ইຎ֦Ջ焒᮷䷱室ک... 1. դ嘨ړ气҅ℂ౯؉᩸Ѻ 2. 奲ݳ>媣ಥ҅ൈᬿ>೰ե

Slide 139

Slide 139 text

ইຎ֦Ջ焒᮷䷱室ک... 1. դ嘨ړ气҅ℂ౯؉᩸Ѻ 2. 奲ݳ>媣ಥ҅ൈᬿ>೰ե 3. 僻ℂӥಋ䦒҅ض௏ᘍ޾ು᨝

Slide 140

Slide 140 text

ইຎ֦Ջ焒᮷䷱室ک... 1. դ嘨ړ气҅ℂ౯؉᩸Ѻ 2. 奲ݳ>媣ಥ҅ൈᬿ>೰ե 3. 僻ℂӥಋ䦒҅ض௏ᘍ޾ು᨝ 4. ӧ䥁᯿䯤҅כ೮ၚێ

Slide 141

Slide 141 text

உ櫞㻟?

Slide 142

Slide 142 text

姜᪠娒ᑕ ӧஉ櫞

Slide 143

Slide 143 text

Demo 4 ӧஉ櫞 4 https:"#github.com/onevcat/ComponentNetworking

Slide 144

Slide 144 text

䋿֢ Ԟӧ櫞

Slide 145

Slide 145 text

䋿֢ LINE SDK Networking 5 ҁ㰣 Ԃ ૡࠟ䦒樌҂ 5 https:"#github.com/line/line-sdk-ios-swift/tree/master/LineSDK/ LineSDK/Networking

Slide 146

Slide 146 text

No content

Slide 147

Slide 147 text

ఽ拽 ᘰ室 (Ask Speaker ൉׀ Kingfisher 揳℅) @onevcat | 2019.9.22 Taipei