What’s this “some” in SwiftUI?

An Intuitive Explanation of The New Opaque Result Type

Mischa Hildebrand
11 min readJun 8, 2019

At this year’s WWDC, Apple introduced SwiftUI — a completely new approach to create user interfaces in Xcode in a declarative way. To help you get started with the new framework, they published some beautifully designed tutorials.

💡 Some advice upfront:

If you get stuck in the first tutorial in section 1, it’s probably for either one (or both) of the following reasons:

  1. You need the beta of the new macOS 10.15 (Catalina) for all SwiftUI features to work properly. (In particular, the canvas won’t work without it.) The latest Xcode 11 beta is not enough. ⚠️
  2. Don’t despair when you can’t figure out how to “create a new Xcode project using the SwiftUI app template”. It’s not in the template selection. It’s a check box you have to tick in the next step. Turns out you can actually scroll the tutorial page and you get all the necessary step-by-step instructions below. 😱

What’s "some" thing?

The next thing you’ll notice is that new keyword some which was introduced in Swift 5.1. That might be confusing in the beginning. After all, a computed property always returns some value of a certain type, right?!

Apparently, that some thing does something and that something is related to a concept called opaque types. Adding the keyword some in front of a return type indicates that the return type is opaque.

What is “opaque”?

Opaque types are frequently referred to as reverse generic types. But that’s kind of hard to understand without any more explanation. So let’s try to go step-by-step and recall generics first.

Generics

Generic types are basically placeholders you can use when you want to declare functions that work with multiple types. A good example is the max function in Swift, which returns the maximum of the two input parameters, which are of an unknown type T.

func max<T>(_ x: T, _ y: T) -> T

This declaration states that both input parameters must have the same type T, but which type exactly that is remains unknown. Unfortunately, without any information about the parameters’ type, we can’t do much with them in the function’s body. That’s why we use protocols to describe (or constrain) what a generic type can do without exposing its actual type.

func max<T>(_ x: T, _ y: T) -> T where T: Comparable

The protocol here is Comparable which requires that every type conforming to it must implement the comparison operator:

public protocol Comparable : Equatable {   static func < (lhs: Self, rhs: Self) -> Bool   // ...}

With the generic constraint T: Comparable we still don’t know what exactly T is, but we do know that we can compare two instances of T with the > operator. Thus, we can implement the max function.

func max<T>(_ x: T, _ y: T) -> T where T: Comparable {
if y > x {
return y
} else {
return x
}
}

Generic types hide the actual type of a value within a function’s implementation. From outside, where you call the function, you always know the type of the values you pass as parameters and you know the return type. (Even if you don’t because you got all confused, the Swift compiler knows and infers the type according to the function signature. 😉)

For example, we can pass two integers to the max function.

let x: Int = 1
let y: Int = 701
let maximum = max(x, y) // type: Int

As a result, the type of the return value maximum is of type Int as well, according to the function signature max<T>(_ x: T, y: T) -> T.

Opaque Types

Opaque types are the reverse in that the outside world doesn’t exactly know the type of a function’s return value. But inside the function’s implementation, you do know exactly what concrete types you’re dealing with.

An opaque type is like Kinder surprise eggs.

As I’ve recently learned that these candy eggs are banned in the U.S., I’ll quickly describe how they look like, though you should already have a good impression from the image.

From the outside, all Kinder surprise eggs are the same and share some properties. Every egg

  • is wrapped in aluminum foil, colored red and white, with a logo imprint,
  • has a chocolate eggshell consisting of a brown and a white layer and
  • contains a little yellow plastic container with a surprise inside

However, what’s inside this yellow plastic container varies from egg to egg. Thus, we can classify the eggs by their content type. It might be

