Advanced Git, Second Edition

Git is key to great version control and collaboration on software projects.
Stop struggling with Git and spend more time on the stuff that matters!

Home iOS & Swift Tutorials

Opaque Return Types and Type Erasure

Learn how to use opaque return types and type erasure to improve your understanding of intuitive and effective APIs, both as a developer and a consumer.

4.9/5 8 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

In this tutorial, you’ll learn about using opaque return types and type erasure in your code. These are advanced concepts and more commonly used by library authors than app developers. But even if you don’t plan on building a library anytime soon, understanding how and why these features are used will help you become a better app developer. :]

Getting Started

First, download the project materials by clicking the Download Materials button at the top or bottom of this tutorial.

Unlike most raywenderlich.com tutorials, the project materials for this tutorial contain two different projects. The first is a library called MagicImage, which provides a simple API for applying filters to images. The second is a project to demonstrate the library’s functionality, imaginatively called MagicImageDemo.

You won’t be opening the MagicImage library Xcode project in this tutorial — it’s embedded in the demo project for you already. Instead, open MagicImageDemo.xcodeproj in Xcode.

The demo project replicates the filter screen of photo-sharing apps like Instagram. During this tutorial, you’ll add new filters to the demo app. You’ll add new functionality to the underlying library, MagicImage, as well as fix issues in its current implementation.

Build and run the app. You’ll see an image of a butterfly and a filter bar below the image, containing a single filter called Normal.

The Starter Project

In Xcode, open Assets.xcassets. Then, open the Butterfly image. Note how the image appears true to life, with lush green plants in the background and a white butterfly in the foreground. Yet in the app, the image displays with a sepia filter applied.

Next, open FiltergramView.swift. This is the main view for the demo. The view itself has two sections stacked vertically. The image and the filter bar.

When the view appears it calls loadImage. This method loads the butterfly image and then transforms it into a new image by applying a sepia filter. The sepia image is then set as the image displayed in the app.

The sepia filter is an instance of a protocol called MIFilter. apply(to:) on UIImage is part of the MagicImage API. Both of these are discussed below.

The MIFilter protocol

Sharing Secrets

You’ll need to think about the code in this tutorial from two points of view: as the author of a library used by other developers and as the author of an app that uses the library. To make things clear, the text will refer to three different personas throughout the tutorial:

  • 🤓 Liam the Library Author. He’s in charge of writing the MagicImage library.
  • 🦸‍♀️Abbie the App Author. She uses the MagicImage library to enhance her latest app, Filtergram.
  • 🤖 And not forgetting Corinne the Compiler. Liam and Abbie need to listen to what she has to say!

The concepts you’ll learn in this tutorial can be quite hard to grasp. You might find it useful to think of type information as some sort of secret shared between Liam, Abbie and Corinne. Each of the concepts has the secret shared by a different mix of people.

Interesting Images

The MagicImage library removes the pain from applying filters to images. Under the hood, it uses Core Image. Core Image then uses Quartz, through another framework called Core Graphics, to actually render the images. Core Image and Core Graphics are complex frameworks that would take more than a tutorial to cover in their own rights.

Core Image provides its own image type, CIImage, and Core Graphics also provides its own image type, CGImage. These are separate from the SwiftUI Image type and the UIKit UIImage type. Confusing? Yes! This is why you need a library to make it easier. :]

Open MIFilter.swift. The MagicImage library exposes a protocol called MIFilter, which defines a method called apply(to:). This method applies a filter to an existing image: specifically, an MIImage.

Open MagicImage+UIImage.swift. First, note MIImage is simply a typealias for CIImage. Second, note this file adds an extension on UIImage that adds a single method: apply(_:). This method applies an MIFilter to itself before returning a new UIImage. You don’t need to understand exactly what this is doing, but if you’re curious, you can learn about Core Image from the official Apple documentation.

Note: You can also learn about Core Image in our video course on the subject.

