Slide 1

Slide 1 text

A Network Layer in Swift - from simple to advanced - Frederik Vogel

Slide 2

Slide 2 text

About me • Freddy • from Germany • working at LINE Fukuoka • passioned about Swift and the power of Apps • like Board Games

Slide 3

Slide 3 text

Network Layer

Slide 4

Slide 4 text

Motivation • fundamental to many Apps • important to understand, control and own • let’s only depend on Foundation • fitting for our API, grow dynamically

Slide 5

Slide 5 text

Overview • Networking • from simple to more common • testability • NSOperation • properly setting up • chaining / setting dependencies

Slide 6

Slide 6 text

Simple Case one endpoint one use case

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

When to use? • MVP(Minimum Viable Product) • Single use case/purpose • Single API call • No need for Test / No need for abstraction

Slide 13

Slide 13 text

Common Case multiple APIs multiple use cases

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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,

Slide 22

Slide 22 text

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,

Slide 23

Slide 23 text

We need a Network layer

Slide 24

Slide 24 text

How can we abstract about it?

Slide 25

Slide 25 text

Different perspectives • group after functionality • dynamic vs static parts • dependencies • time / sequence

Slide 26

Slide 26 text

Group functionality

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

functionality • URLRequest - configure a request • URLSession - dispatch a request • URLSessionTask - control an ongoing request • Decoder - decode a response

Slide 32

Slide 32 text

Look at change

Slide 33

Slide 33 text

static / dynamic • URLRequest - change per API • URLSession - usually stays same • URLSessionTask - change per Request • Decoder - usually same method

Slide 34

Slide 34 text

Look at sequence and time

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

Dispatcher Client Request Dispatcher Protocol

Slide 37

Slide 37 text

Let’s look at code

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Dispatcher Protocol protocol Dispatcher { func dispatch(request: APIRequest, completionHandler: @escaping (Result) -> Void) }

Slide 40

Slide 40 text

Network Dispatcher struct NetworkDispatcher: Dispatcher { let baseURL: URL let session: URLSession init(baseURL: URL, session: URLSession = URLSession.shared) { self.baseURL = baseURL self.session = session } }

Slide 41

Slide 41 text

Network Dispatcher struct NetworkDispatcher: Dispatcher { func dispatch(request: APIRequest, completionHandler: @escaping (Result) -> Void) { // 1. build URLRequest from APIRequest // 2. dispatch URLRequest with URLSession // 3. handle error with Result } }

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Client struct Client { private let dispatcher: Dispatcher private let decoder = JSONDecoder() init(dispatcher: Dispatcher) { self.dispatcher = dispatcher } }

Slide 44

Slide 44 text

Client struct Client { func execute (request: APIRequest, completionHandler: (Result)) { dispatcher.dispatch(request: request) { (result: Result) in let decodedResult = result.flatMap { (data: Data) -> Result in do { let decodedData = try self.decoder.decode(ResponseType.self, from: data) return .success(decodedData) } catch let error { return .failure(error) } } completionHandler(decodedResult) } } }

Slide 45

Slide 45 text

Extend Client extension Client { func login(user: String, password: String, completionHandler: @escaping (Result) -> Void) { let request = APIRequest.login(user: user, password: password) let task = execute(request: request, completionHandler: completionHandler) return task } }

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

little break…

Slide 49

Slide 49 text

Advanced Case multiple APIs multiple use cases depending APIs cancel tasks

Slide 50

Slide 50 text

NSOperation • handle longer concurrent Tasks • GCD • Manage own state • KVC • Often managed by Operation Queues

Slide 51

Slide 51 text

NSOperationQueue • GCD, dispatch Queue • Observes Operation via KVO • starts operation when ready • removes Operation when finished • can cancel Operations

Slide 52

Slide 52 text

Subclass Operation NSOperation AsyncOperation GetUserOperation LoginOperation

Slide 53

Slide 53 text

State

Slide 54

Slide 54 text

Operation States ready finished executing canceled pending

Slide 55

Slide 55 text

State Properties • isReady • isCancelled • isExecuting • isFinished

Slide 56

Slide 56 text

State Properties • isReady • isCancelled • isExecuting • isFinished

Slide 57

Slide 57 text

Operation State • isExecuting, when set to true, isFinished must be false • isFinished when set to true, isExecuting must be false

Slide 58

Slide 58 text

Thread safety • properties must be thread safe • for overriding isExecuting, isFinished, isReady • should be also for custom input/output properties

Slide 59

Slide 59 text

KVO • isExecuting must send KVO Notification • isFinished must send KVO Notification

Slide 60

Slide 60 text

AsyncOperation

Slide 61

Slide 61 text

AsyncOperation private var _isExecuting: Bool = false override var isExecuting: Bool { get { _isExecuting } set { self._isExecuting = newValue } }

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

AsyncOperation final override func start() { if isCancelled { finish() return } isExecuting = true main() } final func finish() { isExecuting = false isFinished = true }

Slide 65

Slide 65 text

APIOperation

Slide 66

Slide 66 text

GetUserOperation final class GetUserOperation: AsyncOperation { let apiClient: Client var token: String var result: Result? 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() } } }

Slide 67

Slide 67 text

Chain Operations

Slide 68

Slide 68 text

Result -> Input GetUserOperation LoginOperation BlockOperation get Result set Input

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

a lot of code, a lot of content

Slide 73

Slide 73 text

GitHub • If you are interested, take your time and have a look • https://github.com/Schaltfehler/Network-Layer- Example

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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?

Slide 76

Slide 76 text

Thank you!