  • a little puzzle,
  • a miniature toy or
  • a solid plastic figure.

Let’s model this in code! First, we define a protocol for all surprise eggs as follows:

protocol SurpriseEgg {    associatedType ContentType    var foil: Foil { get }
var chocolateShell: Chocolate { get }
var container: Container { get }
var content: ContentType { get }
}

Next, we create the concrete types which implement that protocol:

struct PuzzleEgg: SurpriseEgg {
var content: Puzzle
// ...
}
struct ToyEgg: SurpriseEgg {
var content: MiniatureToy
// ...
}
struct FigureEgg: SurpriseEgg {
var content: PlasticFigure
// ...
}

Notice how the associated type ContentType specified in the protocol abstracts the specific type of the content. By substituting this placeholder with the actual content type, we fill in the gap and make the type concrete.

Now imagine you’re a shop owner who sells these Kinder surprise eggs. Every week you purchase three pallets of them directly from the manufacturer and sell them to your customers. You don’t really care what’s inside these eggs and you really can’t tell, because you can’t look inside. In Swift terminology: You don’t know any of the eggs’ concrete type. If you pick a particular egg from the pallets, it could have any of the following concrete types:

PuzzleEgg
ToyEgg
FigureEgg

What you do know is that it’s definitely a Kinder Surprise egg, i.e. it conforms to the SurpriseEgg protocol. So if you had a function for picking a random egg from the pallet, it would need to have this protocol as its return type:

func pickRandomEgg() -> SurpriseEgg 

ℹ️ Note: This wouldn’t compile in Swift and I’ll get back to that in a minute. Just assume for a moment, it would compile.

Now, let’s assume in a second scenario the manufacturer had sorted the eggs by their concrete type and all eggs on one pallet would be of the same type. You still don’t know the concrete type of each pallet, but you do know that as long as you pick eggs from the same pallet, their concrete type will always be the same.

That’s exactly what the some keyword does in code:

func pickEggFromPallet1() -> some SurpriseEgg

Adding this keyword turns the return type into an opaque type which means that you and the compiler both know that this function will always return only one particular concrete type — you’ll just never know which one!

And that’s the difference between returning a protocol type (SurpriseEgg) and an opaque type (some SurpriseEgg) from a function: If we only used the protocol, the function could (possibly) return a different concrete type each time you call it — sometimes you’ll get an egg with a plastic figure, sometimes you’ll get one with a puzzle, sometimes one with a miniature toy. If we use an opaque result type instead, we enforce that the function will always return the same concrete type, e.g. it might always return a puzzle, the opaque type just hides the concrete type’s identity from the caller.

What does it help? 🤔

Well, if we don’t know what exactly the type of our value is, what’s the point of knowing that it’s “concrete” now? Certainly, we cannot use any functions or properties specific to the concrete type!

Yes, that’s true. However, there are two subtle, yet important things that suddenly become possible with opaque types:

1. Return Protocol Types With Associated Types

We can now return “generic” protocol types from a function (or a computed property) — or more precisely: protocols with associated types (also known as PATs).

If you’ve been working with protocols in Swift for some time, you’ll most likely have come across this compiler error:

Protocol ‘SurpriseEgg’ can only be used as a generic constraint because it has Self or associated type requirements.

My guess is that 95% of all Swift developers don’t understand what this means. 🤯 Fortunately, there is a nice post on Stackoverflow and another one on Paul Hudson’s blog that explain what’s going on in detail, but it’s still not simple to grasp. Swift doesn’t allow us to use protocols with associated types as return types.

We can use “regular” protocols, for example, we could define a Chocolate protocol as follows,

protocol Chocolate {
var weight: Float { get }
func eat()
}

and use that protocol as a return type of a function,

func giveMeChocolate() -> Chocolate {
return RitterSport()
}

(assuming that the type RitterSport conforms to the Chocolate protocol). We create an instance of the concrete type RitterSport here, but because we have specified the protocol Chocolate as the function’s return type, that information — more technically spoken: the chocolate’s type identity — gets lost the moment the value is returned.

However, we cannot do that with a protocol that has associated types. Let’s look at the example from above again:

protocol SurpriseEgg {    associatedType ContentType    var foil: Foil { get }
var chocolateShell: Chocolate { get }
var container: Container { get }
var content: ContentType { get }
}

We cannot use this protocol as a return type. As mentioned before, the following declaration wouldn’t compile and throw our “beloved” error:

func pickEgg() -> SurpriseEgg           // 💥 Error

Why? Because the compiler cannot infer the associated type from this definition and the return type would be incomplete. However, when we add the some keyword and return an opaque type instead, the function will compile:

func pickEgg() -> some SurpriseEgg     // ✅ Compiles

That’s because with opaque types, the compiler remembers the concrete type of the return value under the hood. It knows the concrete type’s identity. As a result, you can use two (or more) opaque values that were returned from the same function in another generic function that requires two or more input parameters of the same type. That’s what the next section is all about.

2. Compose Opaque Return Values With Generic Functions

If we wanted to swap the position of two surprise eggs with the same concrete type on the pallet, we could define a generic function like this:

func swapEggsOfSameType<T: SurpriseEgg>(first: T, second: T)

Calling this function requires not only that the first and the second parameter are of a type that implements the protocol SurpriseEgg, but that type needs to be the same for both parameters. Thus, despite the fact that FigureEgg and PuzzleEgg both conform to the protocol SurpriseEgg, the following code would not compile:

let figureEgg = FigureEgg()              // type: FigureEgg
let puzzleEgg = PuzzleEgg() // type: PuzzleEgg
swapEggsOfSameType(figureEgg, puzzleEgg) // 💥 Error: different type

Even if we could use the protocol SurpriseEgg as return type of the pickEgg() function and both eggs where typed as SurpriseEgg rather than as their concrete type, it still would not be possible to call the swapEggsOfSameType function with those eggs as parameters, precisely because they might not be of the same (concrete) type.

func pickEgg() -> SurpriseEgg { ... }   //  1️⃣💥 Errorlet oneEgg     = pickEgg()              // type: SurpriseEgg
let anotherEgg = pickEgg() // type: SurpriseEgg
swapEggsOfSameType(oneEgg, anotherEgg) // 2️⃣💥 Error

For example, oneEgg might be a FigureEgg and anotherEgg might be a PuzzleEgg. That’s why the compiler cannot allow the function call in the last line and throws an error.

If we use an opaque type instead, the return type’s identity is maintained under the hood:

func pickEgg() -> some SurpriseEgg { ... }let oneEgg     = pickEgg()   // type: SurpriseEgg of specific type X
let anotherEgg = pickEgg() // type: SurpriseEgg of the same type X
swapEggsOfSameType(oneEgg, anotherEgg) // ✅ compiles

The compiler knows that oneEgg has the same concrete type as anotherEgg, thus it allows us to pass them as parameters to the swapEggsOfSameType function.

A common example where this is of the uttermost importance is when you want to compare two return values with each other. You can only compare types that conform to the Equatable protocol which requires the definition of the == operator:

protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}