Finally, switch back to MIFilter.swift. After the protocol definition, this file defines three filters. The first, IdentityFilter, is pretty boring. It simply returns the original image without any modification. Sepia uses the sepia tone CIFilter to apply a sepia effect to images. Finally, Compose is a different kind of filter that takes two input filters and composes (combines) them into one.

Protocols Primer

As a quick recap: Protocols define a contract other entities must fulfill when adopting the protocol. For example, the protocol Equatable from the Swift standard library requires any type that conforms to it to implement the type methods == and !=:

static func == (lhs: Self, rhs: Self) -> Bool
static func != (lhs: Self, rhs: Self) -> Bool

Protocols are useful because they allow developers to hide the specific implementation of a type away from others. If a function in MagicImage returns a protocol, then you can model this as Liam knows the secret of the returned type while Abbie and Corinne don’t.

Who knows the secret?

  • 🤓 Liam the library author ✅
  • 🤖 Corinne the Compiler ❌
  • 🦸‍♀️Abbie the App Author ❌
Note: To learn more about protocols you can read our tutorial about protocol oriented programming.

Fun With Filters

Currently, MagicImage provides only three filters, and two of them are pretty boring. Time to fix that! :] Open MIFilter.swift.

Under the definition for the Sepia filter, add the following code:

// 1
public struct Olde: MIFilter {
  // 2
  public var name = "Olde"
  public var id: String { self.name }

  public init() {}

  // 3
  public func apply(to miImage: MIImage) -> MIImage {
    let ciFilter = CIFilter.sepiaTone()
    ciFilter.inputImage = miImage
    ciFilter.intensity = 0.75

    // Create a CIImage from our filter
    guard let outputImage = ciFilter.outputImage else {
      return miImage
    }

    return outputImage
  }
}

// 4
public struct Posterize: MIFilter {
  public var name = "Posterize"
  public var id: String { self.name }

  public init() {}

  public func apply(to miImage: MIImage) -> MIImage {
    let ciFilter = CIFilter.colorPosterize()
    ciFilter.inputImage = miImage
    ciFilter.levels = 10

    guard let outputImage = ciFilter.outputImage else {
      return miImage
    }

    return outputImage
  }
}

public struct Crystallize: MIFilter {
  public var name = "Crystallize"
  public var id: String { self.name }

  public init() {}

  public func apply(to miImage: MIImage) -> MIImage {
    let ciFilter = CIFilter.crystallize()
    ciFilter.inputImage = miImage
    ciFilter.radius = 50

    guard let outputImage = ciFilter.outputImage else {
      return miImage
    }

    return outputImage
  }
}

// 5  
public struct HorizontalFlip: MIFilter {
  public var name = "Flip Horizontal"
  public var id: String { self.name }

  public init() {}

  public func apply(to miImage: MIImage) -> MIImage {
    return miImage.oriented(.upMirrored)
  }
}

This looks like a lot of code, but much of it is quite similar. Here’s a breakdown:

  1. Define a new filter called Olde, which conforms to the MIFilter protocol.
  2. Give the filter a name and an ID. In this case, you use the name as the ID.
  3. Write the apply method. This method creates a Core Image sepiaTone filter and applies the filter to the provided MIImage before returning the output from the filter.
  4. The Posterize and Crystallize filters follow the same pattern, albeit with different Core Image filters.
  5. Finally, the HorizontalFlip flip filter uses the built-in oriented(_:) method of CIImage (remembering MIImage is just a typealias for CIImage) to create a filter that flips the input image.

Now that you’ve added some filters, it’s time to give them a whirl in the demo app.

Open FiltergramView.swift. In loadImage(), replace the use of Sepia() with your new filters, one at a time. Build the app each time and marvel at the beauty of your new filters! :]

// One at a time!
let uiImage = inputImage.apply(Olde())
let uiImage = inputImage.apply(Crystallize())
let uiImage = inputImage.apply(Posterize())
let uiImage = inputImage.apply(HorizontalFlip())

