Chapters

Hide chapters

Combine: Asynchronous Programming With Swift

Fourth Edition · iOS 16 · Swift 5.8 · Xcode 14

8. In Practice: Project "Collage Neue"
Written by Marin Todorov

In the past few chapters, you learned a lot about using publishers, subscribers and different operators 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 tandem with system frameworks like Photos.
  • Handling user events with Combine.
  • Using a variety of operators to create different subscriptions to drive your app’s logic.
  • Wrapping existing Cocoa APIs so you can conveniently use them in your Combine code.

The project is called Collage Neue and it’s an iOS app which allows the user to create simple collages out of their photos, like this:

This project 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.

You will work through a number of loosely connected tasks where you will use techniques based on the materials you have covered so far in this book.

Additionally, you will get to use a few operators that will be introduced later on to help you power some of the advanced features of the app.

Without further ado — it’s time to get coding!

Getting Started With “Collage Neue”

To get started with Collage Neue, open the starter project provided with this chapter’s materials. The app’s structure is rather simple — there is a main view to create and preview collages and an additional view where users select photos to add to their in-progress collage:

Note: In this chapter, you will specifically excercise working with Combine. You’ll get to try various ways of binding data but will not focus on working with Combine and SwiftUI specifically; you will look into how to use these two frameworks together in Chapter 15, In Practice: Combine & SwiftUI.

Currently, the project doesn’t implement any 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 CollageNeueModel.swift and import the Combine framework at the top of the file:

import Combine

This will allow you to use Combine types in your model file. To get started, add two new private properties to the CollageNeueModel class:

private var subscriptions = Set<AnyCancellable>()
private let images = CurrentValueSubject<[UIImage], Never>([])

subscriptions is the collection where you will store any subscriptions tied to the lifecycle of the main view or the model itself. In case the model is released, or you manually reset subscriptions, all the ongoing subscriptions will be conveniently canceled.

Note: As mentioned in Chapter 1, “Hello, Combine!,” subscribers return a Cancellable token to allow controlling the lifecycle of a subscription. AnyCancellable is 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.

Generally speaking, a CurrentValueSubject is a perfect fit to represent state, such as an array of photos or a loading state, while PassthroughSubject is more fitting to represent events, for example a user tapping a button, or simply indicating something has happened.

Next, to get some images added to the collage and test your code, append the following line to add():

images.value.append(UIImage(named: "IMG_1907")!)

Whenever the user taps the + button in the top-right navigation item, which is bound to CollageNeueModel.add(), you will add IMG_1907.jpg to the current images array and send that value through the subject.

You can find IMG_1907.jpg in the project’s Asset Catalog — it’s a nice photo I took near Barcelona some years ago.

Conveniently, CurrentValueSubject allows you to mutate its value directly, instead of emitting the new value with send(_:). The two are identical so you can use whichever syntax feels better - you can try send(_:) in the next paragraph.

To also be able to clear the currently selected photos, move over to clear(), in the same file, and add there:

images.send([])

This line sends an empty array as the latest value of images.

Lastly, you need to bind the images subject to a view on screen. There are different ways to do that but, to cover more ground in this practical chapter, you are going to use a @Published property for that.

Add a new property to your model like so:

@Published var imagePreview: UIImage?

@Published is a property wrapper that wraps a “vanilla” property into a publisher - how cool is that? Since your model conforms to ObservableObject, binding imagePreview to a view on screen becomes super simple.

Scroll to bindMainView() and add this code to bind the images subject to the image preview on-screen.

// 1
images
  // 2
  .map { photos in
    UIImage.collage(images: photos, size: Self.collageSize)
  }
  // 3
  .assign(to: &$imagePreview)

The play-by-play for this subscription is as follows:

  1. You begin a subscription to the current collection of photos.
  2. You use map to convert them to a single collage by calling into UIImage.collage(images:size:), a helper method defined in UIImage+Collage.swift.
  3. You use the assign(to:) subscriber to bind the resulting collage image to imagePreview, which is the center screen image view. Using the assign(to:) subscriber automatically manages the subscription lifecycle.

Last, but not least, you need to display imagePreview in your view. Open MainView.swift and find the line Image(uiImage: UIImage()). Replace it with:

