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.
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.
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.
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.
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 ❌
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:
- Define a new filter called
Olde
, which conforms to theMIFilter
protocol. - Give the filter a name and an ID. In this case, you use the name as the ID.
- Write the apply method. This method creates a Core Image
sepiaTone
filter and applies the filter to the providedMIImage
before returning the output from the filter. - The
Posterize
andCrystallize
filters follow the same pattern, albeit with different Core Image filters. - Finally, the
HorizontalFlip
flip filter uses the built-inoriented(_:)
method ofCIImage
(rememberingMIImage
is just a typealias forCIImage
) 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())
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:
- Defines a method,
fetch(completion:)
, which receives a completion handler that’s called with the result of the fetch — in this case, an array ofDataType
. - 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 namedDataType
, 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?
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:
- First, this code updates the definition of the
Compose
struct to be generic over two types,T
andU
. Both of these must conform toMIFilter
. - Then, it updates the private properties
first
andsecond
to have the generic types provided rather than the less specific typeMIFilter
. - 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:
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.
Terrible Types
Before you get too carried away, there are two problems with the generics-based approach you’ve built:
- 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.
- 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.
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.
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
.
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:
- Define a new structure called
AnyFilter
, which conforms toMIFilter
. - Implement
==
, required for conformance toEquatable
. - Define
name
andid
properties, as required by theMIFilter
andIdentifiable
protocols, respectively. - Define a property called
wrappedApply
, which is typed as a function receiving anMIImage
and returning anMIImage
. This is the same definition as theapply(to:)
method defined in theMIFilter
protocol. - Create the default initializer for
AnyFilter
. You wrap the filter provided to the initializer by storing references to its name andapply(to:)
method in the properties ofAnyFilter
. - Finally, when the
apply(to:)
method ofAnyFilter
is called, you proxy the call to theapply(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 ✅
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 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!
Comments