Butterfly with background blurred

Finish this section by updating the filter in loadImage() with selectedFilter, like so:

let uiImage = inputImage.apply(selectedFilter)

The app now uses the filter stored in the FiltergramView‘s state to apply to the input image rather than a hard-coded filter.

Getting Generics

As a refresher, generics allow a developer to write a property or a function that defers specifying the specific type(s) in use until called by another piece of code.

A common example is the Array structure (or any of the other collection types in the Swift standard library). Arrays can hold any type of data. But for any single Array, every element in the array must have the same type. Array is defined as:

@frozen struct Array<Element>

Here, Element is a named generic type. The authors of the Swift standard library cannot know what type is being stored in the Array. That’s up to the developer who’s using it. Instead, they use Element as a placeholder.

In other words, Liam the Library Author doesn’t know the secret of the type, but Abbie and Corinne do.

  • 🤓 Liam the Library Author ❌
  • 🤖 Corinne the Compiler ✅
  • 🦸‍♀️Abbie the App Author ✅

Associated Types

Protocols can also become generic using a feature called associated types. This allows a protocol to provide a name for a type that isn’t defined until the protocol is adopted.

For example, consider a protocol concerned with fetching data:

protocol DataFetcher {
  // 1
  func fetch(completion: [DataType]? -> Void)
  // 2
  associatedtype DataType
}

This protocol:

  1. Defines a method, fetch(completion:), which receives a completion handler that’s called with the result of the fetch — in this case, an array of DataType.
  2. Defines an associated type, DataType. The library author (Liam) doesn’t want to limit what type of data this protocol fetches. It should be generic over any type of data. So instead, the protocol defines an associated type named DataType, which can be filled in later.

Later, when Abbie wants to create a structure to fetch JSON data, she could write the following code:

struct JSONDataFetcher: DataFetcher {
  func fetch(completion: [JSONData]? -> Void) {
    // ... fetch JSON data from your API
  }
}

As with generic types and functions, Abbie has to define the concrete type, in this case JSONData, but Liam doesn’t know or care what it is when he defines the protocol.

Paltry Protocols

Your app will want to apply a filter every time selectedFilter updates. To do this, the compiler needs to tell if two instances of MIFilter are equal. This is done by conforming to the Equatable protocol.

Open MIFilter.swift. Near the top of the file, update the definition for the MIFilter protocol to include both Equatable and Identifiable:

public protocol MIFilter: Equatable, Identifiable {

⚠️ If you build and run the app now, compilation fails with the following error:

Protocol 'MIFilter' can only be used as a generic constraint because it has Self or associated type requirements

What’s going on here? From the section above, you know what associated types are. And the declarations for Equatable and Identifiable don’t include any, so it can’t be that.

If you read the requirements of the Equatable protocol, you find the following required type method:

static func == (lhs: Self, rhs: Self) -> Bool

Self in the above method signature refers to the type of the actual instances being compared. It’s a form of generics as the author of the Swift standard library doesn’t know what type will be being used when == is called. But they’re able to state the lhs and rhs parameters have to be the same type.

As discussed above, the compiler doesn’t know of the underlying types when a protocol is used in place of a concrete type. But the compiler knows of the underlying type when a generic type is used.

Consequently, given your MIFilter protocol now has a Self requirement, it can only be used as a generic constraint. Fortunately, the above section contained a refresher on generics. It’s time to put that refresher to good use.

Generics to the Rescue?

Note: It will take a few changes to get the project compiling again. Don’t panic if it doesn’t compile after every code change! :]

Still in MIFilter.swift, replace the implementation of Compose with the following:

// 1
public struct Compose<T: MIFilter, U: MIFilter>: MIFilter {
  public var name: String {
    return "Compose<\(type(of: first)), \(type(of: second))>"
  }
  public var id: String { self.name }

