Categories
iOS Swift SwiftUI Xcode

Discovering #Preview macro in Swift

Previews are snippets of codes for creating a live preview of a view what we can see in Xcode. This enables to quickly iterate your views because we can see right away how the view looks after each of the code change. Previews are compiled alongside with the app, and therefore we can access all the other code and resources like images. If we want to use preview only resource, we can use Xcode’s development assets feature for making the resource only part of the app when rendering previews. Now when we know what previews are in general, let’s move on to creating a preview.

So far we have created a new struct which conforms to a PreviewProvider protocol if we wanted Xcode to render the view for us. Xcode 15 with Swift 5.9’s macro support introduces a #Preview macro, which replaces the old way of creating live previews. The benefit of the new macro is having to write less code to get going with live previews. Let’s compare the both approaches and have a look at an accessory view’s preview.

// Before
struct RatingsView_Previews: PreviewProvider {
static var previews: some View {
VStack {
ForEach(0…5, id: \.self) { value in
RatingsView(value: .constant(value))
}
}
.previewLayout(.sizeThatFits)
}
}
// After
#Preview(traits: .sizeThatFitsLayout) {
VStack {
ForEach(0…5, id: \.self) { value in
RatingsView(value: .constant(value))
}
}
}
view raw Preview.swift hosted with ❤ by GitHub

In the example above, we wanted to apply a trait since this view is a tiny accessory view, and therefore we would like to see it rendered as small as possible. A list of traits what we can use are listed here:

extension PreviewTrait where T == Preview.ViewTraits {
/// Preview with `.device` layout (the default).
public static var defaultLayout: PreviewTrait<Preview.ViewTraits> { get }
public static var sizeThatFitsLayout: PreviewTrait<Preview.ViewTraits> { get }
public static func fixedLayout(width: CGFloat, height: CGFloat) -> PreviewTrait<T>
public static var portrait: PreviewTrait<Preview.ViewTraits> { get }
public static var landscapeLeft: PreviewTrait<Preview.ViewTraits> { get }
public static var landscapeRight: PreviewTrait<Preview.ViewTraits> { get }
public static var portraitUpsideDown: PreviewTrait<Preview.ViewTraits> { get }
}

When working with full screen views, the preview macro can be as simple as this:

#Preview {
OnboardingView()
}

In addition to traits, we can also give a name to the preview which is displayed in the Xcode’s preview canvas. This can be useful if we create multiple previews for the same view.

A view with multiple previews with different names.

Another thing to note is that the preview canvas in Xcode also lists a pin button next to the name of the preview. Pinning previews is useful if we want to navigate to another file to make some changes and keeping the preview running. Maybe we want to change some constants which affects the layout of the view, but these constants are defined somewhere else. Then it is useful to keep the preview running and seeing how changing a constant in another view is reflected by the view using it.

There is another tip to keep in mind. We can run previews on physical devices as well. We just need to pick the physical device instead of a simulator from the device picker.

Preview canvas, which lists a physical device for preview.

Finally, let’s not forget about the great way to see all the colour scheme and dynamic type variants at the same time. There is a separate variants button for that next to the live preview and selectable preview buttons.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

If-else and switch statements as expressions in Swift

Swift 5.9 arrived with Xcode 15 and one of the new additions to the language is if-else and switch statements as expressions. SE-0380 proposed the new language feature, and we can find in depth information about it there. Let’s go through some of the examples where it comes handy.

Firstly, using if-else or switch for returning values in functions, closures or properties. I write quite a lot of code which just turns an enum value into some other type. Now we can omit the return statement which, in my mind, makes the code much more readable. Here we have an example of returning a different background colour based on the current coffee brewing method.

enum BrewMethod {
case espresso, frenchPress, chemex
}
// Before
func backgroundColor(for method: BrewMethod) -> UIColor {
switch method {
case .espresso: return .systemBrown
case .frenchPress: return .systemCyan
case .chemex: return .systemMint
}
}
// After
func backgroundColor(for method: BrewMethod) -> UIColor {
switch method {
case .espresso: .systemBrown
case .frenchPress: .systemCyan
case .chemex: .systemMint
}
}

