網路之難,難於上青天 - iPlayground 2019

318643095c83b914cf80a7f99f247fe6?s=47 Wei Wang
September 22, 2019

網路之難,難於上青天 - iPlayground 2019

318643095c83b914cf80a7f99f247fe6?s=128

Wei Wang

September 22, 2019
Tweet

Transcript

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

  2. 橕ෝ౯ ሴ ૛ (@onevcat) iOS - 2010 LINE SDK, LINE

    Live Kingfisher, APNGKit, FengNiao etc.
  3. 扖氂 — 姜᪠ӧ櫞 — 姜᪠உ櫞 — 姜᪠ӧ䛑扗春䰬櫞

  4. ౯ጱඳԪ

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

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

    UITableView
  8. ૡٍ (2010, iOS 4) — ䷱ํ NSURLSession — ䷱ํ Codable

    ҅Ԟ䷱ํ NSJSONSerialization — ( 㫋ݘӧ捧ጱݝํ UITableView)
  9. உ櫞㻟?

  10. உ櫞㻟? No

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

  12. None
  13. 姜᪠໛ຝ/੗愇 — AFNetworking — Alamofire — Moya — !"#

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

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

  16. ض፡፡姜᪠抬࿢Ҙ 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. Ӟ樄ত҅உ墋㻌 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()
  18. Ӟ樄ত҅உ墋㻌 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()
  19. Ӟ樄ত҅உ墋㻌 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()
  20. Ӟ樄ত҅உ墋㻌 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()
  21. 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()
  22. 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()
  23. 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()
  24. உ櫞㻟?

  25. உ櫞㻟? No

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

  27. HTTP Client ฎՋ焒 — ୌᒈ޾ᯈᗝ抬࿢ — ᭐晃 session 咳ᭆ抬࿢ —

    ݐ஑޾归ቘࢧ䛑
  28. 抬࿢ 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 } }
  29. 抬࿢ !" 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 } }
  30. 抬࿢ !" 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 } }
  31. ࢧ䛑 (Data?, URLResponse?, Error?)

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

  33. ࢧ䛑 (Data?, URLResponse?, Error?) ᴴਧ Data !" Codable ጱ扖 HTTPResponse<T:

    Codable>
  34. ࢧ䛑 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 } }
  35. 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() } }
  36. 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() } }
  37. ֵአ 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<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  38. ֵአ 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<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  39. ౯㮉؉ԧՋ焒 — HTTPRequest 䌔 url ҅ method ҅ body ੗愇

    — Client ളݑ HTTPRequest ҅旉䟵 URLRequest ҅妔ࢧ Data — HTTPResponse 䌔 (Data?, URLResponse?, Error?) 旉捧凚 T: Codable
  40. දᜉ - 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<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" 㺔氂 1: অग़ ??? !" 㺔氂 2: HTTPBinPostResult 娒捌࢏כ挨Ҙ }
  41. දᜉ - 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) { !" #$% }
  42. දᜉ - 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) { !" #$% }
  43. දᜉ - 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) { !" #$% }
  44. දᜉ - 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() }
  45. දᜉ - 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() }
  46. දᜉ - 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<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  47. දᜉ - 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<HTTPBinPostResult, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  48. දᜉ - 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<HTTPBinPostResult, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  49. දᜉ - Request protocol Request { associatedtype Response: Decodable var

    url: URL { get } var method: String { get } var parameters: [String: Any] { get } } extension Request { func buildRequest() !" URLRequest }
  50. දᜉ - 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] } }
  51. දᜉ - Request let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!,

    method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: Result<HTTPBinPostResult, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  52. දᜉ - 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<HTTPBinPostResult, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  53. ౯㮉؉ԧՋ焒 — Request protocol ು᨝ — Response Type አ associatedtype

    ޾ Request 昧ള᩸㬵 — አ Result<T, Error> 墋۸ Response
  54. ౯㮉஑کՋ焒 !" ਠᗦጱ抬࿢ොୗ 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) } }
  55. உ櫞㻟?

  56. உ櫞㻟? No

  57. ֕ฎ

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

  59. 匍䋿ฎ 䵝ᯡጱ

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

    } .joined(separator: "&") !" foo=bar&flag=1 .data(using: .utf8)
  61. 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"
  62. 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 }
  63. 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 }
  64. 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 }
  65. Case 2 - Response struct HTTPBinPostResult: Codable { struct Form:

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

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

    Codable { let foo: String } let form: Form } !" 403 Forbidden { "error": true, "code": 403, "reason": "Invalid Token." }
  68. 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)) } }
  69. 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)) } }
  70. Case 3 - Client — ᯿手抬࿢ — ๅෛ token —

    揾ා旉䟵
  71. Case 3 - Client: ᯿手 func send<T: Codable>( _ request:

    HTTPRequest, leftRetryCount: Int = 2, handler: @escaping (Result<T, Error>) !" Void)
  72. 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)) } }
  73. Case 3 - Client: ๅෛ token !" ইຎ制䙪嘨ฎ 403 Ӭ

    error code ฎ 999, !" ڬෛ token҅ԏ஍᯿ෛ抬࿢ܻ API struct RefreshTokenRequest: Request { #$ %& }
  74. 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)) } }
  75. Case 3 - Client: 揾ා旉䟵 Response: Status Code: 200 Content-Type:

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

    application/json Body: <Empty>
  77. 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)) } }
  78. ᗦঅጱቘమӮኴ 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() } }
  79. 䵝ᯡጱ匍䋿Ӯኴ 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() } }
  80. உ櫞㻟?

  81. உ櫞㻟? Yes

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

  83. SRP 㻌Ӟ实揣ܻ㳷 Single Responsibility Principle

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

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

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

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

  88. ᯈᗝ抬࿢ — GET, POST, !"# — httpMethod — JSON, Form,

    !"# — httpBody — Token, User agent — setValue(_:forHTTPHeaderField:)
  89. ು᨝ғRequest Adapter request.xxx = yyy

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

  91. ು᨝ғRequest Adapter protocol RequestAdapter { func adapted(_ request: URLRequest) throws

    !" URLRequest }
  92. 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"}
  93. 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"}
  94. 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 } } }
  95. 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 } } }
  96. HTTP Method Adapter extension Request { func buildRequest() !" URLRequest

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

    URLRequest { var request = URLRequest(url: url) #$ request.httpMethod = method.rawValue request = try method.adapter.adapted(request) #$%&' } }
  98. 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"}
  99. None
  100. Content Adapter enum ContentType: String { case json = "application/json"

    case urlForm = "application/x-!"#-form-urlencoded" var headerAdapter: AnyAdapter { return AnyAdapter { req in $% &'( } } }
  101. 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) } } }
  102. 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 { #$%&' } } } }
  103. Content Adapter extension Request { func buildRequest() throws !" URLRequest

    { #$ %&' request = try RequestContentAdapter( method: method, contentType: contentType, content: parameters) .adapted(request) } }
  104. ग़㮆 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 } }
  105. 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 } }
  106. 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) } } }
  107. 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) } } }
  108. ౯㮉؉ԧՋ焒 — 㴕ୌ RequestAdapter protocol ( ਧ嬝᭗አጱ 䯤ୌ URLRequest ጱොୗ

    ) — ֵ HTTPMethod ޾ ContentType ඪൔ RequestAdapter — 凚 Request ᯈᗝ [RequestAdapter]
  109. Client ጱ实揣 1. ᯈᗝ抬࿢ 2. 归ቘࢧ䛑

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

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

  113. ು᨝ғResponse Decision ౯㮉ํইӥ䷥ᒽғ — continue 媣媲䁆ᤈӥӞ㮆䷥ᒽ — restart ᯿ෛ昲ᤈ抬࿢ —

    errored 䝦ڊ梊抅ᤒ晄०䤂 — done ྋଉਠ౮抬࿢
  114. Decision Action enum DecisionAction<Req: Request> { case continueWith(Data, HTTPURLResponse) case

    restartWith([Decision]) case errored(Error) case done(Req.Response) }
  115. 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) }
  116. Parsing Decision

  117. 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)) } } }
  118. 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)) } } }
  119. ๅग़ໜֺ 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)) } } }
  120. ๅग़ໜֺ 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)) } } }
  121. ๅग़ໜֺ struct DataMappingDecision: Decision { let condition: (Data) !" Bool

    let transform: (Data) !" Data func shouldApply #$ %& { return condition(data) } func apply #$ %& { closure(.continueWith(transform(data), response)) } }
  122. ๅग़ໜֺ DataMappingDecision( condition: { $0.isEmpty }, transform: { _ in

    "{}".data(using: .utf8)! } ) <empty> !" "{}"
  123. ๅग़ໜֺ #if SERVER_NOT_READY DataMappingDecision( condition: { _ in true },

    transform: { _ in #"{"dummy": "data"}"# } ) #end
  124. 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 } }
  125. Request protocol Ӿጱ Decision extension Request { !" #$% var

    decisions: [Decision] { return [ RefreshTokenDecision(), RetryDecision(leftCount: 2), BadResponseStatusCodeDecision(), DataMappingDecision( condition: { $0.isEmpty }, transform: { _ in "{}".data(using: .utf8)! } ), ParseResultDecision() ] } }
  126. 归ቘ 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(!" #$) }
  127. 归ቘ 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(!" #$) }
  128. ౯㮉؉ԧՋ焒 — 㴕ୌ Decision protocol (ਧ嬝归ቘ Response ጱොୗ) — Ⴒےݱ圵

    Decision 䋿֢ — 凚 Request ᯈᗝ [Decision]
  129. ๋஍... 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 }
  130. ๋஍... !" क़ࣁ䷱ํ捧҅㲌ࣁय़ӧݶ 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) } }
  131. դ嘨㲌ࣁጱ奈侜҅ ᚆ捰ኞၚ꧌佝ኝᢸ޾ࡅ䗫

  132. ᭐晃 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() ] } }
  133. 㱢㵟 زկ۸ Ⴔศ 奈ڍ䤖 ݢ介手 चෝ protocol 欥ၚ ๅग़䷥ᒽ ݢ䢷઀

    奞ᔉଶഴګ ݢ඙֢
  134. None
  135. 揾Ⴎૡᑕ䒍੪ฎ 春焒䰼䋿僻嶆Ӭນᆳ —— Nelson

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

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

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

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

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

  141. உ櫞㻟?

  142. 姜᪠娒ᑕ ӧஉ櫞

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

  144. 䋿֢ Ԟӧ櫞

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

    LineSDK/Networking
  146. None
  147. ఽ拽 ᘰ室 (Ask Speaker ൉׀ Kingfisher 揳℅) @onevcat | 2019.9.22

    Taipei