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

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.

Frederik Vogel

January 25, 2020
Tweet

Other Decks in Technology

Transcript

  1. About me • Freddy • from Germany • working at

    LINE Fukuoka • passioned about Swift and the power of Apps • like Board Games
  2. Motivation • fundamental to many Apps • important to understand,

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

    testability • NSOperation • properly setting up • chaining / setting dependencies
  4. 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()
  5. 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()
  6. 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()
  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. When to use? • MVP(Minimum Viable Product) • Single use

    case/purpose • Single API call • No need for Test / No need for abstraction
  10. 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")
  11. 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() }
  12. 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()
  13. 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()
  14. 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()
  15. 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,
  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") 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,
  17. 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()
  18. 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()
  19. 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()
  20. 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()
  21. functionality • URLRequest - configure a request • URLSession -

    dispatch a request • URLSessionTask - control an ongoing request • Decoder - decode a response
  22. static / dynamic • URLRequest - change per API •

    URLSession - usually stays same • URLSessionTask - change per Request • Decoder - usually same method
  23. 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? }
  24. Network Dispatcher struct NetworkDispatcher: Dispatcher { let baseURL: URL let

    session: URLSession init(baseURL: URL, session: URLSession = URLSession.shared) { self.baseURL = baseURL self.session = session } }
  25. 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> } }
  26. 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)) } }
  27. Client struct Client { private let dispatcher: Dispatcher private let

    decoder = JSONDecoder() init(dispatcher: Dispatcher) { self.dispatcher = dispatcher } }
  28. 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) } } }
  29. 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 } }
  30. 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 ... }
  31. For Testing let dispatcher = MockedNetworkDispatcher(response: "ok") let client =

    Client(dispatcher: dispatcher) client.login(user: "Freddy", password: "secret") { result in ... }
  32. NSOperation • handle longer concurrent Tasks • GCD • Manage

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

    • starts operation when ready • removes Operation when finished • can cancel Operations
  34. Operation State • isExecuting, when set to true, isFinished must

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

    overriding isExecuting, isFinished, isReady • should be also for custom input/output properties
  36. AsyncOperation private var _isExecuting: Bool = false override var isExecuting:

    Bool { get { _isExecuting } set { self._isExecuting = newValue } }
  37. 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 } } }
  38. 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) ) } }
  39. AsyncOperation final override func start() { if isCancelled { finish()

    return } isExecuting = true main() } final func finish() { isExecuting = false isFinished = true }
  40. 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() } } }
  41. Chain Operations let login = LoginOperation(apiClient: client, user: "Freddi", password:

    "1234") let getUser = GetUserOperation(apiClient: client)
  42. 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 } }
  43. 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])
  44. GitHub • If you are interested, take your time and

    have a look • https://github.com/Schaltfehler/Network-Layer- Example
  45. 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
  46. 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?