Image(uiImage: model.imagePreview ?? UIImage())

You use the latest preview, or an empty UIImage if a preview doesn’t exist.

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 +:

You 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 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 MainView called updateUI(photosCount:), which does various UI updates: it’ll disable the Save button when the current selection contains an odd number of photos, enable the Clear button whenever there is a collage in progress and more.

To call upateUI(photosCount:) every 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 logging or others.

Usually, it’s recommended to update UI from a sink(...) or assign(to:on:) but, in order to give it a try, in this section you’ll do that in handleEvents.

Go back to CollageNeueModel.swift and add a new property:

let updateUISubject = PassthroughSubject<Int, Never>()

To exercise using subjects to communicate between different types (e.g. in this case you’re using it so your model can “talk back” to your view) you add a new subject called the updateUISubject.

Via this new subject you will emit the number of currently selected photos so the view can observe the count and update its state accordingly.

In bindMainView(), insert this operator just before the line where you use map:

.handleEvents(receiveOutput: { [weak self] photos in
  self?.updateUISubject.send(photos.count)
})

Note: The handleEvents operator 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(photosCount:) just before they are converted into a single collage image inside the map operator.

Now, to observe updateUISubject in MainView, open MainView.swift and a new modifier directly below .onAppear(...):

.onReceive(model.updateUISubject, perform: updateUI)

This modifier observes the given publisher and calls updateUI(photosCount:) for the lifetime of the view. If you’re curious, scroll down to updateUI(photosCount:) and peak into the code.

Build and run the project and 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 will be enabled, like so:

Presenting Views

You saw how easy it is to route your UI’s data through a subject and bind it to some controls on-screen. Next, you’ll tackle another common task: Presenting a new view and getting some data back when the user is done using it.

The general idea of binding data remains the same. You just need more publishers, or subjects, to define the correct data flow.

Open PhotosView and 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 the necessary Combine code to your model to allow the user to select some Camera Roll photos and add them to their collage.

Add the following subject in CollageNeueModel.swift:

private(set) var selectedPhotosSubject =
  PassthroughSubject<UIImage, Never>()

This code allows CollageNeueModel to replace the subject with a new one after the subject has completed but other types only have access to send or subscribe to receive events.

Speaking of that, let’s hook up the collection view delegate method to that subject.

Scroll down to selectImage(asset:). The already-provided code fetches the given photo asset from the device library. Once the photo is ready, you should use the subject to send out the image to any subscribers.

Replace the // Send the selected image 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 is being dismissed to tear down any external subscriptions.

Again, you can achieve this in a couple of different ways, but for this chapter, open PhotosView.swift and find the .onDisappear(...) modifier.

Add inside .onDisappear(...):

model.selectedPhotosSubject.send(completion: .finished)

This code will send a finished event when you navigate back from the presented view. To wrap up the current task, you still need to subscribe to the selected photos and display those in your main view.

Open CollageNeueModel.swift, find add(), and replace its body with:

let newPhotos = selectedPhotosSubject

newPhotos
  .map { [unowned self] newImage in
  // 1
    return self.images.value + [newImage]
  }
  // 2
  .assign(to: \.value, on: images)
  // 3
  .store(in: &subscriptions)

In the code above, you:

  1. Get the current list of selected images and append any new images to it.
  2. Use assign to send the updated images array through the images subject.
  3. You store the new subscription in subscriptions. However, the subscription will end whenever the user dismisses the presented view controller.

With your new binding ready to test, the last step is to lift the flag that presents the photo picker view.

Open MainView.swift and find the + button action closure where you call model.add(). Add one more line to that closure:

isDisplayingPhotoPicker = true

The isDisplayingPhotoPicker state property is already wired to present PhotosView when set to true so you’re ready to test!

Run the app and 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 Neue 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 where you will see your new collage in full glory:

There is one loose end to take care of before moving on. If you navigate few times between the photo picker and the main view you will notice that you cannot add any more photos after the very first time.

Why is this happening?

The issue stems from how you’re reusing selectedPhotosSubject each time you present the photo picker. The first time you close that view, you send a finished completion event and the subject is completed.

