Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Wei Wang
September 22, 2019

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

Wei Wang

September 22, 2019
Tweet

More Decks by Wei Wang

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. ౯ጱඳԪ

    View Slide

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

    View Slide

  6. View Slide

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

    View Slide

  8. ૡٍ (2010, iOS 4)
    — ䷱ํ
    NSURLSession
    — ䷱ํ
    Codable
    ҅Ԟ䷱ํ
    NSJSONSerialization

    (
    㫋ݘӧ捧ጱݝํ
    UITableView)

    View Slide

  9. உ櫞㻟?

    View Slide

  10. உ櫞㻟?
    No

    View Slide

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

    View Slide

  12. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. உ櫞㻟?

    View Slide

  25. உ櫞㻟?
    No

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  34. ࢧ䛑
    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
    }
    }

    View Slide

  35. 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()
    }
    }

    View Slide

  36. 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()
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  39. ౯㮉؉ԧՋ焒

    HTTPRequest

    url
    ҅
    method
    ҅
    body
    ੗愇

    Client
    ളݑ
    HTTPRequest
    ҅旉䟵
    URLRequest
    ҅妔ࢧ
    Data

    HTTPResponse

    (Data?, URLResponse?,
    Error?)
    旉捧凚
    T: Codable

    View Slide

  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?) in
    print(res#$value#$form.foo %& "")
    !" 㺔氂 1: অग़ ???
    !" 㺔氂 2: HTTPBinPostResult 娒捌࢏כ挨Ҙ
    }

    View Slide

  41. දᜉ - 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)
    {
    !" #$%
    }

    View Slide

  42. දᜉ - 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)
    {
    !" #$%
    }

    View Slide

  43. දᜉ - 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)
    {
    !" #$%
    }

    View Slide

  44. දᜉ - 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()
    }

    View Slide

  45. දᜉ - 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()
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    }

    View Slide

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

    View Slide

  51. දᜉ - 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)
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  55. உ櫞㻟?

    View Slide

  56. உ櫞㻟?
    No

    View Slide

  57. ֕ฎ

    View Slide

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

    View Slide

  59. 匍䋿ฎ
    䵝ᯡጱ

    View Slide

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

    View Slide

  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"

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  78. ᗦঅጱቘమӮኴ
    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()
    }
    }

    View Slide

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

    View Slide

  80. உ櫞㻟?

    View Slide

  81. உ櫞㻟?
    Yes

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  89. ು᨝ғRequest Adapter
    request.xxx = yyy

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  99. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  111. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  116. Parsing Decision

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  134. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  141. உ櫞㻟?

    View Slide

  142. 姜᪠娒ᑕ
    ӧஉ櫞

    View Slide

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

    View Slide

  144. 䋿֢
    Ԟӧ櫞

    View Slide

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

    View Slide

  146. View Slide

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

    View Slide