  // 2
  let first: T
  let second: U

  // 3
  public init(first: T, second: U) {
    self.first = first
    self.second = second
  }

  public func apply(to miImage: MIImage) -> MIImage {
    return second.apply(to: first.apply(to: miImage))
  }
}

Here’s what’s going on:

  1. First, this code updates the definition of the Compose struct to be generic over two types, T and U. Both of these must conform to MIFilter.
  2. Then, it updates the private properties first and second to have the generic types provided rather than the less specific type MIFilter.
  3. Finally, the code updates the initializer to accept parameters of the two generic types rather than any MIFilter.

Next, open MagicImage+UIImage.swift. Update the declaration of apply(_:)like so:

func apply<T: MIFilter>(_ filter: T) -> UIImage {

Like the previous change, you update apply(_:) to be generic, stating it accepts a filter with a concrete type T that conforms to MIFilter rather than any filter conforming to MIFilter.

Now change the scheme to MagicImage from MagicImageDemo:

Compiling the MagicImage Library

Build with Command-B. This will now build successfully. :]

Tape and String — Apply a Temporary Fix

Change the scheme back to MagicImageDemo.

The demo project won’t compile yet because it’s still trying to use the MIImage protocol as a type constraint without generics. You’ll fix this properly later, but for now the easiest way to get the app compiling is to update any type references from MIFilter to IdentityFilter.

Open FiltergramView.swift. Remove the typecast from the declaration of the selectedFilter state:

@State private var selectedFilter = IdentityFilter()

And underneath, update the type of the filters property:

let filters: [IdentityFilter]

Next, open FilterBarView.swift. Update the declaration of the selectedFilter binding to be typed as IdentityFilter as well:

@Binding var selectedFilter: IdentityFilter

And similarly with the type of the allFilters array:

let allFilters: [IdentityFilter]

Next, open FilterButton.swift and perform the same steps. Start with the type of the selectedFilter binding:

@Binding var selectedFilter: IdentityFilter

Then, update the filter property:

let filter: IdentityFilter

And now a drum roll for the bit you’ve been waiting for…

Open FiltergramView.swift again and add the following view modifier to the end of the body property:

.onChange(of: selectedFilter) { _ in loadImage() }

All the above changes were made in service of this one line! Now that MIFilter is Equatable, SwiftUI can compare two filters and call loadImage when the selectedFilter state changes.

Build and run the demo project. It will now compile correctly again.

App running with Equatable MIFilters

Terrible Types

Before you get too carried away, there are two problems with the generics-based approach you’ve built:

  1. The public API of your MagicImage library gives away too much internal detail. This makes it harder for you (or Liam) to change the internals of the library later without breaking code written by the users of your library.
  2. Although the demo app now compiles, it isn’t very useful. Why? Because the filters all need to have the same type! This won’t make for an interesting app at all.

Types are a pain

Before tackling the second point, you should explore why the first is so problematic.

Open FiltergramView.swift. Find the code in loadImage() that sets let uiImage and replace it with the following:

let oldeMirror = Compose(first: Olde(), second: HorizontalFlip())
let uiImage = inputImage.apply(oldeMirror)

OldeMirror is a filter created by combining the Olde and HorizontalFlip filters.

Option-click oldeMirror to open the inferred-type dialog.

Inferring the type of Olde Mirror

Notice how the type of oldeMirror is Compose<Olde, HorizontalFlip>. At first glance, this might seem just a little annoying. It’s a bit of a mouthful and isn’t a concise type for Abbie and other users of the MagicImage library to consume. But it gets worse.

Imagine Abbie really likes the Olde Mirror filter and wants to compose this filter with other filters as easily as possible. She might be tempted to write a function like so:

func composeWithOldeMirror(
  _ oldeMirror: MIFilter, 
  with newFilter: MIFilter
) -> MIFilter {
    return Compose(first: oldeMirror, second: newFilter)
}