You can still you use it to create new subscriptions but those subscriptions complete as soon as you create them.

To fix this, create a new subject each time you present the photo picker. Scroll to add() and insert to its top:

selectedPhotosSubject = PassthroughSubject<UIImage, Never>()

This will create a new subject each time you present the photo picker. You should now be free to navigate back and forth between the views while still being able to add more photos to the collage.

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 real apps, 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 simply 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 use a Combine Future to allow other types to subscribe to the operation result.

Note: If you need to refresh your knowledge on Future, revisit the “Hello Future” section in Chapter 2: Publishers & Subscribers.

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> {
  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 block 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:

  1. First, you create a request to store image.
  2. Then, you attempt to get the newly-created asset’s identifier via request.placeholderForCreatedAsset?.localIdentifier.
  3. If the creation has failed and you didn’t get an identifier back, you resolve the future with a PhotoWriter.Error.couldNotSavePhoto error.
  4. 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 CollageNeueModel.swift and inside save() append:

guard let image = imagePreview else { return }

// 1
PhotoWriter.save(image)
  .sink(
    receiveCompletion: { [unowned self] completion in
      // 2
      if case .failure(let error) = completion {
        lastErrorMessage = error.localizedDescription
      }
      clear()
    },
    receiveValue: { [unowned self] id in
      // 3
      lastSavedPhotoID = id
    }
  )
  .store(in: &subscriptions)

In this code, you:

  1. Subscribe the PhotoWriter.save(_:) future by using sink(receiveCompletion:receiveValue:).
  2. In case of completion with a failure, you save the error message to lastErrorMessage.
  3. In case you get back a value — the new asset identifier — you store it in lastSavedPhotoID.

lastErrorMessage and lastSavedPhotoID are already wired in the SwiftUI code to present the user with the respective messages.

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. As mentioned earlier, Combine code has to deal with a lot of asynchronously executed pieces of work 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 with UIKit/AppKit code (i.e. you have subclasses of UIViewController, UICollectionController, etc.) or when you have ObservableObjects for your SwiftUI views, you will need to pay attention to your memory management for all these classes.

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 than self if 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.

Sharing Subscriptions

Looking back to the code in CollageNeueModel.add(), you could do a few more things with the images being selected by the user in PhotosView.

This poses an uneasy question: Should you subscribe multiple times to the same selected photos 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 other unexpected work.

Publisher code subscribe assign map {...} filter {...} Publisher 2 Publisher 1

The correct way to go when creating multiple subscriptions to the same publisher is to share the original publisher using the share() operator. This wraps the publisher in a class and therefore it can safely emit to multiple subscribers without performing its underlying work again.

Still in CollageNeueModel.swift, find the line let newPhotos = selectedPhotosSubject and replace it with:

let newPhotos = selectedPhotosSubject.share()

Now, it’s safe to create multiple subscriptions to newPhotos without being afraid that the publisher is performing side effects multiple times for each new subscriber:

map {...} filter {...} Publisher code Share() Publisher Publisher code

A caveat to keep in mind is that share() does not re-emit any values from the shared subscription, so you only get values that occur after you subscribe.

For example, if you have two subscriptions on a share()d publisher and the source publisher emits synchronously upon subscribing, only the first subscriber will get the value, since the second one wasn’t subscribed when the value was actually emitted. If the source publisher emits asynchronously, that’s less often 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.

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.

Open CollageNeueModel.swift and replace the line where you share the selectedPhotosSubject subscription let newPhotos = selectedPhotosSubject.share() with:

let newPhotos = selectedPhotosSubject
  .prefix(while: { [unowned self] _ in
    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 selectedPhotosSubject alive as long as the total count of images selected is less than six. This 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 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 the same way, you can implement any logic you need by combining all the operators you already know and love like filter, dropFirst, map and so on.

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 one more optional task before moving on to more theory in the next chapter, keep reading below.

Open Utility/PHPhotoLibrary+Combine.swift and read the code that gets the Photos library authorization for the Collage Neue 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 PhotosView!

For bonus points, display an error message 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 views 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 more into the ways Combine integrates with the existing Foundation and UIKit/AppKit APIs.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.