├── .github └── FUNDING.yml ├── .gitignore ├── Assets └── flatmap_playground_example.png ├── Combine.playground ├── Pages │ ├── @Published properties.xcplaygroundpage │ │ └── Contents.swift │ ├── Cancellation & Memory Management.xcplaygroundpage │ │ └── Contents.swift │ ├── Combining Operators.xcplaygroundpage │ │ └── Contents.swift │ ├── Custom UIKit Publishers.xcplaygroundpage │ │ └── Contents.swift │ ├── Debugging.xcplaygroundpage │ │ └── Contents.swift │ ├── Flatmap and error types.xcplaygroundpage │ │ └── Contents.swift │ ├── Foundation and Combine.xcplaygroundpage │ │ └── Contents.swift │ ├── Future and Promises.xcplaygroundpage │ │ └── Contents.swift │ ├── Publishers & Subscribers.xcplaygroundpage │ │ └── Contents.swift │ ├── Scheduling.xcplaygroundpage │ │ └── Contents.swift │ ├── Simple Operators.xcplaygroundpage │ │ └── Contents.swift │ ├── Subjects.xcplaygroundpage │ │ └── Contents.swift │ ├── Subscriptions.xcplaygroundpage │ │ └── Contents.swift │ └── What is Combine.xcplaygroundpage │ │ └── Contents.swift └── contents.xcplayground ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [AvdLee] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /Assets/flatmap_playground_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AvdLee/CombineSwiftPlayground/f2992d65acc99ee0f1f7898f75f868496792a555/Assets/flatmap_playground_example.png -------------------------------------------------------------------------------- /Combine.playground/Pages/@Published properties.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import UIKit 5 | import Combine 6 | 7 | /*: 8 | ## @Published properties 9 | A [Property Wrapper](https://www.avanderlee.com/swift/property-wrappers/) that adds a `Publisher` to any property. 10 | 11 | _Note: Xcode Playgrounds don't support running this Playground page with the @Published property unfortunately._ 12 | */ 13 | final class FormViewModel { 14 | @Published var isSubmitAllowed: Bool = true 15 | } 16 | 17 | final class FormViewController: UIViewController { 18 | 19 | var viewModel = FormViewModel() 20 | let submitButton = UIButton() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | // subscribe to a @Published property using the $ wrapped accessor 26 | viewModel.$isSubmitAllowed 27 | .receive(on: DispatchQueue.main) 28 | .print() 29 | .assign(to: \.isEnabled, on: submitButton) 30 | } 31 | } 32 | 33 | print("* Demonstrating @Published") 34 | 35 | let formViewController = FormViewController(nibName: nil, bundle: nil) 36 | print("Button enabled is \(formViewController.submitButton.isEnabled)") 37 | formViewController.viewModel.isSubmitAllowed = false 38 | print("Button enabled is \(formViewController.submitButton.isEnabled)") 39 | 40 | /*: 41 | ## ObservableObject 42 | - a class inheriting from `ObservableObject` automagically synthesizes an observable 43 | - ... which fires whenever any of the `@Published` properties of the class change 44 | 45 | */ 46 | print("\n* Demonstrating ObservableObject") 47 | 48 | class ObservableFormViewModel: ObservableObject { 49 | @Published var isSubmitAllowed: Bool = true 50 | @Published var username: String = "" 51 | @Published var password: String = "" 52 | var somethingElse: Int = 10 53 | } 54 | 55 | var form = ObservableFormViewModel() 56 | 57 | let formSubscription = form.objectWillChange.sink { _ in 58 | print("Form changed: \(form.isSubmitAllowed) \"\(form.username)\" \"\(form.password)\"") 59 | } 60 | 61 | form.isSubmitAllowed = false 62 | form.username = "Florent" 63 | form.password = "12345" 64 | form.somethingElse = 0 // note that this doesn't output anything 65 | 66 | //: [Next](@next) 67 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Cancellation & Memory Management.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | import UIKit 6 | 7 | /*: 8 | # Cancellation 9 | A subscription returns a `Cancellable` object 10 | 11 | Correct memory management using `Cancellable` makes sure you're not retaining any references. 12 | */ 13 | 14 | class MyClass { 15 | var cancellable: Cancellable? = nil 16 | var variable: Int = 0 { 17 | didSet { 18 | print("MyClass object.variable = \(variable)") 19 | } 20 | } 21 | 22 | init(subject: PassthroughSubject) { 23 | cancellable = subject.sink { value in 24 | // Note that we are introducing a retain cycle on `self` 25 | // on purpose, by not using `weak` or `unowned` 26 | self.variable += value 27 | } 28 | } 29 | 30 | deinit { 31 | print("MyClass object deallocated") 32 | } 33 | } 34 | 35 | func emitNextValue(from values: [Int], after delay: TimeInterval) { 36 | DispatchQueue.main.asyncAfter(deadline: .now() + delay) { 37 | var array = values 38 | subject.send(array.removeFirst()) 39 | if !array.isEmpty { 40 | emitNextValue(from: array, after: delay) 41 | } 42 | } 43 | } 44 | 45 | let subject = PassthroughSubject() 46 | var object: MyClass? = MyClass(subject: subject) 47 | 48 | emitNextValue(from: [1,2,3,4,5,6,7,8], after: 0.5) 49 | 50 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 51 | print("Nullify object") 52 | //: **TODO** uncomment the next line to see the change 53 | //object?.cancellable = nil 54 | object = nil 55 | } 56 | 57 | //: [Next](@next) 58 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Combining Operators.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | /*: 7 | # Combining publishers 8 | Several operators let you _combine_ multiple publishers together 9 | */ 10 | 11 | /*: 12 | ## `CombineLatest` 13 | - combines values from multiple publishers 14 | - ... waits for each to have delivered at least one value 15 | - ... then calls your closure to produce a combined value 16 | - ... and calls it again every time any of the publishers emits a value 17 | */ 18 | 19 | print("* Demonstrating CombineLatest") 20 | 21 | //: **simulate** input from text fields with subjects 22 | let usernamePublisher = PassthroughSubject() 23 | let passwordPublisher = PassthroughSubject() 24 | 25 | //: **combine** the latest value of each input to compute a validation 26 | let validatedCredentialsSubscription = Publishers 27 | .CombineLatest(usernamePublisher, passwordPublisher) 28 | .map { (username, password) -> Bool in 29 | !username.isEmpty && !password.isEmpty && password.count > 12 30 | } 31 | .sink { valid in 32 | print("CombineLatest: are the credentials valid? \(valid)") 33 | } 34 | 35 | //: Example: simulate typing a username and the password twice 36 | usernamePublisher.send("avanderlee") 37 | passwordPublisher.send("weakpass") 38 | passwordPublisher.send("verystrongpassword") 39 | 40 | /*: 41 | ## `Merge` 42 | - merges multiple publishers value streams into one 43 | - ... values order depends on the absolute order of emission amongs all merged publishers 44 | - ... all publishers must be of the same type. 45 | */ 46 | print("\n* Demonstrating Merge") 47 | let publisher1 = [1,2,3,4,5].publisher 48 | let publisher2 = [300,400,500].publisher 49 | 50 | let mergedPublishersSubscription = Publishers 51 | .Merge(publisher1, publisher2) 52 | .sink { value in 53 | print("Merge: subscription received value \(value)") 54 | } 55 | //: [Next](@next) 56 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Custom UIKit Publishers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import Combine 4 | /*: 5 | [Previous](@previous) 6 | ## Custom UIKit Publishers 7 | Unfortunately, not all UIKit elements are ready to use with Combine. A UISwitch, for example, does not support KVO. Therefore, custom UIKit publishers. 8 | */ 9 | /// A custom subscription to capture UIControl target events. 10 | final class UIControlSubscription: Subscription where SubscriberType.Input == Control { 11 | private var subscriber: SubscriberType? 12 | private let control: Control 13 | 14 | init(subscriber: SubscriberType, control: Control, event: UIControl.Event) { 15 | self.subscriber = subscriber 16 | self.control = control 17 | control.addTarget(self, action: #selector(eventHandler), for: event) 18 | } 19 | 20 | func request(_ demand: Subscribers.Demand) { 21 | // We do nothing here as we only want to send events when they occur. 22 | // See, for more info: https://developer.apple.com/documentation/combine/subscribers/demand 23 | } 24 | 25 | func cancel() { 26 | subscriber = nil 27 | } 28 | 29 | @objc private func eventHandler() { 30 | _ = subscriber?.receive(control) 31 | } 32 | 33 | deinit { 34 | print("UIControlTarget deinit") 35 | } 36 | } 37 | 38 | /// A custom `Publisher` to work with our custom `UIControlSubscription`. 39 | struct UIControlPublisher: Publisher { 40 | 41 | typealias Output = Control 42 | typealias Failure = Never 43 | 44 | let control: Control 45 | let controlEvents: UIControl.Event 46 | 47 | init(control: Control, events: UIControl.Event) { 48 | self.control = control 49 | self.controlEvents = events 50 | } 51 | 52 | /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)` 53 | /// 54 | /// - SeeAlso: `subscribe(_:)` 55 | /// - Parameters: 56 | /// - subscriber: The subscriber to attach to this `Publisher`. 57 | /// once attached it can begin to receive values. 58 | func receive(subscriber: S) where S : Subscriber, S.Failure == UIControlPublisher.Failure, S.Input == UIControlPublisher.Output { 59 | subscriber.receive(subscription: UIControlSubscription(subscriber: subscriber, control: control, event: controlEvents)) 60 | } 61 | } 62 | 63 | /// Extending the `UIControl` types to be able to produce a `UIControl.Event` publisher. 64 | protocol CombineCompatible { } 65 | extension UIControl: CombineCompatible { } 66 | extension CombineCompatible where Self: UIControl { 67 | func publisher(for events: UIControl.Event) -> UIControlPublisher { 68 | return UIControlPublisher(control: self, events: events) 69 | } 70 | } 71 | 72 | /*: 73 | ## Responding to UITouch events 74 | #### With the above, we can easily create a publisher to listen for `UIButton` events as an example. 75 | */ 76 | /// With the above, we can easily create a publisher to listen for `UIButton` events as an example. 77 | let button = UIButton() 78 | let subscription = button.publisher(for: .touchUpInside).sink { button in 79 | print("Button is pressed!") 80 | } 81 | button.sendActions(for: .touchUpInside) 82 | subscription.cancel() 83 | 84 | /*: 85 | ## Solving the UISwitch KVO problem 86 | #### As the `UISwitch.isOn` property does not support KVO this extension can become handy. 87 | */ 88 | extension CombineCompatible where Self: UISwitch { 89 | /// As the `UISwitch.isOn` property does not support KVO this publisher can become handy. 90 | /// The only downside is that it does not work with programmatically changing `isOn`, but it only responds to UI changes. 91 | var isOnPublisher: AnyPublisher { 92 | return publisher(for: [.allEditingEvents, .valueChanged]).map { $0.isOn }.eraseToAnyPublisher() 93 | } 94 | } 95 | 96 | let switcher = UISwitch() 97 | switcher.isOn = false 98 | let submitButton = UIButton() 99 | submitButton.isEnabled = false 100 | 101 | switcher.isOnPublisher.assign(to: \.isEnabled, on: submitButton) 102 | 103 | /// As the `isOn` property is not sending out `valueChanged` events itself, we need to do this manually here. 104 | /// This is the same behavior as it would be if the user switches the `UISwitch` in-app. 105 | switcher.isOn = true 106 | switcher.sendActions(for: .valueChanged) 107 | print(submitButton.isEnabled) 108 | //: [Next](@next) 109 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Debugging.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import UIKit 5 | import Combine 6 | /*: 7 | ## Debugging 8 | Operators which help debug Combine publishers 9 | 10 | More info: [https://www.avanderlee.com/debugging/combine-swift/‎](https://www.avanderlee.com/debugging/combine-swift/‎) 11 | */ 12 | 13 | enum ExampleError: Swift.Error { 14 | case somethingWentWrong 15 | } 16 | 17 | /*: 18 | ### Handling events 19 | Can be used combined with breakpoints for further insights. 20 | - exposes all the possible events happening inside a publisher / subscription couple 21 | - very useful when developing your own publishers 22 | */ 23 | let subject = PassthroughSubject() 24 | let subscription = subject 25 | .handleEvents(receiveSubscription: { (subscription) in 26 | print("Receive subscription") 27 | }, receiveOutput: { output in 28 | print("Received output: \(output)") 29 | }, receiveCompletion: { _ in 30 | print("Receive completion") 31 | }, receiveCancel: { 32 | print("Receive cancel") 33 | }, receiveRequest: { demand in 34 | print("Receive request: \(demand)") 35 | }).replaceError(with: "Error occurred").sink { _ in } 36 | 37 | subject.send("Hello!") 38 | subscription.cancel() 39 | 40 | // Prints out: 41 | // Receive request: unlimited 42 | // Receive subscription 43 | // Received output: Hello! 44 | // Receive cancel 45 | 46 | //subject.send(completion: .finished) 47 | 48 | /*: 49 | ### `print(_:)` 50 | Prints log messages for every event 51 | */ 52 | 53 | let printSubscription = subject 54 | .print("Print example") 55 | .replaceError(with: "Error occurred") 56 | .sink { _ in } 57 | 58 | subject.send("Hello!") 59 | printSubscription.cancel() 60 | 61 | // Prints out: 62 | // Print example: receive subscription: (PassthroughSubject) 63 | // Print example: request unlimited 64 | // Print example: receive value: (Hello!) 65 | // Print example: receive cancel 66 | 67 | /*: 68 | ### `breakpoint(_:)` 69 | Conditionally break in the debugger when specific values pass through 70 | */ 71 | let breakSubscription = subject 72 | .breakpoint(receiveOutput: { value in 73 | value == "Hello!" 74 | }) 75 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Flatmap and error types.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import UIKit 5 | import Combine 6 | 7 | /*: 8 | [Previous](@previous) 9 | ## flatmap 10 | - with `flatmap` you provide a new publisher every time you get a value from the upstream publisher 11 | - ... values all get _flattened_ into a single stream of values 12 | - ... it looks like Swift's `flatMap` where you flatten inner arrays of an array, just asynchronous. 13 | 14 | ## matching error types 15 | - use `mapError` to map a failure into a different error type 16 | */ 17 | 18 | //: define the error type we need 19 | enum RequestError: Error { 20 | case sessionError(error: Error) 21 | } 22 | 23 | //: we will send URLs through this publisher to trigger requests 24 | let URLPublisher = PassthroughSubject() 25 | 26 | //: use `flatMap` to turn a URL into a requested data publisher 27 | let subscription = URLPublisher.flatMap { requestURL in 28 | URLSession.shared 29 | .dataTaskPublisher(for: requestURL) 30 | .mapError { error -> RequestError in 31 | RequestError.sessionError(error: error) 32 | } 33 | } 34 | .assertNoFailure() 35 | .sink { result in 36 | print("Request completed!") 37 | _ = UIImage(data: result.data) 38 | } 39 | 40 | URLPublisher.send(URL(string: "https://httpbin.org/image/jpeg")!) 41 | //: [Next](@next) 42 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Foundation and Combine.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | import UIKit 6 | 7 | /*: 8 | ## Foundation and Combine 9 | Foundation adds Combine publishers for many types, like: 10 | */ 11 | 12 | /*: 13 | ### A URLSessionTask publisher and a JSON Decoding operator 14 | */ 15 | struct DecodableExample: Decodable { } 16 | 17 | URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.avanderlee.com/feed/")!) 18 | .map { $0.data } 19 | .decode(type: DecodableExample.self, decoder: JSONDecoder()) 20 | 21 | /*: 22 | ### A Publisher for notifications 23 | */ 24 | NotificationCenter.default.publisher(for: .NSSystemClockDidChange) 25 | 26 | /*: 27 | ### KeyPath binding to NSObject instances 28 | */ 29 | let ageLabel = UILabel() 30 | Just(28) 31 | .map { "Age is \($0)" } 32 | .assign(to: \.text, on: ageLabel) 33 | 34 | /*: 35 | ### A Timer publisher exposing Cocoa's `Timer` 36 | - this one is a bit special as it is a `Connectable` 37 | - ... use `autoconnect` to automatically start it when a subscriber subscribes 38 | */ 39 | let publisher = Timer 40 | .publish(every: 1.0, on: .main, in: .common) 41 | .autoconnect() 42 | 43 | //: [Next](@next) 44 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Future and Promises.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import UIKit 5 | import Combine 6 | 7 | /*: 8 | ## Future and Promises 9 | - a `Future` delivers exactly one value (or an error) and completes 10 | - ... it's a lightweight version of publishers, useful in contexts where you'd use a closure callback 11 | - ... allows you to call custom methods and return a Result.success or Result.failure 12 | */ 13 | 14 | struct User { 15 | let id: Int 16 | let name: String 17 | } 18 | let users = [User(id: 0, name: "Antoine"), User(id: 1, name: "Henk"), User(id: 2, name: "Bart")] 19 | 20 | enum FetchError: Error { 21 | case userNotFound 22 | } 23 | 24 | func fetchUser(for userId: Int, completion: (_ result: Result) -> Void) { 25 | if let user = users.first(where: { $0.id == userId }) { 26 | completion(Result.success(user)) 27 | } else { 28 | completion(Result.failure(FetchError.userNotFound)) 29 | } 30 | } 31 | 32 | let fetchUserPublisher = PassthroughSubject() 33 | 34 | fetchUserPublisher 35 | .flatMap { userId -> Future in 36 | Future { promise in 37 | fetchUser(for: userId) { (result) in 38 | switch result { 39 | case .success(let user): 40 | promise(.success(user)) 41 | case .failure(let error): 42 | promise(.failure(error)) 43 | } 44 | } 45 | } 46 | } 47 | .map { user in user.name } 48 | .catch { (error) -> Just in 49 | print("Error occurred: \(error)") 50 | return Just("Not found") 51 | } 52 | .sink { result in 53 | print("User is \(result)") 54 | } 55 | 56 | fetchUserPublisher.send(0) 57 | fetchUserPublisher.send(5) 58 | 59 | //: [Next](@next) 60 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Publishers & Subscribers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | /*: 5 | # Publishers and Subscribers 6 | - A Publisher _publishes_ values ... 7 | - .. a subscriber _subscribes_ to receive publisher's values 8 | 9 | __Specifics__: 10 | - Publishers are _typed_ to the data and error types they can emit 11 | - A publisher can emit, zero, one or more values and terminate gracefully or with an error of the type it declared. 12 | */ 13 | 14 | /*: 15 | ## Example 1 16 | "publish" just one value then complete 17 | */ 18 | let publisher1 = Just(42) 19 | 20 | // You need to _subscribe_ to receive values (here using a sink with a closure) 21 | let subscription1 = publisher1.sink { value in 22 | print("Received value from publisher1: \(value)") 23 | } 24 | 25 | /*: 26 | ## Example 2 27 | "publish" a series of values immediately 28 | */ 29 | let publisher2 = [1,2,3,4,5].publisher 30 | 31 | let subscription2 = publisher2 32 | .sink { value in 33 | print("Received value from publisher2: \(value)") 34 | } 35 | 36 | 37 | /*: 38 | ## Example 3 39 | assign publisher values to a property on an object 40 | */ 41 | print("") 42 | class MyClass { 43 | var property: Int = 0 { 44 | didSet { 45 | print("Did set property to \(property)") 46 | } 47 | } 48 | } 49 | 50 | let object = MyClass() 51 | let subscription3 = publisher2.assign(to: \.property, on: object) 52 | 53 | //: [Next](@next) 54 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Scheduling.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | 6 | /*: 7 | # Scheduling operators 8 | - Combine introduces the `Scheduler` protocol 9 | - ... adopted by `DispatchQueue`, `RunLoop` and others 10 | - ... lets you determine the execution context for subscription and value delivery 11 | */ 12 | 13 | let firstStepDone = DispatchSemaphore(value: 0) 14 | 15 | /*: 16 | ## `receive(on:)` 17 | - determines on which scheduler values will be received by the next operator and then on 18 | - used with a `DispatchQueue`, lets you control on which queue values are being delivered 19 | */ 20 | print("* Demonstrating receive(on:)") 21 | 22 | let publisher = PassthroughSubject() 23 | let receivingQueue = DispatchQueue(label: "receiving-queue") 24 | let subscription = publisher 25 | .receive(on: receivingQueue) 26 | .sink { value in 27 | print("Received value: \(value) on thread \(Thread.current)") 28 | if value == "Four" { 29 | firstStepDone.signal() 30 | } 31 | } 32 | 33 | for string in ["One","Two","Three","Four"] { 34 | DispatchQueue.global().async { 35 | publisher.send(string) 36 | } 37 | } 38 | 39 | firstStepDone.wait() 40 | 41 | /*: 42 | ## `subscribe(on:)` 43 | - determines on which scheduler the subscription occurs 44 | - useful to control on which scheduler the work _starts_ 45 | - may or may not impact the queue on which values are delivered 46 | */ 47 | print("\n* Demonstrating subscribe(on:)") 48 | let subscription2 = [1,2,3,4,5].publisher 49 | .subscribe(on: DispatchQueue.global()) 50 | .handleEvents(receiveOutput: { value in 51 | print("Value \(value) emitted on thread \(Thread.current)") 52 | }) 53 | .receive(on: receivingQueue) 54 | .sink { value in 55 | print("Received value: \(value) on thread \(Thread.current)") 56 | } 57 | 58 | //: [Next](@next) 59 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Simple Operators.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import Combine 5 | /*: 6 | # Simple operators 7 | - Operators are functions defined on publisher instances... 8 | - ... each operator returns a new publisher ... 9 | - ... operators can be chained to add processing steps 10 | */ 11 | 12 | /*: 13 | ## Example: `map` 14 | - works like Swift's `map` 15 | - ... operates on values over time 16 | */ 17 | let publisher1 = PassthroughSubject() 18 | 19 | let publisher2 = publisher1.map { value in 20 | value + 100 21 | } 22 | 23 | let subscription1 = publisher1 24 | .sink { value in 25 | print("Subscription1 received integer: \(value)") 26 | } 27 | 28 | let subscription2 = publisher2 29 | .sink { value in 30 | print("Subscription2 received integer: \(value)") 31 | } 32 | 33 | print("* Demonstrating map operator") 34 | print("Publisher1 emits 28") 35 | publisher1.send(28) 36 | 37 | print("Publisher1 emits 50") 38 | publisher1.send(50) 39 | 40 | subscription1.cancel() 41 | subscription2.cancel() 42 | 43 | /*: 44 | ## Example: `filter` 45 | - works like Swift's `filter` 46 | - ... operates on values over time 47 | */ 48 | 49 | let publisher3 = publisher1.filter { 50 | // only let even values pass through 51 | ($0 % 2) == 0 52 | } 53 | 54 | let subscription3 = publisher3 55 | .sink { value in 56 | print("Subscription3 received integer: \(value)") 57 | } 58 | 59 | print("\n* Demonstrating filter operator") 60 | print("Publisher1 emits 14") 61 | publisher1.send(14) 62 | 63 | print("Publisher1 emits 15") 64 | publisher1.send(15) 65 | 66 | print("Publisher1 emits 16") 67 | publisher1.send(16) 68 | 69 | //: [Next](@next) 70 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Subjects.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | import Foundation 3 | import Combine 4 | 5 | /*: 6 | # Subjects 7 | - A subject is a publisher ... 8 | - ... relays values it receives from other publishers ... 9 | - ... can be manually fed with new values 10 | - ... subjects as also subscribers, and can be used with `subscribe(_:)` 11 | */ 12 | 13 | /*: 14 | ## Example 1 15 | Using a subject to relay values to subscribers 16 | */ 17 | let relay = PassthroughSubject() 18 | 19 | let subscription = relay 20 | .sink { value in 21 | print("subscription1 received value: \(value)") 22 | } 23 | 24 | relay.send("Hello") 25 | relay.send("World!") 26 | 27 | //: What happens if you send "hello" before setting up the subscription? 28 | 29 | /*: 30 | ## Example 2 31 | Subscribing a subject to a publisher 32 | */ 33 | 34 | let publisher = ["Here","we","go!"].publisher 35 | 36 | publisher.subscribe(relay) 37 | 38 | /*: 39 | ## Example 3 40 | Using a `CurrentValueSubject` to hold and relay the latest value to new subscribers 41 | */ 42 | 43 | let variable = CurrentValueSubject("") 44 | 45 | variable.send("Initial text") 46 | 47 | let subscription2 = variable.sink { value in 48 | print("subscription2 received value: \(value)") 49 | } 50 | 51 | variable.send("More text") 52 | //: [Next](@next) 53 | -------------------------------------------------------------------------------- /Combine.playground/Pages/Subscriptions.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Combine 4 | import UIKit 5 | 6 | /*: 7 | ## Subscription details 8 | - A subscriber will receive a _single_ subscription 9 | - _Zero_ or _more_ values can be published 10 | - At most _one_ {completion, error} will be called 11 | - After completion, nothing more is received 12 | */ 13 | 14 | enum ExampleError: Swift.Error { 15 | case somethingWentWrong 16 | } 17 | 18 | let subject = PassthroughSubject() 19 | 20 | // The handleEvents operator lets you intercept 21 | // All stages of a subscription lifecycle 22 | subject.handleEvents(receiveSubscription: { (subscription) in 23 | print("New subscription!") 24 | }, receiveOutput: { _ in 25 | print("Received new value!") 26 | }, receiveCompletion: { _ in 27 | print("A subscription completed") 28 | }, receiveCancel: { 29 | print("A subscription cancelled") 30 | }) 31 | .replaceError(with: "Failure") 32 | .sink { (value) in 33 | print("Subscriber received value: \(value)") 34 | } 35 | 36 | subject.send("Hello!") 37 | subject.send("Hello again!") 38 | subject.send("Hello for the last time!") 39 | subject.send(completion: .failure(.somethingWentWrong)) 40 | subject.send("Hello?? :(") 41 | 42 | //: [Next](@next) 43 | -------------------------------------------------------------------------------- /Combine.playground/Pages/What is Combine.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | [Previous](@previous) 3 | # What is Combine? 4 | - A declarative Swift API for processing values over time 5 | - _"Customize handling of asynchronous events by combining event-processing operators."_ 6 | */ 7 | import Combine 8 | //: [Next](@next) 9 | -------------------------------------------------------------------------------- /Combine.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Antoine van der Lee 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Swift Playground explaining the concepts of the new Combine framework 2 | This playground will help you to get started with [Combine - Apple Developer Documentation](https://developer.apple.com/documentation/combine). 3 | 4 | ## Included in this playground 5 | The playground is a paged playground and is built up in several chapters 6 | 7 | - What is Combine? 8 | - Publishers & Subscribers 9 | - Rules of subscriptions 10 | - Foundation and Combine 11 | - @Published property and bindings 12 | - Memory management explained with `AnyCancellable` 13 | - Flatmap and matching error types 14 | - Combining Publishers 15 | - Future and Promises 16 | - Custom `Publisher` and UIKit extensions 17 | - Debugging publishers 18 | 19 | More to come! 20 | 21 | ## Requirements 22 | - Xcode 11 beta 4 23 | 24 | ## Example of a playground page 25 | ![](Assets/flatmap_playground_example.png) 26 | 27 | ## Interesting resources 28 | Some interesting resources regarding Combine. 29 | 30 | - [Getting started with the Combine framework in Swift](https://www.avanderlee.com/swift/combine/) 31 | - [Creating a custom Combine Publisher to extend UIKit](https://www.avanderlee.com/swift/custom-combine-publisher/) 32 | - [Combine debugging using operators in Swift](https://www.avanderlee.com/swift/combine-swift/) 33 | - [RxSwift to Apples Combine](https://medium.com/gett-engineering/rxswift-to-apples-combine-cheat-sheet-e9ce32b14c5b) 34 | - [WWDC 2019 s721 - Combine in practice](https://developer.apple.com/videos/play/wwdc2019/721/) 35 | - [WWDC 2019 s722 - Introducing Combine](https://developer.apple.com/videos/play/wwdc2019/722/) 36 | - [Open Source insight of Combine](https://github.com/broadwaylamb/OpenCombine) 37 | 38 | ## Interesting Frameworks 39 | Some interesting frameworks regarding Combine. 40 | 41 | - [Hover - Async network layer with Combine](https://github.com/onurhuseyincantay/Hover) 42 | - [Conbini - Custom `Publisher`s, operators, and `Subscriber`s](https://github.com/dehesa/Conbini) 43 | --------------------------------------------------------------------------------