But this won’t work. Why? Because it throws the same error you saw above:

Protocol 'MIFilter' can only be used as a generic constraint because it has Self or associated type requirements

To get this to work, Abbie would have to modify the function declaration to include the actual types, which looks a bit like this:

func composeWithOldeMirror<T: MIFilter>
(
  _ oldeMirror: Compose<Olde, HorizontalFlip>, 
  with newFilter: T
) -> Compose<Compose<Olde, HorizontalFlip>, T> {
    return Compose(first: oldeMirror, second: newFilter)
  }

It’s doubtful anyone would claim this code was easy to read. But the problem runs deeper than mere aesthetics.

Imagine Abbie is now running some performance analysis of MagicImage. She notices the Olde Mirror filter performs twice as fast if the Olde filter is applied after the flipping filter. So she changes the implementation of oldeMirror from:

let oldeMirror = Compose(first: Olde(), second: HorizontalFlip())

To:

let oldeMirror = Compose(first: HorizontalFlip(), second: Olde())

Abbie’s composeWithOldeMirror function will now fail to compile. Instead, she’ll see the following error:

Cannot convert value of type 'Compose<Olde, HorizontalFlip>' to expected argument type 'Compose<HorizontalFlip, Olde>'.

The problem is the Magic Image API forces users to care about internal types they don’t even need to know exist. The Compose type isn’t a part of the Magic Image library’s public interface but has leaked out regardless. As a library author, you’ve lost the flexibility that came with using protocols. Or have you?

Taming Types

Since version 5.1, Swift has supported a concept called opaque return types. A function (or method) with an opaque return type keeps the type secret from callers of the function. Instead, the function describes the return value in terms of the protocols it supports. So how is this different from just using a protocol?

It all comes down to what Corinne the Compiler sees. With protocols, the type secret is hidden from Abbie as well as Corinne. But when using opaque return types, the compiler gets access to the type secret as well.

  • 🤓 Liam the Library Author ✅
  • 🤖 Corinne the Compiler ✅
  • 🦸‍♀️Abbie the App Author ❌

You can think of opaque return types as “reverse generics”. Both features allow the compiler to know the type secret. But unlike generics (where Liam doesn’t know the secret), with opaque return types, it is Abbie who doesn’t know the secret.

In Swift, a function is declared as returning an opaque return type using the some keyword. For example, like this:

func selectedFilter() -> some MIFilter { ... }

An important distinction with functions returning an opaque type is that all branches in the function must return the same type.

For example, the following protocol-based code snippet will compile:

protocol Animal { }

struct Dog: Animal { }
struct Cat: Animal { }

func getFavouriteAnimal() -> Animal {
  if isDogLover {
    return Dog()
  }
  return Cat()
}

However, using opaque return types:

protocol Animal { }

struct Dog: Animal { }
struct Cat: Animal { }

// Update function to use an opaque return type
func getFavouriteAnimal() -> some Animal {
  if isDogLover {
    return Dog()
  }
  return Cat()
}

The code would fail with the following compiler error:

Function declares an opaque return type, but the return statements in its body do not have matching underlying types

This is because the Swift compiler needs the function to return a single type to “know” the type secret.

Hiding Filters

It’s time to use opaque return types in MagicImage. Open MIFilter.swift. In the Utility Functions section, add the following free function at the bottom of the file:

public func compose<T: MIFilter, U: MIFilter>(
  _ first: T, 
  _ second: U
) -> some MIFilter {
  Compose(first: first, second: second)
}

Here, you define a new function, compose, which wraps the existing Compose filter while returning an opaque type rather than the protocol returned by Compose.

Next, open FiltergramView.swift and update the implementation of oldeMirror in loadImage() so it’s defined like so:

let oldeMirror = compose(Olde(), HorizontalFlip())

If you Option-click oldeMirror, the inferred-type dialog shows its type as some MIFilter.

