8.
In Practice: Project "Collage"
Written by Marin Todorov
In the past few chapters, you learned a lot about using publishers, subscribers and all kinds of different operators. You did that in the “safety” of a Swift playground. But now, it’s time to put those new skills to work and get your hands dirty with a real iOS app.
To wrap up this section, you’ll work on a project that includes real-life scenarios where you can apply your newly acquired Combine knowledge.
This project will take you through:
- Using Combine publishers in your UIKit view controllers.
- Handling user events with Combine.
- Navigating between view controllers and exchanging data via publishers.
- Using a variety of operators to create different subscriptions to implement your app’s logic.
- Wrapping existing Cocoa APIs so you can conveniently use them in your Combine code.
The project is called Collage and it’s an iOS app which allows the user to create simple collages out of their photos, like this:
All of the above is a lot of work, but it will get you some practical experience with Combine before you move on to learning about more operators, and is a nice break from theory-heavy chapters.
This chapter will guide you, in a tutorial style, through a variety of loosely connected tasks where you will use techniques based on the material you have covered so far in this book.
Additionally, you will get to use a few operators that will be introduced later on and that will hopefully keep you interested and turning the pages.
Without further ado — it’s time to get coding!
Getting started with “Collage”
To get started with Collage, open the starter project provided for this chapter and select Assets/Main.storyboard in the project navigator. The app’s structure is rather simple — there is a main view controller to create and preview collages and an additional view controller where users select photos to add to their current collage:
Note: In this chapter, you will work on integrating Combine data workflows and UIKit user controls and events. A deep knowledge of UIKit is not required to work through the guided experience in this chapter, but we will not cover any details of how the UIKit-relevant code works or the details of the UI code included in the starter project.
Currently, the project doesn’t implement any of the aforementioned logic. But, it does include some code you can leverage so you can focus only on Combine related code. Let’s start by fleshing out the user interaction that adds photos to the current collage.
Open MainViewController.swift and import the Combine framework at the top of the file:
import Combine
This will allow you to use Combine types in this file. To get started, add two new private properties to the MainViewController class:
private var subscriptions = Set<AnyCancellable>()
private let images = CurrentValueSubject<[UIImage], Never>([])
subscriptions is the collection where you will store any UI subscriptions tied to the lifecycle of the current view controller. When you bind your UI controls tying those subscriptions to the lifecycle of the current view controller is usually what you need. This way, in case the view controller is popped out of the navigation stack or dismissed otherwise, all UI subscriptions will be canceled right away.
Note: As mentioned in Chapter 1, “Hello, Combine!,” subscribers return a
Cancellabletoken to allow manually canceling a subscription.AnyCancellableis a type-erased type to allow storing cancelables of different types in the same collection like in your code above.
You will use images to emit the user’s currently selected photos for the current collage. When you bind data to UI controls, it’s most often suitable to use a CurrentValueSubject instead of a PassthroughSubject. The former always guarantees that upon subscription at least one value will be sent and your UI will never have an undefined state. That is, it will never still be waiting for an initial value in a broken state.
Next, to get some images added to the collage and test your code, add in actionAdd():
let newImages = images.value + [UIImage(named: "IMG_1907.jpg")!]
images.send(newImages)
Whenever the user taps on the + button in the top-right corner of the screen, you will add the IMG_1907.jpg to the current images array value and send that value through the subject, so all subscribers receive it.
You can find IMG_1907.jpg in the project’s Asset Catalog — it’s a nice photo I took near Barcelona some years ago.
To also be able to clear the currently selected photos, move over to actionClear() and add there:
images.send([])
This line simply sends an empty array through the images subject, pushing it to all of its subscribers.
Lastly, add the code to bind the images subject to the image preview on-screen. Append at the end of viewDidLoad():
// 1
images
// 2
.map { photos in
UIImage.collage(images: photos, size: collageSize)
}
// 3
.assign(to: \.image, on: imagePreview)
// 4
.store(in: &subscriptions)
The play-by-play for this subscription is as follows:
- You begin a subscription to the current collection of photos.
- You use
mapto convert them to a single collage by calling intoUIImage.collage(images:size:), a helper method defined in UIImage+Collage.swift. - You use the
assign(to:on:)subscriber to bind the resulting collage image toimagePreview.image, which is the center screen image view. - Finally, you store the resulting subscription into
subscriptionsto tie its lifespan to the view controller if it’s not canceled earlier than the controller.
Time to test that new subscription! Build and run the app and click the + button few times. You should see a collage preview, featuring one more copy of the same photo each time you click +:
Thanks to the simplicity of binding via assign, you can get the photos collection, convert it to a collage and assign it to an image view in a single subscription!
In a typical scenario, however, you will need to update not one UI control but several. Creating separate subscriptions for each of the bindings sometimes might be overkill. So, let’s see how we can perform a number of updates as a single batch.
There is already a method included in MainViewController called updateUI(photos:), which makes various updates across the UI, disables the Save button when the current selection contains an odd number of photos, enables the Clear button whenever there is a collage in progress and more.
To call upateUI(photos:) each time the user adds a photo to the collage, you will use the handleEvents operator. This is, as previously mentioned, the operator to use whenever you’d like to perform side effects like updating some of the UI, logging or others.
Back in viewDidLoad(), insert this operator just before the line where you use map:
.handleEvents(receiveOutput: { [weak self] photos in
self?.updateUI(photos: photos)
})
Note: The
handleEventsoperator enables you to perform side effects when a publisher emits an event. You’ll learn a lot more about it in Chapter 10, “Debugging.”
This will feed the current selection to updateUI(photos:) just before they are converted into a single collage image inside the map operator.
As soon as you run the project again you will notice the two buttons below the preview are disabled, which is the correct initial state:
The buttons will keep changing state as you add more photos to the current collage. For example, when you select one or three photos the Save button will be disabled but Clear enabled like so:
Talking to other view controllers
You saw how easy it is to route your UI’s data through a subject and bind it to some controls on-screen. Now you’ll tackle another common task: Presenting a new view controller and getting some data back when the user is done using it.
The generic idea of exchanging data between two view controllers is exactly the same as subscribing to a subject in the same view controller. At the end of the day, you just have a publisher that emits some output and you use a subscriber to make something useful with the emitted values.
Scroll back to actionAdd(), comment the existing body of that method and add the following code instead:
let photos = storyboard!.instantiateViewController(
withIdentifier: "PhotosViewController") as! PhotosViewController
navigationController!.pushViewController(photos, animated: true)
This code instantiates a PhotosViewController from the project storyboard and pushes it onto the navigation stack. Since accessing the photos library and displaying a list of the available photos isn’t specifically a Combine related task, that code is already fleshed out for you.
Open PhotosViewController.swift and in viewDidLoad() you will see it already contains the code to load photos from the Camera Roll and display them in a collection view.
Your next task is to add a subject to the view controller and emit any images that the user taps in the Camera Roll list.
First and foremost, just like before, add a new import in PhotosViewController.swift:
import Combine
Unlike the images subject in the main view controller which you both subscribe to and send through, in this view controller you’d like to only allow other consumers to subscribe, making it a read-only publisher. In other words, you’d like to be able to send values from within PhotosViewController but only allow MainViewController to subscribe. To achieve that, you’ll use another common pattern: Exposing a publisher publicly by abstracting a private subject.
Add this pair of public and private properties under their respective MARK comments:
// MARK: - Public properties
var selectedPhotos: AnyPublisher<UIImage, Never> {
return selectedPhotosSubject.eraseToAnyPublisher()
}
// MARK: - Private properties
private let selectedPhotosSubject =
PassthroughSubject<UIImage, Never>()
This code allows the current type to use selectedPhotosSubject to send values while other types can only access the type-erased selectedPhotos to subscribe: For example, selectedPhotos cannot be used to send new values through the publisher. Speaking of, let’s hook up the collection view delegate method to that subject.
Scroll down to collectionView(_:didSelectItemAt:). The code in that method flashes the tapped collection cell and then fetches the photo asset from the device library. Finally, having the photo ready, you should use the subject to send out the image to any subscribers.
Replace the // Send the selected photo comment with:
self.selectedPhotosSubject.send(image)
Well, that was easy! However, since you’re exposing the subject to other types, you’d like to explicitly send a completion event in case the view controller is being dismissed to tear down any external subscriptions. Scroll up to viewWillDisappear(animated:) and append:
selectedPhotosSubject.send(completion: .finished)
This code will send a finished event when you navigate back from the view controller. To wrap up the current task, you still need to subscribe to the selected photos from your main view controller. Open MainViewController.swift, find actionAdd() and insert before the last line presenting PhotosViewController:
let newPhotos = photos.selectedPhotos
newPhotos
.map { [unowned self] newImage in
// 1
return self.images.value + [newImage]
}
// 2
.assign(to: \.value, on: images)
// 3
.store(in: &subscriptions)
In this subscription, you:
- Get the current list of selected images and append any new images to it.
- Use
assignto send the updated images array through theimagessubject. - You store the new subscription in
subscriptions. However, the subscription will end whenever the user dismisses the presented view controller.
Now, run the app and let’s try out the newly added code. Tap on the + button and you will see the system Photos access dialogue pop-up on-screen. Since this is your own app it’s safe to tap Allow Access to All Photos to allow accessing the complete Photo library on your Simulator from the Collage app:
This will reload the collection view with the default photos included with the iOS Simulator, or your own photos if you’re testing on your device:
Tap a few of those. They’ll flash to indicate they’ve been added to the collage. Then, tap to go back to the main screen you will see your new collage in full glory:
Wrapping a callback function as a future
In a playground, you might play with subjects and publishers and be able to design everything exactly as you like it, but in day-to-day iOS code, you will interact with various Cocoa APIs, such as accessing the Camera Roll, reading the device’s sensors or interacting with some database.
Later in this book, you will learn how to create your own custom publishers. However, in many cases adding a subject to an existing Cocoa class is enough to plug its functionality in your Combine workflow.
In this part of the chapter, you will work on a new custom type called PhotoWriter which will allow you to save the user’s collage to disk. You will use the callback-based Photos API to do the saving and a future to allow other types to subscribe to the operation result.
Open Utility/PhotoWriter.swift, which contains an empty PhotoWriter class, and add the following static function to it:
static func save(_ image: UIImage) -> Future<String, PhotoWriter.Error> {
return Future { resolve in
}
}
This function will try to asynchronously store the given image on disk and return a future that this API’s consumers will subscribe to.
You’ll use the closure-based Future initializer to return a ready-to-go future which will execute the code in the provided closure once initialized.
Let’s start fleshing out the future’s logic by inserting the following code inside the closure:
do {
} catch {
resolve(.failure(.generic(error)))
}
This is a pretty good start. You will perform the saving inside the do and, should it throw an error, you’ll resolve the future with a failure.
Since you don’t know the exact errors that could be thrown while saving the photo, you just take the thrown error and wrap it as a PhotoWriter.Error.generic error.
Now, for the real “meat” of the function: Insert the following inside the do body:
try PHPhotoLibrary.shared().performChangesAndWait {
// 1
let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
// 2
guard let savedAssetID =
request.placeholderForCreatedAsset?.localIdentifier else {
// 3
return resolve(.failure(.couldNotSavePhoto))
}
// 4
resolve(.success(savedAssetID))
}
Here, you use PHPhotoLibrary.performChangesAndWait(_) to access the Photos library synchronously. The future’s closure is itself executed asynchronously, so don’t worry about blocking the main thread. With this, you’ll perform the following changes from within the closure:
- First, you create a request to store
image. - Then, you attempt to get the newly-created asset’s identifier via
request.placeholderForCreatedAsset?.localIdentifier. - If the creation has failed and you didn’t get an asset id back, you resolve the future with a
PhotoWriter.Error.couldNotSavePhotoerror. - Finally, in case you got back a
savedAssetID, you resolve the future with success.
That’s everything you need to wrap a callback function, resolve with a failure if you get back an error or resolve with success in case you have some result to return!
Now, you can use PhotoWriter.save(_) to save the current collage when the user taps Save. Open MainViewController.swift and at the bottom of actionSave() append:
// 1
PhotoWriter.save(image)
.sink(receiveCompletion: { [unowned self] completion in
// 2
if case .failure(let error) = completion {
self.showMessage("Error", description: error.localizedDescription)
}
self.actionClear()
}, receiveValue: { [unowned self] id in
// 3
self.showMessage("Saved with id: \(id)")
})
.store(in: &subscriptions)
In the previous code you:
- You subscribe the
PhotoWriter.save(_)future by usingsink(receiveCompletion:receiveValue:). - In case of completion with a failure, you call into
showMessage(_:description:)to display an error alert on-screen. - In case you get back a value — the new asset id — you use
showMessage(_:description:)to let the user know their collage is saved successfully.
Run the app one more time, pick a couple of photos and tap Save. This will call into your shiny, new publisher and, upon saving the collage, will display an alert like so:
A note on memory management
Here is a good place for a quick side-note on memory management with Combine. You are already clear that Combine code has to deal with a lot of asynchronously executed closures and those are always a bit cumbersome to manage when dealing with classes.
When you write your own custom Combine code, you might be dealing predominantly with structs, so you won’t need to explicitly specify capturing semantics in the closures you use with map, flatMap, filter, etc.
However, when you’re dealing with UI code, and specifically with UIKit/AppKit related code, you will always need to work with classes like UIViewController, UICollectionController, NSFetchedController, etc.
When writing Combine code, standard rules apply, so you should use the same Swift capture semantics as always:
-
If you’re capturing an object that could be released from memory, like the presented photos view controller earlier, you should use
[weak self]or another variable thanselfif you capture another object. -
If you’re capturing an object that could not be released, like the main view controller in that Collage app, you can safely use
[unowned self]. For example, one that you never pop-out of the navigation stack and is therefore always present.
With that said, let’s continue with the next task: Presenting a view controller and fetching back the result via a future.
Presenting a view controller as a future
This task builds upon two of the tasks you’ve already completed. Previously you:
- Wrapped a UI-less callback function as a
Future. - Manually presented a view controller and subscribed to one of its exposed publishers.
This time, you will use a future to present a new view controller on-screen, wait until the user is done with it and then complete the future — all in one go!
In a new extension on ViewController, you will recreate the logic you have in MainViewController.showMessage(title:description:) but build it using a Combine future.
To do that, open the placeholder file UIViewController+Combine.swift and add the initial skeleton of the new method inside the provided extension:
func alert(title: String, text: String?) -> AnyPublisher<Void, Never> {
let alertVC = UIAlertController(title: title,
message: text,
preferredStyle: .alert)
}
The method returns an AnyPublisher<Void, Never> as you are not interested in returning any values but simply in completing the publisher when the user taps Close.
You begin by creating an alert controller called alertVC. Next, you will present it on-screen and dismiss it when the future completes.
Right after the line let alertVC = ..., append:
return Future { resolve in
alertVC.addAction(UIAlertAction(title: "Close",
style: .default) { _ in
resolve(.success(()))
})
self.present(alertVC, animated: true, completion: nil)
}
.handleEvents(receiveCancel: {
self.dismiss(animated: true)
})
.eraseToAnyPublisher()
Here, you create a Future, and upon subscription you add a Close button to the alert and present it on-screen. If the user taps the button, you resolve the future with success.
In case the subscription gets canceled, you dismiss the alert automatically from within handleEvents(receiveCancel:). This code handles the case when you tie the alert subscription to the currently-presented view controller and that controller gets dismissed itself. This will cancel the alert subscription and dismiss that alert as well.
To test this code, replace the code in the existing showMessage method in MainViewController with:
alert(title: title, text: description)
.sink(receiveValue: { _ in })
.store(in: &subscriptions)
Build and run the app another time and save some collages. You will see the alerts behaving just like before, only this time with your new Combine-ified code.
Sharing subscriptions
Looking back to the code in actionAdd(), you could do a few more things with the images being selected by the user in the presented PhotosViewController.
This poses an uneasy question: Should you subscribe multiple times to the same photos.selectedPhotos publisher, or do something else?
Turns out, subscribing to the same publisher might have unwanted side effects. If you think about it, you don’t know what the publisher is doing upon subscription, do you? It might be creating new resources, making network requests or something else.
The correct way to go when creating multiple subscriptions to the same publisher is to share the original publisher via the share() operator. This wraps the publisher in a class and therefore it can safely emit to multiple subscribers.
Find the line let newPhotos = photos.selectedPhotos and replace it with:
let newPhotos = photos.selectedPhotos.share()
Now, it’s safe to create multiple subscriptions to newPhotos without being afraid that the publisher is performing side effects multiple times upon each of the subscriptions:
A caveat to keep in mind is that share() does not re-emit any values from the shared subscription.
For example, if you have two subscriptions on a share() and the source publisher emits synchronously upon subscribing that will send the initial output value only to the first subscriber before the second one has the chance to subscribe. (If the source publisher emits asynchronously, that’s obviously not an issue.)
A reliable solution to that problem is building your own sharing operator which re-emits, or replays, past values when a new subscriber subscribes. Building your own operators is not complicated at all - you will, in fact, build one called shareReplay() in Chapter 18, “Custom Publishers & Handling Backpressure,” which will allow you to use share in the way described above.
Publishing properties with @Published
The Combine framework offers a few property wrappers, a new feature introduced in Swift 5.1. Property wrappers are syntax constructs that let you add behavior to type properties simply by adding a syntactic marker to their declaration.
Combine offers two property wrappers: @Published and @ObservedObject. In this chapter, you will get to try @Published and will cover @ObservedObject in a later one.
The @Published property wrapper allows you to automatically add a publisher to back your property that will emit a new output value every time you change the original property’s value.
The syntax looks like this:
struct Person {
@Published var age: Int = 0
}
The property age behaves just like any normal property. You can get and set its value imperatively as usual. The compiler will, however, generate another property automatically in your type, with the same accessibility level (private or public), called $age.
$age is a publisher that can never error out and its output is of the same type as the age property. Whenever you modify the value of age, $age will emit that new value.
Note:
@Publishedrequires an initial value. You either need to provide a default value for the original property or initialize it when instantiating your type.
This automation of creating publishers for your types allows you to super easily provide consumers of your APIs the ability to subscribe for data changes in real-time.
To try out @Published, you will add a new property to PhotosViewController to expose how many photos the user has selected. Open PhotosViewController.swift and add the new property below selectedPhotos:
var selectedPhotosCount = 0
Every time the user taps a photo, you will increase the value of the new property to keep track of how many the user has selected. Scroll down to collectionView(_:didSelectItemAt:) and find this line: self.selectedPhotosSubject.send(image).
Below that line, add:
self.selectedPhotosCount += 1
So far, selectedPhotosCount is a vanilla Int property. You can get and set its value, but you cannot subscribe to it.
Go back to the property declaration and add @Published like so:
@Published var selectedPhotosCount = 0
This makes the compiler generate behind the scenes a publisher for the property called $selectedPhotosCount.
You can now subscribe to this publisher from your main view controller and display the info about how many photos were selected on the main screen.
Subscribing works like any other publisher, just don’t forget to add the $ prefix to the property name. Open MainViewController.swift and scroll to actionAdd(). Here, towards the top of the method just after you create photos, add:
photos.$selectedPhotosCount
.filter { $0 > 0 }
.map { "Selected \($0) photos" }
.assign(to: \.title, on: self)
.store(in: &subscriptions)
You subscribe photos.$selectedPhotosCount and bind it to the view controller’s title property.
What we call a “binding” here, and also later in this book, is a subscription that “ends” on an assign(to:on:) subscriber. The noun “binding” describes very well the nature of such a subscription, or at least better than “assigning”. You bind the output of a publisher to a specific instance property on the receiving end.
You provide assign(to:on:) a value and a key path, and it updates that key path with any values it receives. It’s a practical subscriber in common use-cases, like binding your model to properties on your views, binding network requests to inputs on your view models and more.
Run the project again and tap a few photos. Then, navigate back and check the main view controller title:
Operators in practice
Now that you learned about a few useful reactive patterns, it’s time to practice some of the operators you covered in previous chapters and see them in action.
Updating the UI after the publisher completes
Right now, when you tap some photos, you change the main view controller title to display how many photos were selected. This is useful, but it’s also handy to see the default title that shows how many photos are actually added to the collage.
Scroll to actionAdd() and add another subscription to newPhotos just after the existing one:
newPhotos
.ignoreOutput()
.delay(for: 2.0, scheduler: DispatchQueue.main)
.sink(receiveCompletion: { [unowned self] _ in
self.updateUI(photos: self.images.value)
}, receiveValue: { _ in })
.store(in: &subscriptions)
This time, you perform the following:
-
ignoreOutput()ignores emitted values, only providing a completion event to the subscriber. -
delay(for:scheduler:)waits a given amount of seconds. This will give few seconds of the previous message saying “X photos selected” to tell the user how many they selected in one go before switching to the total amount of selected photos. -
sink(receiveCompletion:)callsupdateUI(photos:)to update the UI with the default controller title.
The subscription lets the custom title on-screen (that you display in the previous subscription) for 2 seconds and then invokes updateUI(photos:) to reset the title to its default value showing the total number of selected photos:
Accepting values while a condition is met
Change the code where you share the selectedPhotos subscription to the following:
let newPhotos = photos.selectedPhotos
.prefix(while: { [unowned self] _ in
return self.images.value.count < 6
})
.share()
You already learned about prefix(while:) as one of the powerful Combine filtering operators and here you get to use it in practice. The code above will keep the subscription to selectedPhotos alive as long as the total count of images selected is less than six. (I.e. will effectively allow the user to select up to six photos for their collage.)
Adding prefix(while:) just before the call to share() allows you to filter the incoming values, not only on one subscription, but on all subscriptions that consequently subscribe to newPhotos.
Run the app and try adding more than six photos. You will see that after the first six that the main view controller doesn’t accept more. In this chapter’s challenges, you will refine this by popping out the photos controller automatically when the user reaches the photos limit.
And that’s a wrap for this chapter! You did well and deserve a nice pat on the shoulder!
Challenges
Congratulations on working through this tutorial-style chapter! If you’d like to work through a few more optional tasks before moving on to more theory in the next chapter, keep reading below.
Challenge 1: Try more operators
Start by adding yet another filter. Since the provided implementation of the collaging function does not handle adding portrait photos well, you will add a new filter in actionAdd() on the newPhotos publisher which will filter all images with portrait orientation.
Tip: You can check the image’s
sizeproperty and compare thewidthandheightvalues to determine if the orientation is a landscape or a portrait.
Once you’re finished with your first task, create a new subscription to photos.selectedPhotos in which you:
-
Use a
filteroperator to pass the emitted value only in case the current count of total selected images inimages.valueis equal to5, which would mean the user is now adding their sixth image — the maximum amount of photos in a collage. -
Use a
flatMapto display an alert letting the user know they reached the maximum amount of photos and wait until they tap the Close button. -
Use a
sink(receiveValue:)to pop the photos view controller out of the navigation stack.
This subscription should, when the maximum number of photos for a collage is selected, pop the photos view controller automatically and take the user back to the main view controller:
Note: When testing this last functionality, pay attention, because any portrait photos you select won’t be counted towards the maximum of six. This got me a few times while I was working on this chapter because the system photo selector crops all photos as square thumbnails and you can’t really tell if they are in portrait or landscape orientation.
Challenge 2: PHPhotoLibrary authorization publisher
Open Utility/PHPhotoLibrary+Combine.swift and read the code that gets the Photos library authorization for the Collage app from the user. You will certainly notice that the logic is quite straightforward and is based on a “standard” callback API.
This provides you with a great opportunity to wrap a Cocoa API as a future on your own. For this challenge, add a new static property to PHPhotoLibrary called isAuthorized, which is of type Future<Bool, Never> and allows other types to subscribe to the Photos library authorization status.
You’ve already done this a couple of times in this chapter and the existing fetchAuthorizationStatus(callback:) function should be pretty straight forward to use. Good luck! Should you experience any difficulties along the way, don’t forget that you can always peak into the challenge folder provided for this chapter and have a look at the example solution.
Finally, don’t forget to use the new isAuthorized publisher in PhotosViewController code! That’s a good opportunity to exercise receiving values in the main queue as well.
For bonus points, display an error message via your custom alert publisher in case the user doesn’t grant access to their photos and navigate back to the main view controller when they tap Close.
To play with different authorization states and test your code, open the Settings app on your Simulator or device and navigate to Privacy/Photos.
Change the authorization status of Collage to either “None” or “All Photos” to test how your code behaves in those states:
If you made it successfully on your own so far into the challenges, you really deserve an extra round of applause! Either way, one possible solution you can consult with at any time is provided in the challenges folder for this chapter.
Key points
- In your day-to-day tasks, you’ll most likely have to deal with callback or delegate-based APIs. Luckily, those are easily wrapped as futures or publishers by using a subject.
- Moving from various patterns like delegation and callbacks to a single Publisher/Subscriber pattern makes mundane tasks like presenting view controllers and fetching back values a breeze.
- To avoid unwanted side-effects when subscribing a publisher multiple times, use a shared publisher via the
share()operator.
Where to go from here?
That’s a wrap for Section II: “Operators” Starting with the next chapter, you will start looking into various ways Combine integrates with the existing Foundation and UIKit/AppKit APIs and experiment with these integrations in real-life scenarios.