Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

First Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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

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.

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 Cancellable token to allow manually canceling 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. 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:

  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:on:) subscriber to bind the resulting collage image to imagePreview.image, which is the center screen image view.
  4. Finally, you store the resulting subscription into subscriptions to 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 your 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 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(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.

let photos = storyboard!.instantiateViewController(
  withIdentifier: "PhotosViewController") as! PhotosViewController

navigationController!.pushViewController(photos, animated: true)
import Combine
// MARK: - Public properties
var selectedPhotos: AnyPublisher<UIImage, Never> {
  return selectedPhotosSubject.eraseToAnyPublisher()
}

// MARK: - Private properties
private let selectedPhotosSubject =
  PassthroughSubject<UIImage, Never>()
self.selectedPhotosSubject.send(image)
selectedPhotosSubject.send(completion: .finished)
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)

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.

static func save(_ image: UIImage) -> Future<String, PhotoWriter.Error> {
  return Future { resolve in

  }
}
do {

} catch {
  resolve(.failure(.generic(error)))
}
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))
}
// 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)

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.

Presenting a view controller as a future

This task builds upon two of the tasks you’ve already completed. Previously you:

func alert(title: String, text: String?) -> AnyPublisher<Void, Never> {
  let alertVC = UIAlertController(title: title,
                                  message: text,
                                  preferredStyle: .alert)

}
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()
alert(title: title, text: description)
  .sink(receiveValue: { _ in })
  .store(in: &subscriptions)

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.

let newPhotos = photos.selectedPhotos.share()

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.

struct Person {
  @Published var age: Int = 0
}
var selectedPhotosCount = 0
self.selectedPhotosCount += 1
@Published var selectedPhotosCount = 0
photos.$selectedPhotosCount
  .filter { $0 > 0 }
  .map { "Selected \($0) photos" }
  .assign(to: \.title, on: self)
  .store(in: &subscriptions)

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.

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)

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

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.

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.

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.

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now