Inferring the type of Olde Mirror as an opaque return type

Note: If you see an error instead of the result above, build your project by pressing Command-B and try again.

At first glance, an opaque return type looks a lot like a protocol. But there are important differences.

An opaque return type refers to a single specific type. The caller of the function (Abbie) is not let in on the secret, but the compiler is.

With protocols, the return type could be any structure or object that conforms to the protocol. And neither Abbie nor Corrine the compiler learns the secret.

Before continuing, revert the changes to loadImage() so it uses the selectedFilter binding again:

func loadImage() {
  guard let inputImage = UIImage(named: "Butterfly") else { return }

  let uiImage = inputImage.apply(selectedFilter)
  image = Image(uiImage: uiImage)
}

Now you’re going to apply a new technique which will make the code even more beautiful.

Enter Type Erasure

Earlier, you changed the Filtergram app so the list of filters was an array of type [IdentityFilter]. This won’t let you provide different filters for the user to choose from. It’s time to fix that! :]

The problem is that the compiler requires you to specify the exact type of MIFilter in the filters array because MIFilter has a self-type requirement. But you want to store filters with different types in an array. To work around this, you’ll use a technique known as type erasure.

You likely have used type erasure before without realizing it. Apple uses it frequently in the Swift standard library. Examples include AnyView, which is a type erased View in SwiftUI, and AnyCancellable, which is a type erased Cancellable in Combine.

Unlike opaque return types, which are a feature of Swift, type erasure is a catch-all term for several techniques you can apply in any strongly typed language. Here, you’ll use a technique known as Boxing to type erase MIFilter.

The general idea is to create a concrete wrapper type (a box) that wraps either an instance of the wrapped type or any properties and methods of that type. Anytime a method is called on the wrapper type, it proxies the call to the wrapped type. Time to give it a go!

Create a new Swift file in the MagicImage Xcode project called AnyFilter.swift. Add the following code:

// 1
public struct AnyFilter: MIFilter {
  // 2
  public static func == (lhs: AnyFilter, rhs: AnyFilter) -> Bool {
    lhs.id == rhs.id
  }

  // 3
  public let name: String
  public var id: String { "AnyFilter(\(self.name))" }

  // 4
  private let wrappedApply: (MIImage) -> MIImage

  // 5
  public init<T: MIFilter>(_ filter: T) {
    name = filter.name
    wrappedApply = filter.apply(to:)
  }

  // 6
  public func apply(to miImage: MIImage) -> MIImage {
    return wrappedApply(miImage)
  }
}

In this code, you:

  1. Define a new structure called AnyFilter, which conforms to MIFilter.
  2. Implement ==, required for conformance to Equatable.
  3. Define name and id properties, as required by the MIFilter and Identifiable protocols, respectively.
  4. Define a property called wrappedApply, which is typed as a function receiving an MIImage and returning an MIImage. This is the same definition as the apply(to:) method defined in the MIFilter protocol.
  5. Create the default initializer for AnyFilter. You wrap the filter provided to the initializer by storing references to its name and apply(to:) method in the properties of AnyFilter.
  6. Finally, when the apply(to:) method of AnyFilter is called, you proxy the call to the apply(to:) method of the wrapped filter.

Now, anywhere in your code you want to erase the type of an MIFilter, you can simply wrap it in an AnyFilter, like so:

Before:

// Type is Posterize
let posterize = Posterize()

After:

// Type is AnyFilter
let posterize = AnyFilter(Posterize())

You might find it annoying to have to keep wrapping filters with AnyFilter(). In that case, a simple trick is to define a method asAnyFilter() in an extension of MIFilter. Add the following to AnyFilter.swift at the end of the file:

public extension MIFilter {
  func asAnyFilter() -> AnyFilter {
    return AnyFilter(self)
  }
}