Self is a placeholder, similar to an associated type, and always refers to the concrete type that implements the protocol. Let’s get back to our Chocolate example from above and make it Equatable:

protocol Chocolate: Equatable {
var weight: Float { get }
func eat()
}

If we now get cravings and call the giveMeChocolate function twice,

func giveMeChocolate() -> Chocolate {
return RitterSport()
}
let someChocolate = giveMeChocolate()
let someMoreChocolate = giveMeChocolate()

both chocolate return values someChocolate and someMoreChocolate are equatable, but we still cannot compare them because they could be of a different concrete type:

if someChocolate == someMoreChocolate {          // 💥 Error
print("Give me something different! 🍫")
}

Again, if we return an opaque type from the giveMeChocolate function instead, this comparison suddenly becomes possible:

func giveMeChocolate() -> some Chocolate {
return RitterSport()
}
let someChocolate = giveMeChocolate()
let someMoreChocolate = giveMeChocolate()
if someChocolate == someMoreChocolate { // ✅ compiles
print("Give me something different! 🍫")
}

Opaque Types in SwiftUI

Now, that we’re all craving for chocolate and Kinder surprise eggs, the only remaining question is: Why does Apple use an opaque result type for the body property of their views in all of the SwiftUI tutorials?

First of all, that’s not a requirement. Theoretically, you could always return the concrete type of your view instead of using the opaque some View type. The following code will compile:

struct ContentView: View {
var body: Text {
Text("Hello World")
}
}

However, the power of SwiftUI lies in its composability. Usually, you would use stack views and other containers like lists to compose your layout from other views. These containers are all generic types and their concrete type changes every time you add, remove, move or replace a view. For example, let’s have a look on the following simple custom view:

struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
Image(systemName: "video.fill")
}
}
}

The type of the VStack that contains the text and the image is inferred to be

VStack<TupleView<(Text, Image)>>

If we want to change the layout of our view by adding a second text right below the existing text, the stack view’s type would change as follows:

VStack<TupleView<(Text, Text, Image)>>

So if we use the concrete type of the view that we return from our body property, we would have to adjust that type manually every time we change the layout. In the example above, we would go from

var body: VStack<TupleView<(Text, Image)>> { ... }

to

var body: VStack<TupleView<(Text, Text, Image)>> { ... }

which can already be annoying. But most likely, we will make use of multiple containers. We might want to nest a stack in another stack, and maybe that other stack in a list. Suddenly, our body’s concrete type looks like this: 🤯

List<Never, TupleView<(HStack<TupleView<(VStack<TupleView<(Text, Text)>>, Text)>>, HStack<TupleView<(VStack<TupleView<(Text, Text)>>, Text)>>)>>

You’ll probably agree that this doesn’t make any sense. Updating this type when you change a single view inside the hierarchy is incredibly tedious. On top of that, the type isn’t readable at all. Our minds cannot process it in a reasonable time and all we care about is actually that the body returns some View.

That’s precisely why Apple uses an opaque view type in their tutorials and why you should always do the same:

var body: some View { ... }

Thanks for Reading! ✌️

Phew, you’ve made it! 👏
If you have any questions or corrections, please leave a comment below. I always announce new articles via my Twitter account
@DerHildebrand.

--

--

Mischa Hildebrand

iOS Developer, Radio Journalist, Physicist. Trying to make the world a better place with loads of positive vibes. 🤗