A Network Layer in Swift

A Network Layer in Swift

The first half we look at how we can abstract and make a very simple and easy network layer that is testable and fits out API.
The second half we look at NSOperation and what you need to take care of when subclassing.

Ba7334ebfd8c140737b4e80c83b5b481?s=128

Frederik Vogel

January 25, 2020
Tweet

Transcript

  1. A Network Layer in Swift - from simple to advanced

    - Frederik Vogel
  2. About me • Freddy • from Germany • working at

    LINE Fukuoka • passioned about Swift and the power of Apps • like Board Games
  3. Network Layer

  4. Motivation • fundamental to many Apps • important to understand,

    control and own • let’s only depend on Foundation • fitting for our API, grow dynamically
  5. Overview • Networking • from simple to more common •

    testability • NSOperation • properly setting up • chaining / setting dependencies
  6. Simple Case one endpoint one use case

  7. Simple Case let url = URL(string: "https://api.myservice.com/users/1")! let dataTask =

    URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() let user = try! decoder.decode(User.self, from: data!) } dataTask.resume()
  8. Simple Case let url = URL(string: "https://api.myservice.com/users/1")! let dataTask =

    URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() let user = try! decoder.decode(User.self, from: data!) } dataTask.resume()
  9. Simple Case let url = URL(string: "https://api.myservice.com/users/1")! let dataTask =

    URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() let user = try! decoder.decode(User.self, from: data!) } dataTask.resume()
  10. Simple Case let url = URL(string: "https://api.myservice.com/users/1")! let dataTask =

    URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() let user = try! decoder.decode(User.self, from: data!) } dataTask.resume()
  11. Simple Case let url = URL(string: "https://api.myservice.com/users/1")! let dataTask =

    URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() let user = try! decoder.decode(User.self, from: data!) } dataTask.resume()
  12. When to use? • MVP(Minimum Viable Product) • Single use

    case/purpose • Single API call • No need for Test / No need for abstraction
  13. Common Case multiple APIs multiple use cases

  14. More complex let url = URL(string: "https://api.myservice.com/update")!

  15. More complex let url = URL(string: "https://api.myservice.com/update")! var request =

    URLRequest(url: url) request.httpMethod = "POST"
  16. More complex let url = URL(string: "https://api.myservice.com/update")! var request =

    URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type")
  17. More complex let url = URL(string: "https://api.myservice.com/update")! var request =

    URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() }
  18. More complex let url = URL(string: "https://api.myservice.com/update")! var request =

    URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in } dataTask.resume()
  19. More complex let url = URL(string: "https://api.myservice.com/update")! var request =

    URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume()
  20. More complex let url = URL(string: "https://api.myservice.com/update")! var request =

    URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume() let url = URL(string: "https://api.myservice.com/weather")! var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() guard let data = data, let success = let weather = try! decoder.decode(Weather.self, from: data!) else { fatalError() } } dataTask.resume()
  21. More complex let url = URL(string: "https://api.myservice.com/update")! var request =

    URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume() let url = URL(string: "https://api.myservice.com/weather")! var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() guard let data = data, let success = let weather = try! decoder.decode(Weather.self, from: data!) else { fatalError() } } dataTask.resume() let url = URL(string: “https://api.myservice.com/country”)! var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() guard let data = data,
  22. More complex let url = URL(string: "https://api.myservice.com/update")! var request =

    URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume() let url = URL(string: "https://api.myservice.com/weather")! var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() guard let data = data, let success = let weather = try! decoder.decode(Weather.self, from: data!) else { fatalError() } } dataTask.resume() let url = URL(string: “https://api.myservice.com/country”)! var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() guard let data = data, let url = URL(string: "https://api.myservice.com/update")! var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume() let url = URL(string: "https://api.myservice.com/weather")! var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() guard let data = data, let success = let weather = try! decoder.decode(Weather.self, from: data!) else { fatalError() } } dataTask.resume() let url = URL(string: “https://api.myservice.com/country”)! var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in let decoder = JSONDecoder() guard let data = data,
  23. We need a Network layer

  24. How can we abstract about it?

  25. Different perspectives • group after functionality • dynamic vs static

    parts • dependencies • time / sequence
  26. Group functionality

  27. functionality let url = URL(string: "https://api.myservice.com/update")! var request = URLRequest(url:

    url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume()
  28. functionality let url = URL(string: "https://api.myservice.com/update")! var request = URLRequest(url:

    url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume()
  29. functionality let url = URL(string: "https://api.myservice.com/update")! var request = URLRequest(url:

    url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume()
  30. functionality let url = URL(string: "https://api.myservice.com/update")! var request = URLRequest(url:

    url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: ["key":"abc123"]) } catch { fatalError() } let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in guard let data = data, let success = String(data: data, encoding: .utf8), success == "OK" else { fatalError() } } dataTask.resume()
  31. functionality • URLRequest - configure a request • URLSession -

    dispatch a request • URLSessionTask - control an ongoing request • Decoder - decode a response
  32. Look at change

  33. static / dynamic • URLRequest - change per API •

    URLSession - usually stays same • URLSessionTask - change per Request • Decoder - usually same method
  34. Look at sequence and time

  35. None
  36. Dispatcher Client Request Dispatcher Protocol

  37. Let’s look at code

  38. Request enum HTTPMethod: String { case get = "GET" case

    post = "POST" } struct APIRequest { let method: HTTPMethod let path: String let queryParameter: [URLQueryItem]? let headers: [String: String]? let body: Data? }
  39. Dispatcher Protocol protocol Dispatcher { func dispatch(request: APIRequest, completionHandler: @escaping

    (Result<Data, Error>) -> Void) }
  40. Network Dispatcher struct NetworkDispatcher: Dispatcher { let baseURL: URL let

    session: URLSession init(baseURL: URL, session: URLSession = URLSession.shared) { self.baseURL = baseURL self.session = session } }
  41. Network Dispatcher struct NetworkDispatcher: Dispatcher { func dispatch(request: APIRequest, completionHandler:

    @escaping (Result<Data, Error>) -> Void) { // 1. build URLRequest from APIRequest // 2. dispatch URLRequest with URLSession // 3. handle error with Result<Data, Error> } }
  42. Network Dispatcher // 2. dispatch URLRequest with URLSession let dataTask

    = session.dataTask(with: urlRequest) { (data: Data?, response: URLResponse?, error: Error?) in // 3. handle error with Result<Data, Error> if let error = error { completionHandler(.failure(error)) return } guard let data = data else { completionHandler(.failure(DispatcherError.noData)); return } guard let httpResponse = response as? HTTPURLResponse else { completionHandler(.failure(DispatcherError.noHTTPResponse)); return } switch httpResponse.statusCode { case 200 ... 299: completionHandler(.success(data)) default: let error = DispatcherError.httpStatus(httpResponse.statusCode) completionHandler(.failure(error)) } }
  43. Client struct Client { private let dispatcher: Dispatcher private let

    decoder = JSONDecoder() init(dispatcher: Dispatcher) { self.dispatcher = dispatcher } }
  44. Client struct Client { func execute<ResponseType: Codable> (request: APIRequest, completionHandler:

    (Result<ResponseType, Error>)) { dispatcher.dispatch(request: request) { (result: Result<Data, Error>) in let decodedResult = result.flatMap { (data: Data) -> Result<ResponseType, Error> in do { let decodedData = try self.decoder.decode(ResponseType.self, from: data) return .success(decodedData) } catch let error { return .failure(error) } } completionHandler(decodedResult) } } }
  45. Extend Client extension Client { func login(user: String, password: String,

    completionHandler: @escaping (Result<AuthToken, Error>) -> Void) { let request = APIRequest.login(user: user, password: password) let task = execute(request: request, completionHandler: completionHandler) return task } }
  46. Use Client let baseURL = URL(“https://api.myservice.com”) let dispatcher = NetworkDispatcher(baseURL:

    baseURL) let client = Client(dispatcher: dispatcher) client.login(user: "Freddy", password: "secret") { result in ... }
  47. For Testing let dispatcher = MockedNetworkDispatcher(response: "ok") let client =

    Client(dispatcher: dispatcher) client.login(user: "Freddy", password: "secret") { result in ... }
  48. little break…

  49. Advanced Case multiple APIs multiple use cases depending APIs cancel

    tasks
  50. NSOperation • handle longer concurrent Tasks • GCD • Manage

    own state • KVC • Often managed by Operation Queues
  51. NSOperationQueue • GCD, dispatch Queue • Observes Operation via KVO

    • starts operation when ready • removes Operation when finished • can cancel Operations
  52. Subclass Operation NSOperation AsyncOperation GetUserOperation LoginOperation

  53. State

  54. Operation States ready finished executing canceled pending

  55. State Properties • isReady • isCancelled • isExecuting • isFinished

  56. State Properties • isReady • isCancelled • isExecuting • isFinished

  57. Operation State • isExecuting, when set to true, isFinished must

    be false • isFinished when set to true, isExecuting must be false
  58. Thread safety • properties must be thread safe • for

    overriding isExecuting, isFinished, isReady • should be also for custom input/output properties
  59. KVO • isExecuting must send KVO Notification • isFinished must

    send KVO Notification
  60. AsyncOperation

  61. AsyncOperation private var _isExecuting: Bool = false override var isExecuting:

    Bool { get { _isExecuting } set { self._isExecuting = newValue } }
  62. AsyncOperation let stateQueue = DispatchQueue(label: "AsyncOperation.rw.state", attributes: .concurrent) private var

    _isExecuting: Bool = false override var isExecuting: Bool { get { return stateQueue.sync { _isExecuting } } set { stateQueue.sync(flags: .barrier) { self._isExecuting = newValue } } }
  63. AsyncOperation let stateQueue = DispatchQueue(label: "AsyncOperation.rw.state", attributes: .concurrent) private var

    _isExecuting: Bool = false override var isExecuting: Bool { get { return stateQueue.sync { _isExecuting } } set { willChangeValue(forKey: #keyPath(isExecuting) ) stateQueue.sync(flags: .barrier) { self._isExecuting = newValue } didChangeValue(forKey: #keyPath(isExecuting) ) } }
  64. AsyncOperation final override func start() { if isCancelled { finish()

    return } isExecuting = true main() } final func finish() { isExecuting = false isFinished = true }
  65. APIOperation

  66. GetUserOperation final class GetUserOperation: AsyncOperation { let apiClient: Client var

    token: String var result: Result<User, Error>? init(apiClient: Client, token: String) { self.apiClient = apiClient self.token = token } override func main() { apiClient.user(token: token) { result in self.result = result self.finish() } } }
  67. Chain Operations

  68. Result -> Input GetUserOperation LoginOperation BlockOperation get Result set Input

  69. Chain Operations let login = LoginOperation(apiClient: client, user: "Freddi", password:

    "1234") let getUser = GetUserOperation(apiClient: client)
  70. Chain Operations let login = LoginOperation(apiClient: client, user: "Freddi", password:

    "1234") let getUser = GetUserOperation(apiClient: client) let adapter = BlockOperation() { if let result = login.result, case .success(let auth) = result { getUser.token = auth.key } }
  71. Chain Operations let login = LoginOperation(apiClient: client, user: "Freddi", password:

    "1234") let getUser = GetUserOperation(apiClient: client) let adapter = BlockOperation() { if let result = login.result, case .success(let auth) = result { getUser.token = auth.key } } adapter.addDependency(login) getUser.addDependency(adapter) getUser.completionBlock = { print("Finished!!") } queue.addOperations([login, getUser, adapter])
  72. a lot of code, a lot of content

  73. GitHub • If you are interested, take your time and

    have a look • https://github.com/Schaltfehler/Network-Layer- Example
  74. Recap • abstract as much as you API needs •

    use a dispatch protocol to cut off actual request • use NSOperation to manage concurrency, dependency and task state
  75. Let’s talk! • Many different ways, this is just one

    possible way • Do you know other way or different idea? • Do you use frameworks or write you own Networking code? • Do you think Combine or Async Await will replace Operations? • Do you like Board Games?
  76. Thank you!