When using AnyFilter in her app, Abbie prevents Liam and Corinne from seeing the underlying type of the wrapped filter. In the game of “Who knows the secret?”, type erasure is a bit like “reverse protocols”:

  • 🤓 Liam the library author ❌
  • 🤖 Corinne the Compiler ❌
  • 🦸‍♀️Abbie the App Author ✅
Note: SwiftUI provides a type-erased view via the AnyView structure. However, you should limit your use of AnyView as much as possible because SwiftUI’s view hierarchy diffing algorithm is significantly less efficient when dealing with AnyView. Using AnyView too often will create performance problems for your app.

Finishing Filtergram

It’s time to put AnyFilter to good use.

⚠️ Like when you updated all the types to [IdentityFilter] earlier, you’ll need to make several changes before the demo app will compile. Don’t panic!

Open FiltergramView.swift. Update the definition of the selected filter state:

@State private var selectedFilter = IdentityFilter().asAnyFilter()

Along with the type of the filters array:

let filters: [AnyFilter]

And replace the contents of the initializer with the following:

let identity = IdentityFilter()
let sepia = Sepia()
let olde = Olde()
let posterize = Posterize()
let crystallize = Crystallize()
let flipHorizontally = HorizontalFlip()

filters = [
  identity.asAnyFilter(),
  sepia.asAnyFilter(),
  olde.asAnyFilter(),
  AnyFilter(posterize),
  AnyFilter(crystallize),
  AnyFilter(flipHorizontally)
]

Here, you define some filters and add them to the filters array. Note how it doesn’t matter if you use .asAnyFilter() or the AnyFilter initializer. Feel free to use whichever you prefer.

Next, open FilterBarView.swift and update the type for the selectedFilter binding:

@Binding var selectedFilter: AnyFilter

Next, update the type for the allFilters array.:

let allFilters: [AnyFilter]

And the same for the selectedFilter and filters values in the preview:

let selectedFilter = IdentityFilter().asAnyFilter()
let filters: [AnyFilter] = [
  IdentityFilter().asAnyFilter()
]

Next, open FilterButton.swift and update the type of the SelectedFilter binding:

@Binding var selectedFilter: AnyFilter

As well as the Filter property:

let filter: AnyFilter

In the body property, update the definition of isSelectedFilter:

let isSelectedFilter = selectedFilter == AnyFilter(filter)

Here, you use the Identifiable protocol to update the look of the button when the filter it represents is selected.

Also, update FilterButton in the preview to type erase the selected filter and filter to AnyFilter:

FilterButton(
  selectedFilter: .constant(IdentityFilter().asAnyFilter()),
  filter: IdentityFilter().asAnyFilter())

Build and run. The app now compiles without any errors.

The final app with multiple filters

The app now has a list of filter buttons along the bottom of the screen. You were able to store filters of different types in the same array by erasing the type of each filter.

Woo-hoo! Tap the buttons to change the filter applied to the butterfly image.

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of this tutorial.

You’ve successfully used opaque return types in the Magic Image library and seen how you can apply type erasure in the Filtergram app to hide concrete types from the compiler.

Along with generics and protocols, opaque return types and type erasure allow you to hide the “secret” type information from different parties. As a reminder:

Both opaque return types and type erasure are advanced topics you might not use regularly. But they’re also both used extensively in common Swift frameworks like SwiftUI and Combine, so understanding what they are and why they’re necessary is valuable for every budding Swift developer.

You can learn more about opaque return types in the Swift documentation. Although you might not define functions that return opaque types often, you’ll use them every time you create a view with SwiftUI.

Apple doesn’t cover type erasure specifically anywhere in its official documents because it’s not a feature of the language per se. But it’s a technique that’s used heavily in the standard library. When using Combine, you’ll come across AnyCancellable and AnyPublisher frequently in the APIs. SwiftUI provides AnyView to allow you to type erase views.

We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!

Average Rating

4.9/5

Add a rating for this content

8 ratings

More like this

Contributors

Comments