├── .gitignore ├── CombineExamples.playground ├── Sources │ ├── AppState.swift │ ├── Environment.swift │ ├── User.swift │ └── API.swift ├── playground.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── contents.xcplayground └── Pages │ ├── Linking publishers.xcplaygroundpage │ └── Contents.swift │ ├── Retain cycles.xcplaygroundpage │ └── Contents.swift │ ├── Publishers and Subscribers.xcplaygroundpage │ └── Contents.swift │ ├── Creating an APICall with publishers.xcplaygroundpage │ └── Contents.swift │ └── Using publishers in real life.xcplaygroundpage │ └── Contents.swift ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xcuserdatad -------------------------------------------------------------------------------- /CombineExamples.playground/Sources/AppState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct AppState { 4 | public let token: String? 5 | } 6 | 7 | -------------------------------------------------------------------------------- /CombineExamples.playground/Sources/Environment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Environment { 4 | public var api = API() 5 | } 6 | 7 | public var Current = Environment() 8 | -------------------------------------------------------------------------------- /CombineExamples.playground/Sources/User.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct User { 4 | public let id: String 5 | public let name: String 6 | public let email: String? 7 | public let isVerified: Bool 8 | } 9 | 10 | -------------------------------------------------------------------------------- /CombineExamples.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CombineExamples.playground/playground.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Combine Example 2 | 3 | This is a Combine crash course playground that shows some of the basics of how to use combine and some more real world uses. It's only really scratching the surface of Combine, and meant to be a gentle introduction to some of the concepts. Fixes, more features, issues and suggestions are most welcome. 4 | 5 | Swift 5.1 -------------------------------------------------------------------------------- /CombineExamples.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nodes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CombineExamples.playground/Pages/Linking publishers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | 5 | /// In order to pass data between publishers we use the flatMap operator. You can think of it as taking the output of one publisher and using it as input to another to create a chain-like dataflow 6 | /// E.g. take a publisher that publishes Integers 7 | 8 | let intPublisher: PassthroughSubject = .init() 9 | 10 | // This function creates a publisher that squres values. It uses a Combine Publisher called `Just`, which is used for turning a simple value into a Publisher 11 | 12 | func squarePublisher(_ value: Input) -> AnyPublisher { 13 | Just(value*value).eraseToAnyPublisher() 14 | } 15 | 16 | func addOnePublisher(_ value: Input) -> AnyPublisher { 17 | Just(value+1).eraseToAnyPublisher() 18 | } 19 | 20 | 21 | intPublisher 22 | .flatMap { squarePublisher($0) } 23 | .flatMap { addOnePublisher($0) } 24 | .flatMap(addOnePublisher) // You can go point-free also 25 | .sink { double in 26 | print("output is: \(double)") 27 | } 28 | 29 | intPublisher.send(2) 30 | 31 | 32 | 33 | //: [Next](@next) 34 | -------------------------------------------------------------------------------- /CombineExamples.playground/Sources/API.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | extension String: Error {} 5 | 6 | public enum Credentials { 7 | public static let validUsername = "validUsername" 8 | public static let validPassword = "validPassword" 9 | } 10 | 11 | public struct API { 12 | 13 | public enum Error: Swift.Error, LocalizedError { 14 | case invalidCredentials 15 | 16 | public var errorDescription: String? { 17 | "\(self)" 18 | } 19 | } 20 | 21 | public var fetchUser = fetchUserImpl(token:) 22 | public var login = loginImpl(username: password: ) 23 | } 24 | 25 | private func fetchUserImpl(token: String) -> AnyPublisher { 26 | 27 | Just.init(User(id: "ABCD1234", name: "Hoppekat", email: "test@test.dk", isVerified: false)) 28 | .setFailureType(to: Error.self) 29 | .eraseToAnyPublisher() 30 | } 31 | 32 | private func loginImpl(username: String, password: String) -> AnyPublisher { 33 | 34 | guard username == Credentials.validUsername, password == Credentials.validPassword else { 35 | return Fail(error: API.Error.invalidCredentials) 36 | .eraseToAnyPublisher() 37 | } 38 | 39 | return Just("TheToken") 40 | .setFailureType(to: Error.self) 41 | .eraseToAnyPublisher() 42 | } 43 | 44 | -------------------------------------------------------------------------------- /CombineExamples.playground/Pages/Retain cycles.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import PlaygroundSupport 5 | import Combine 6 | import UIKit 7 | 8 | /// Since saving a Cancellable on an instance that may have reference to self, it's important to avoid retain cycles 9 | /// If you are referencing self in a publisher chain where self is holding on to the subscription via a cancellable, you can capture unowned self, like so 10 | 11 | class SomeVC: UIViewController { 12 | 13 | var cancellables: Set = [] 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | doSomethingAsynchronous() 19 | .flatMap { [unowned self] _ in self.doSomethingAsynchronous2() } 20 | .sink(receiveCompletion: { _ in 21 | print("Done") 22 | }, receiveValue: {_ in }) 23 | .store(in: &cancellables) 24 | 25 | } 26 | 27 | func doSomethingAsynchronous() -> AnyPublisher { 28 | Empty(completeImmediately: true).eraseToAnyPublisher() 29 | } 30 | func doSomethingAsynchronous2() -> AnyPublisher { 31 | Empty(completeImmediately: true).eraseToAnyPublisher() 32 | } 33 | } 34 | 35 | let vc = SomeVC() 36 | _ = vc.view 37 | 38 | // Make sure to always use the reference cycle tool in XCode to avoid leaks 39 | 40 | //: [Next](@next) 41 | -------------------------------------------------------------------------------- /CombineExamples.playground/Pages/Publishers and Subscribers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | 4 | /// In Combine there are `Publishers` and `Subscribers`. Publishers emit data over time. Let's make a type of Publisher: 5 | 6 | let publisher: PassthroughSubject = .init() 7 | 8 | /// Lifting real world data like textinput into the world of publishers is in Combine done using Subjects. There are `CurrentValueSubjects` that hold on to their value and thus always has one to give, and `PassthroughSubjects` like the one above that only pass on values when they receive one. 9 | /// As a Subscriber you need to subscribe to a Publisher in order to receive data. The simplest Subcriber is called `Sink`and is provided by Combine. It looks like this when you use it: 10 | 11 | publisher.sink { text in 12 | print(text) 13 | } 14 | /// As you can see,`Sink`is a Subscriber that transforms the data from the Publisher world back into the world of closures. 15 | /// 16 | /// To get a Subject to publish data you invoke the `.send(_:)` method, like so: 17 | /// 18 | publisher.send("Hello, old friend") 19 | /// 20 | /// So in order to publish some characters entered and print them by subscribing, this is how you could do it: 21 | 22 | let textObserver: PassthroughSubject = .init() 23 | 24 | let textCancellable = textObserver 25 | .sink { text in 26 | print(text) 27 | } 28 | 29 | textObserver.send("H") 30 | textObserver.send("e") 31 | textObserver.send("l") 32 | textObserver.send("l") 33 | textObserver.send("o") 34 | 35 | /// When you subscribe to a publisher you get a token back, a `Cancellable`. The subscription lives until the Cancellable is deallocated or `.cancel()` is called on it, which means you need to store the cancellable somewhere to get any output. If the cancellable is deallocated, your subscription is terminated 36 | 37 | -------------------------------------------------------------------------------- /CombineExamples.playground/Pages/Creating an APICall with publishers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import Foundation 5 | import UIKit 6 | 7 | /// If you have an API that is using completion closures you can wrap it to return a publisher e.g. `URLSession`It can be done using a Combine publisher type called a `Future`which will complete once in the future and yield a value or an error. it can be done like this 8 | 9 | func urlSessionPublisher(with request: URLRequest) -> AnyPublisher<(Data?, URLResponse?, Error?), Never> { 10 | Future { callback in 11 | URLSession.shared.dataTask(with: request) { (data, response, error) in 12 | callback(.success((data, response, error))) 13 | }.resume() 14 | } 15 | 16 | .eraseToAnyPublisher() 17 | } 18 | 19 | // At the end we call `eraseToAnyPublisher()` in order for publisher types to match up as working with Publishers will return long embedded generic types. 20 | // Luckily Combine comes with a built in dataTaskPublisher and also a publisher for decoding Data 21 | 22 | /// Thus we can make an api-call like so: 23 | 24 | // Our data lives here 25 | let url = URL(string: "https://api.chucknorris.io/jokes/random")! 26 | 27 | // The return type from the API 28 | struct Response: Decodable { 29 | let icon_url: URL 30 | let value: String 31 | } 32 | 33 | let cancellable = URLSession.shared.dataTaskPublisher(for: url) 34 | .map { data, _ in data } 35 | .decode(type: Response.self, decoder: JSONDecoder()) 36 | .sink(receiveCompletion: {_ in }, receiveValue: { 37 | print($0.value) 38 | }) 39 | 40 | /// We can wrap that in a function. Since we are mostly interested in the joke here, we'll map the result to use the `\Response.value` KeyPath and again eraseToAnyPublisher 41 | 42 | func getJoke() -> AnyPublisher { 43 | URLSession.shared.dataTaskPublisher(for: url) 44 | .map { data, _ in data } 45 | .decode(type: Response.self, decoder: JSONDecoder()) 46 | .map(\.value) 47 | .eraseToAnyPublisher() 48 | 49 | } 50 | 51 | /// And now we can subscribe and get a joke 52 | 53 | let cancellable2 = 54 | getJoke() 55 | .sink(receiveCompletion: { completion in 56 | if case .failure(let error) = completion { 57 | print(error) 58 | } 59 | }, receiveValue: { joke in 60 | print("Published joke: \(joke)") 61 | }) 62 | 63 | 64 | //: [Next](@next) 65 | -------------------------------------------------------------------------------- /CombineExamples.playground/Pages/Using publishers in real life.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | //: [Previous](@previous) 3 | 4 | /// Now let's emulate having tapped a loginbutton that triggers a loginflow with username and password. 5 | /// We'll use the username and password to try and login to a server and get a token. 6 | /// So the steps are 7 | /// 1. Create a publisher with appropriate types 8 | /// 2. flatMap on the publisher to get the data out of it and use it as input to a loginpublisher 9 | /// 3. Subscribe to the login publisher using sink and print the result 10 | 11 | let credentialsObserver1: PassthroughSubject<(username: String, password: String), Error> = .init() 12 | 13 | let cancellable1 = credentialsObserver1 14 | .flatMap { credentials in Current.api.login(credentials.username, credentials.password) } 15 | .sink(receiveCompletion: { completion in 16 | if case .failure(let error) = completion { 17 | print(error.localizedDescription) 18 | } 19 | }, receiveValue: { token in 20 | let token = "Received token: \(token)" 21 | print(token) 22 | }) 23 | 24 | credentialsObserver1.send((Credentials.validUsername, Credentials.validPassword)) 25 | 26 | /// Let's use the token to fetch a user from the api. Since text input doesn't fail in this example, the Error type can be Never 27 | 28 | let credentialsObserver2: PassthroughSubject<(username: String, password: String), Never> = .init() 29 | 30 | /// Never is an enum with no cases. It is used here to show that the publisher will never return an error 31 | 32 | let cancellable = credentialsObserver2 33 | .setFailureType(to: Error.self) // For the error types to match up we need to force the Error type to Error.self 34 | .flatMap { credentials in Current.api.login(credentials.username, credentials.password) } 35 | .flatMap { token in Current.api.fetchUser(token) } 36 | .sink(receiveCompletion: { completion in 37 | if case .failure(let error) = completion { 38 | print(error.localizedDescription) 39 | } 40 | }, receiveValue: { user in 41 | print("Received user: \(user)") 42 | }) 43 | 44 | credentialsObserver2.send((Credentials.validUsername, Credentials.validPassword)) 45 | 46 | // It seems like logging in and fetching the user is something we might usually do together. Let's bundle them up in a function. 47 | 48 | func loginAndFetchUser(username: String, password: String) -> AnyPublisher { 49 | 50 | Current.api.login(username, password) 51 | .flatMap { token in Current.api.fetchUser(token) } 52 | .eraseToAnyPublisher() 53 | } 54 | 55 | /// We can then flatmap the function since it returns a Publisher 56 | let credentialsObserver3: PassthroughSubject<(username: String, password: String), Never> = .init() 57 | 58 | let sub = credentialsObserver3 59 | .setFailureType(to: Error.self) 60 | .flatMap(loginAndFetchUser(username: password: )) // <- Used here in a point-free fashion 61 | .sink(receiveCompletion: { completion in 62 | if case .failure(let error) = completion { 63 | print(error.localizedDescription) 64 | } 65 | }, receiveValue: { user in 66 | print("Received user: \(user)") 67 | }) 68 | 69 | /// If any of the publisher in the chain fail, any subsequent links in the chain are skipped and control goes directly to the failure completion 70 | 71 | credentialsObserver3.send((Credentials.validUsername, "invalid")) 72 | 73 | 74 | //: [Next](@next) 75 | --------------------------------------------------------------------------------