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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    (
    㫋ݘӧ捧ጱݝํ
    UITableView)

    View full-size slide

  7. உ櫞㻟?
    No

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. ض፡፡姜᪠抬࿢Ҙ
    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 full-size slide

  13. Ӟ樄ত҅உ墋㻌
    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 full-size slide

  14. Ӟ樄ত҅உ墋㻌
    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 full-size slide

  15. Ӟ樄ত҅உ墋㻌
    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 full-size 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)
    !" Or whatever you want to do.
    }
    task.resume()

    View full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

  20. உ櫞㻟?
    No

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  23. 抬࿢
    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 full-size slide

  24. 抬࿢
    !" 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 full-size slide

  25. 抬࿢
    !" 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  29. ࢧ䛑
    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 full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. ֵአ 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 full-size slide

  33. ֵአ 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 full-size slide

  34. ౯㮉؉ԧՋ焒

    HTTPRequest

    url
    ҅
    method
    ҅
    body
    ੗愇

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

    HTTPResponse

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

    View full-size slide

  35. දᜉ - 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 full-size slide

  36. දᜉ - 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 full-size slide

  37. දᜉ - 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 full-size slide

  38. දᜉ - 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 full-size slide

  39. දᜉ - 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 full-size slide

  40. දᜉ - 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 full-size slide

  41. දᜉ - 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 full-size slide

  42. දᜉ - 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 full-size slide

  43. දᜉ - 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 full-size slide

  44. දᜉ - 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 full-size slide

  45. දᜉ - 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 full-size slide

  46. දᜉ - 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 full-size slide

  47. දᜉ - 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 full-size slide

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

    View full-size slide

  49. ౯㮉஑کՋ焒
    !" ਠᗦጱ抬࿢ොୗ
    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 full-size slide

  50. உ櫞㻟?
    No

    View full-size slide

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

    View full-size slide

  52. 匍䋿ฎ
    䵝ᯡጱ

    View full-size slide

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

    View full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

  56. Case 1 - Request
    func buildRequest() !" URLRequest {
    #$%&'
    if method () "GET" {
    var components = URLComponents(
    url: url,
    resolvingAgainstBaseURL: false)!
    components.queryItems = parameters.map {
    URLQueryItem(name: $0.key, value: $0.value as? String)
    }
    request.url = components.url
    } else *+ 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  60. 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 full-size slide

  61. 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 full-size slide

  62. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  65. 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 full-size slide

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

    View full-size slide

  67. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  70. 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 full-size slide

  71. ᗦঅጱቘమӮኴ
    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 full-size slide

  72. 䵝ᯡጱ匍䋿Ӯኴ
    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 full-size slide

  73. உ櫞㻟?
    Yes

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  84. 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 full-size slide

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

    View full-size slide

  86. 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 full-size slide

  87. 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 full-size slide

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

    View full-size slide

  89. 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 full-size slide

  90. 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 full-size slide

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

    View full-size slide

  92. 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 full-size slide

  93. 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 full-size slide

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

    View full-size slide

  95. ग़㮆 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 full-size slide

  96. 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 full-size slide

  97. 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 full-size slide

  98. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  105. 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 full-size slide

  106. Parsing Decision

    View full-size slide

  107. 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 full-size slide

  108. 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 full-size slide

  109. ๅग़ໜֺ
    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 full-size slide

  110. ๅग़ໜֺ
    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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  114. 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 full-size slide

  115. 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 full-size slide

  116. 归ቘ 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 full-size slide

  117. 归ቘ 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 full-size slide

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

    View full-size slide

  119. ๋஍...
    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 full-size slide

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

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

    View full-size slide

  122. ᭐晃 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  130. 姜᪠娒ᑕ
    ӧஉ櫞

    View full-size slide

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

    View full-size slide

  132. 䋿֢
    Ԟӧ櫞

    View full-size slide

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

    View full-size slide

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

    View full-size slide