Secondly, we can use this new feature for initializing variables with more complex logic without needing to create an immediately executing closure. Personally, I tend to do this quite a lot in my projects. Here is an example of assigning a value to the title variable based on the current brewing method. Note that we can omit return and the closure declaration now.

let method: BrewMethod = .espresso
// Before
let title: String = {
switch method {
case .espresso: return "Espresso"
case .frenchPress: return "French Press"
case .chemex: return "Chemex"
}
}()
// After
let title2: String =
switch method {
case .espresso: "Espresso"
case .frenchPress: "French Press"
case .chemex: "Chemex"
}

Here is another example where we use if-else statement for the same purpose. Especially handy to contain some more complex logic.

let method: BrewMethod = …
let hasSelection = …
let bannerText =
if !hasSelection {
"Please select a brewing method"
}
else if method == .chemex {
"Yada yada"
}
else {
"Other yada yada"
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Collapsible wheel picker for forms in SwiftUI

While working on an app, I needed a way for showing a picker with a wheel style in a form. If we add a wheel picker to a form, it is always shown. Better would be to show a row which describes the current selection and if I tap on the row, it reveals the picker with wheel style. This is exactly what we are going to build in this blog post. Moreover, we will first create a general purpose collapsible view which can be used in other use cases as well. In the end, we will have a collapsible wheel picker which behaves like this:

We start with creating a general purpose collapsible view which has two closures: one for title view and the other for collapsible view. The view implementation is fairly simple. A button controls the collapsed state and then optionally we add the collapsible secondary view. We are using view builders here since we want to construct views with closures which support creating multiple child views. The view is optimized for forms, and therefore we use the Group view which automatically adds a divider between the button and the secondary view. Button uses a plain style which removes the tint colour of it in forms.

struct CollapsibleView<Label, Content>: View where Label: View, Content: View {
@State private var isSecondaryViewVisible = false
@ViewBuilder let label: () -> Label
@ViewBuilder let content: () -> Content
var body: some View {
Group {
Button(action: { isSecondaryViewVisible.toggle() }, label: label)
.buttonStyle(.plain)
if isSecondaryViewVisible {
content()
}
}
}
}

Now we can use this view to create a new CollapsibleWheelPicker. This view just adds a picker with wheel style as the secondary view.

struct CollapsibleWheelPicker<SelectionValue, Content, Label>: View where SelectionValue: Hashable, Content: View, Label: View {
@Binding var selection: SelectionValue
@ViewBuilder let content: () -> Content
@ViewBuilder let label: () -> Label
var body: some View {
CollapsibleView(label: label) {
Picker(selection: $selection, content: content) {
EmptyView()
}
.pickerStyle(.wheel)
}
}
}

A full example looks like this:

struct ContentView: View {
@State private var selection = 1
let items = [0, 1, 2, 3, 4, 5, 6, 7, 8]
var body: some View {
NavigationStack {
Form {
CollapsibleWheelPicker(selection: $selection) {
ForEach(items, id: \.self) { item in
Text("\(item)")
}
} label: {
Text("Cups of Water")
Spacer()
Text("\(selection)")
}
}
}
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

@Observable macro in SwiftUI

@Observable macro was announced in WWDC23 with the aim of simplifying observation related code and improving the performance of the app. The performance boost comes from the fact that SwiftUI starts tracking which properties are used in SwiftUI view’s body and only then view rendering is triggered when tracked properties change. If any of the other properties change in the same model object, no new re-rendering happens – excellent.

It has always felt a bit odd to use the @Published property wrapper for each of the property, which should trigger view updates. I feel overwhelmingly happy that I can clean up all of that noise from the code now. Let’s compare two model objects: one which uses ObservableObject and the other one using the new @Observable macro. Note that we need to import the new Observation framework for being able to use @Observable.

struct ContentView: View {
@StateObject private var viewModel = ContentViewModel()
var body: some View {
VStack {
Text(viewModel.title)
TextField("Username", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
}
.padding()
}
}
final class ContentViewModel: ObservableObject {
@Published var username: String = ""
var title: String {
if username.isEmpty {
return "Hello, world!"
}
else {
return "Hello \(username)!"
}
}
}
view raw Old.swift hosted with ❤ by GitHub
A simple view with a view model conforming to ObservableObject.
import Observation
import SwiftUI
struct ContentView2: View {
@Bindable private var viewModel = Content2ViewModel()
var body: some View {
VStack {
Text(viewModel.title)
TextField("Username", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
}
.padding()
}
}
@Observable class Content2ViewModel {
var username: String = ""
var title: String {
if username.isEmpty {
return "Hello, world!"
}
else {
return "Hello \(username)!"
}
}
}
view raw New.swift hosted with ❤ by GitHub
A simple view with a view model using the new @Observable macro.

When we compare these two views then the main differences are that we can drop using @Published property wrappers, instead of conforming to ObservableObject protocol we instead add the @Observable macro in front of the view model’s definition. Instead of @StateObject we can just use @State since we still want the view to own and keep one instance of the view model.

Here we have a tiny example, but if we are dealing with larger models then having a chance to skip all the @Published property wrappers is a bliss.

Note: In Xcode 15 beta 1 the Swift compiler crashes if the Content2ViewModel is part of the ContentView2 extension.

Time to get back to consuming WWDC23 information, thank you for reading!

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Ratings view in SwiftUI

I am currently building a new app where I needed to show ratings for items. Ratings are from 0 to 5 with 0.5 step. In this blog post, we’ll implement a SwiftUI view which uses SF symbols to display ratings. Since we need to make sure it works properly with each of the step, we are going to leverage the usefulness of SwiftUI previews and render each state one by one vertically. Swift has a function called stride what we can use to easily create an array of double values with a step of 0.5 and feed it in to ForEach.

struct RatingsView_Previews: PreviewProvider {
static var previews: some View {
VStack {
let steps = Array(stride(from: 0.0, through: 5.0, by: 0.5))
ForEach(steps, id: \.self) { value in
RatingsView(value: value)
}
}
.previewLayout(.sizeThatFits)
}
}
view raw Preview.swift hosted with ❤ by GitHub

The view needs to render 5 stars next to each other and figure out which stars are filled, half filled or empty. Rendering 5 images is straight-forward in SwiftUI. We can use HStack and ForEach with an integer range from 0 to 5. Since we use SF symbols, then the colour of the filled star can be applied with the foregroundColor view modifier.

/// A view displaying a star rating with a step of 0.5.
struct RatingsView: View {
/// A value in range of 0.0 to 5.0.
let value: Double
var body: some View {
HStack(spacing: 0) {
ForEach(0..<5) { index in
Image(systemName: imageName(for: index, value: value))
}
}
.foregroundColor(.yellow)
}
func imageName(for starIndex: Int, value: Double) -> String {
// redacted
}
}

The most complicated part is implementing a function what returns the name of the SF symbol for the current star index and the double value passed into the view. When I thought about it then I ended up with two solutions: one with if clauses and the other one with switch statement.

func imageName(for starIndex: Int, value: Double) -> String {
// Version A
if value >= Double(starIndex + 1) {
return "star.fill"
}
else if value >= Double(starIndex) + 0.5 {
return "star.leadinghalf.filled"
}
else {
return "star"
}
// Version B
switch value – Double(starIndex) {
case ..<0.5: return "star"
case 0.5..<1.0: return "star.leadinghalf.filled"
default: return "star.fill"
}
}

The solution A is checking if the value is larger than the star index + 1 which means that in case of value 1.0 the zero indexed star image is rendered as filled star. Half-filled cases are handled by checking if the zero indexed star index + 0.5 is less than the current value. The solution B uses open-ended ranges and a switch statement. When we subtract the zero indexed star index from the current value, then if it is less than 0.5 then the star should be not filled, if 0.5 to 1.0 then half-filled and in other cases filled. When comparing these implementations, then the B is more concise, but on the other hand A is more readable. Years ago I read a book “Masters of Doom” and John Romero’s programming principles. Don’t know why, but his programming principle “simplify” always pops on my mind when I need to decide on multiple implementations. Like here, only because of this, I am going for solution A.

SwiftUI preview showing each state of the ratings view.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Getting started with matched geometry effect in SwiftUI

Matched geometry effect in SwiftUI is a view modifier for creating transition animations between two views. We create two different views, apply the modifier to both of them with a same identifier and namespace, and removing one and inserting another will create an animation where one view moves to the other view’s position. In other words, the view slides from the removed view’s position to the inserted view’s position. Let’s have a look at a concrete example, where we have two rows of views: rectangles and circles. Tapping on any of the views will remove it from one row and insert it into another row. Since we use matchedGeometryEffect view modifier, the change is animated and one view slides to another row.

The view implementation is straight forward. We have a view which renders two rows with ForEach and each row element is a button what has matchedGeometryEffect view modifier applied to. Model items just have an id and colour which is used for setting the foregroundColor. The view model holds two arrays of items, and select methods just remove one item from one array and insert it in another.

struct ContentView: View {
@StateObject var viewModel = ViewModel()
@Namespace var colorSelectionNamespace
var body: some View {
VStack {
HStack {
ForEach(viewModel.topColors) { item in
Button(action: { viewModel.selectTopColor(item) }) {
Rectangle()
.foregroundColor(item.color)
.frame(width: 40, height: 40)
}
.matchedGeometryEffect(id: item.id, in: colorSelectionNamespace)
}
}
.frame(minHeight: 50)
Spacer()
.frame(height: 200)
HStack {
ForEach(viewModel.bottomColors) { item in
Button(action: { viewModel.selectBottomColor(item) }) {
Circle()
.foregroundColor(item.color)
.frame(width: 20, height: 20)
}
.matchedGeometryEffect(id: item.id, in: colorSelectionNamespace)
}
}
.frame(minHeight: 50)
}
}
}

The view model’s implementation is shown below. Since we want to animate changes, we use withAnimation block and inside the block just remove one item and then insert it to another. Item properties use @Published property wrapper and the view model is ObservableObject, then property changes trigger view refresh. As one view is removed and the other view is inserted, then matchedGeometryEffect will trigger a transition animation where the view moves from one position to another with default fade animation.

extension ContentView {
final class ViewModel: ObservableObject {
@Published var topColors = ColorItem.all
@Published var bottomColors = [ColorItem]()
func selectBottomColor(_ item: ColorItem) {
withAnimation {
guard let index = bottomColors.firstIndex(of: item) else { return }
bottomColors.remove(at: index)
let insertionIndex = (topColors.startIndex…topColors.endIndex).randomElement() ?? 0
topColors.insert(item, at: insertionIndex)
}
}
func selectTopColor(_ item: ColorItem) {
withAnimation {
guard let index = topColors.firstIndex(of: item) else { return }
topColors.remove(at: index)
let insertionIndex = (bottomColors.startIndex…bottomColors.endIndex).randomElement() ?? 0
bottomColors.insert(item, at: insertionIndex)
}
}
}
}
view raw ViewModel.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

Implicit self for weak self captures

Since this week has kept me extremely busy with packing our things, selling furniture, and wrapping up any loose ends before relocating back to Estonia from Japan after 4.5 years, I am going to use this week’s blog post for highlighting a new Swift 5.8 language feature. I welcome this change since how hard I try there still will be cases where I need to capture self in closures.

The language feature we are talking about is of course SE-0365: Allow implicit self for weak self captures, after self is unwrapped. Which means that if we capture self weakly in a closure, use guard let self then there is no need to write self. inside the closure any more.

Let’s take a look at a concrete example from my SignalPath app. It is a tiny change, but I feel like it makes a lot of sense. Especially because I already have explicitly handled weak self with guard let.

// Old
iqDataSource.didChangeSampleCount
.sink { [weak self] sampleCount in
guard let self else { return }
let seriesDelta = self.layout.adjust(to: sampleCount)
guard seriesDelta < 0 else { return }
self.resetTiles(priority: .immediately)
}
.store(in: &cancellables)
// New
iqDataSource.didChangeSampleCount
.sink { [weak self] sampleCount in
guard let self else { return }
let seriesDelta = layout.adjust(to: sampleCount)
guard seriesDelta < 0 else { return }
resetTiles(priority: .immediately)
}
.store(in: &cancellables)
view raw Snippet.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Bizarre error in SwiftUI preview

The other day, I was playing around with matchedGeometryEffect view modifier in my sample app. I was just planning to show a list of items and then animate moving one item from one HStack to another. Suddenly, my SwiftUI preview stopped working. On the other hand, running exactly the same code on the simulator just worked fine. The code was very simple, consisting of view, view model and an Item model struct.

import SwiftUI
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(viewModel.items) { item in
Text(verbatim: item.name)
}
}
.padding()
}
}
extension ContentView {
final class ViewModel: ObservableObject {
let items: [Item] = [
Item(name: "first"),
Item(name: "second")
]
func select(_ item: Item) {
// implement
}
}
struct Item: Identifiable {
let name: String
var id: String { name }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

If you try to render SwiftUI preview (I was using Xcode 14.3) then Xcode is giving up with “Failed to launch the app XXX in reasonable time”. But if I try to build and run it on simulator, it just works fine. After some trial and error, it turned out that SwiftUI previews broke as soon as I added the func select(_ item: Item) function. If you pay a close attention, then you can see that the longer type name for Item is ContentView.Item, but within the ContentView.ViewModel type I am using just Item. I do not know why, but SwiftUI previews seems to get confused by it. As soon as I change the function declaration to func select(_ item: ContentView.Item) the preview starts rendering again. Another way is declaring the Item struct outside the ContentView extension.

The learning point is that if SwiftUI previews stop working suddenly, then make sure to check how nested types are used in function declarations.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
CoreData iOS Swift SwiftUI

NSFetchedResultsController wrapper for SwiftUI view models

Although SwiftUI framework contains a @FetchRequest property wrapper, it is not possible to use it outside of SwiftUI views because it relies on accessing the managed object context from the view environment. While working on an CoreData based app which uses view models a lot within SwiftUI views, I ended up creating a wrapper class for making it easier to use NSFetchedResultsController. The wrapper class is named FetchedResultList and what it does internally is creating a NSFetchedResultsController instance, handling its delegate methods, and notifying about data changes through closures. Here is an example of a view model using the wrapper.

@MainActor final class ViewModel: ObservableObject {
private let list: FetchedResultList<Fruit>
init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) {
list = FetchedResultList(context: context,
sortDescriptors: [
NSSortDescriptor(keyPath: \Fruit.name, ascending: true)
])
list.willChange = { [weak self] in self?.objectWillChange.send() }
}
var fruits: [Fruit] {
list.items
}
@Published var searchText: String = "" {
didSet {
if searchText.isEmpty {
list.predicate = nil
}
else {
list.predicate = NSPredicate(format: "name contains[cd] %@", searchText)
}
}
}
}
view raw Fruits.swift hosted with ❤ by GitHub

The view model conforms to ObservableObject protocol and FetchedResultList provides a willChange closure which gets called when NSFetchedResultsController’s will change delegate is called. Calling the objectWillChange publisher will signal the SwiftUI view about the data change and the view gets re-rendered. The wrapper also supports updating predicate or sort descriptors dynamically. Here, we can see how setting a searchableText property from the view will update the predicate of the wrapper class. The SwiftUI view which uses the view model looks like this:

struct FruitList: View {
@StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack {
List(viewModel.fruits) { item in
NavigationLink(item.name) {
Text(verbatim: "Detail view for \(item.name)")
}
}
.navigationTitle("Fruits")
.searchable(text: $viewModel.searchText)
}
}
}
view raw Fruits.swift hosted with ❤ by GitHub

Let’s take a look at how the wrapper class is implemented. The core logic around the wrapper is creating an instance of NSFetchedResultsController, allowing to reconfigure it dynamically, handling its delegate, and notifying changes through closures. Using closures instead of conforming to ObservableObject is a conscious choice since the main use case is using the wrapper class in view models or in other controllers, and it means that propagating the data change to a SwiftUI view needs to be done manually. It is shorter to call view model’s objectWillChange in a closure than republishing wrapper’s objectWillChange to view model’s objectWillChange. Moreover, it would make more sense to use SwiftUI provided property wrapper instead of this wrapper if we would want to handle CoreData fetching in the SwiftUI view’s implementation.

@MainActor final class FetchedResultList<Result: NSManagedObject> {
private let fetchedResultsController: NSFetchedResultsController<Result>
private let observer: FetchedResultsObserver<Result>
init(context: NSManagedObjectContext, filter: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]) {
let request = NSFetchRequest<Result>(entityName: Result.entity().name ?? "<not set>")
request.predicate = filter
request.sortDescriptors = sortDescriptors.isEmpty ? nil : sortDescriptors
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
observer = FetchedResultsObserver(controller: fetchedResultsController)
observer.willChange = { [unowned self] in self.willChange?() }
observer.didChange = { [unowned self] in self.didChange?() }
refresh()
}
private func refresh() {
do {
try fetchedResultsController.performFetch()
}
catch {
Logger().error("Failed to load results")
}
}
var items: [Result] {
fetchedResultsController.fetchedObjects ?? []
}
var predicate: NSPredicate? {
get {
fetchedResultsController.fetchRequest.predicate
}
set {
fetchedResultsController.fetchRequest.predicate = newValue
refresh()
}
}
var sortDescriptors: [NSSortDescriptor] {
get {
fetchedResultsController.fetchRequest.sortDescriptors ?? []
}
set {
fetchedResultsController.fetchRequest.sortDescriptors = newValue.isEmpty ? nil : newValue
refresh()
}
}
var willChange: (() -> Void)?
var didChange: (() -> Void)?
}

The wrapper class uses private FetchedResultsObserver class which must derive from NSObject because it implements NSFetchedResultsControllerDelegate methods. This approach allows keeping the FetchedResultList class a pure Swift class and not a NSObject subclass which I like to avoid in SwiftUI apps (just a personal preference).

private final class FetchedResultsObserver<Result: NSManagedObject>: NSObject, NSFetchedResultsControllerDelegate {
var willChange: () -> Void = {}
var didChange: () -> Void = {}
init(controller: NSFetchedResultsController<Result>) {
super.init()
controller.delegate = self
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
willChange()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
didChange()
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

Async-await and completion handler compatibility in Swift

The prominent way for writing async code before async-await arrived to Swift was using completion handlers. We pass in a completion handler which then gets called at some later time. When working with larger codebases, it is not straight-forward to convert existing code to use newer techniques like async-await. Often we make these changes over time, which means that in case of wrapping completion handler based code, we would have the same function both in form of completion handler and async await. Fortunately, it is easy to wrap existing completion handler based code and to provide an async-await version. The withCheckedThrowingContinuation() function is exactly for that use-case. It provides an object which will receive the output of our completion handler based code – most of the time a value or an error. If we use Result type in completion handlers, then it is only 3 lines of code to wrap it, thanks to the fact that the continuation has a dedicated function for resuming result types.

final class ImageFetcher {
func fetchImages(for identifiers: [String], completionHandler: @escaping (Result<[String: UIImage], Error>) -> Void) {
// …
}
}
extension ImageFetcher {
func fetchImages(for identifiers: [String]) async throws -> [String: UIImage] {
try await withCheckedThrowingContinuation { continuation in
fetchImages(for: identifiers) { result in
continuation.resume(with: result)
}
}
}
}

Great, but what if we add new code to an existing code base relying heavily on completion handler based code? Can we start with an async function and wrap that as well? Sure. In the example below, we have some sort of DataFetcher which has an async function. If we needed to call this function from a completion handler based code, we can add a wrapping function pretty easily. Later, if we have fully converted to async-await, it can be discarded easily. So how do we do it? We start off the wrapping code by creating a Task which starts running automatically and which also provides an async context for calling async functions. This means that we can call the async function with try await and catching the error if it throws. Then it is just a matter of calling the completion handler. Depends on the use-case and how this code is meant to be used, but we should always think about which thread should be calling the completion handler. In the example, we always switch to the main thread because the Task’s closure is running on a global actor (in other words, on a background thread).

final class DataFetcher {
func fetchData(for identifiers: [String]) async throws -> [String: Data] {
// …
}
}
extension DataFetcher {
func fetchData(for identifiers: [String], completionHandler: @escaping (Result<[String: Data], Error>) -> Void) {
Task {
do {
let data = try await fetchData(for: identifiers)
await MainActor.run {
completionHandler(.success(data))
}
}
catch {
await MainActor.run {
completionHandler(.failure(error))
}
}
}
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.