← All daysDay 2 of 49 · Part 1: Modern Swift: what changed since 2016
Day 2

Build the pieces of a tiny UI framework

Lesson 2: Opaque types and existentials: some vs any

You will buildA Widget protocol, three widgets that draw themselves as text, and a boxed card printed in the console.

TimeAbout 80 minutes

WhyEvery SwiftUI view has var body: some View. Today you find out what some means — and why it is not any — by building a small framework that needs it. You will hit the same compiler error that every SwiftUI beginner hits, and you will know exactly why it happens.

Step 1

Set up the project in Xcode

Over today and tomorrow you build TinyUI, a miniature SwiftUI that draws with text in the console. Today's project holds its widgets. It is a new project, set up from scratch in the same way as yesterday's:

  1. Open Xcode and choose File ▸ New ▸ Project… (⇧⌘N). If you are looking at the Welcome to Xcode window instead, its New Project menu offers the same choices.

  2. Xcode 27 shows a short list of project kinds. Click Choose Template.

  3. A sheet titled Choose a template for your new project appears. In the row of platforms at the top, click macOS. Under Application, select Command Line Tool. Click Next.

  4. Fill in the options. Product Name: Widgets. Team: leave it as it is — a command-line program does not need one. Organization Identifier: the one you normally use, such as com.yourname. Language: Swift. Click Next.

  5. Choose the folder to keep the project in — your Development folder, for example. Tick Create Git repository on my Mac or not, as you prefer. Click Create.

  6. The project window opens. In the Project navigator on the left, open the Widgets folder and click main.swift. Below a comment that gives the file's name and today's date, Xcode has written:

    main.swift, as Xcode made it
    import Foundation
    
    print("Hello, World!")
  7. Check that the run destination in the toolbar says My Mac, then choose Product ▸ Run (⌘R). The debug area opens at the bottom of the window, and its console shows:

    Console
    Hello, World!
    Program ended with exit code: 0

    The last line comes from Xcode, not from the program: it says the program has finished, and 0 means it finished without an error. If you cannot see the console, choose View ▸ Debug Area ▸ Activate Console (⇧⌘C).

  8. Now add a second file. With main.swift still selected, choose File ▸ New ▸ File from Template… (⌘N). Under Source, select Swift File. Click Next.

  9. Name it Widgets, leave the other settings as they are, and click Create. The file appears beside main.swift and opens with a comment and one line of code: import Foundation. Leave that line where it is.

As yesterday: types go in Widgets.swift, and the code that tries them out goes in main.swift, below import Foundation.

Step 2

A protocol for things that can draw themselves

In TinyUI, a widget is anything that can turn itself into lines of text. That is the whole protocol. Text is the simplest widget: it renders as one line.

Add to Widgets.swift
protocol Widget {
    func render() -> [String]          // a widget draws itself as lines of text
}

struct Text: Widget {
    let string: String
    init(_ string: String) { self.string = string }

    func render() -> [String] { [string] }
}

func show(_ widget: some Widget) {
    print(widget.render().joined(separator: "\n"))
}
In main.swift, replace everything below import Foundation with
show(Text("Seattle"))

Run it (⌘R). The console shows

Console
Seattle
Program ended with exit code: 0
Step 3

Containers: Column and Bordered

A container is a widget that holds other widgets. Column holds two and puts one above the other. Bordered holds one and draws a box around it.

Add to Widgets.swift
struct Column<Top: Widget, Bottom: Widget>: Widget {
    let top: Top
    let bottom: Bottom
    init(_ top: Top, _ bottom: Bottom) {
        self.top = top
        self.bottom = bottom
    }

    func render() -> [String] { top.render() + bottom.render() }
}
Add to Widgets.swift
struct Bordered<Content: Widget>: Widget {
    let content: Content
    init(_ content: Content) { self.content = content }

    func render() -> [String] {
        let lines = content.render()
        let width = lines.map(\.count).max() ?? 0
        let edge = "+" + String(repeating: "-", count: width + 2) + "+"
        let rows = lines.map { "| " + $0.padding(toLength: width, withPad: " ", startingAt: 0) + " |" }
        return [edge] + rows + [edge]
    }
}
In main.swift, replace everything below import Foundation with
show(Bordered(Column(Text("Seattle"), Text("Aug 22 - Sep 3"))))

Run it (⌘R). The console shows

Console
+----------------+
| Seattle        |
| Aug 22 - Sep 3 |
+----------------+
Program ended with exit code: 0

Notice the angle brackets. Bordered<Content: Widget> is generic: it works with any content that is a Widget, and the type of that content becomes part of Bordered's own type. That detail is about to matter.

Step 4

A function that returns a widget

Move the card into a function. A function has to declare what type it returns, so write it out:

Add to Widgets.swift
func tripCard() -> Bordered<Column<Text, Text>> {
    Bordered(Column(Text("Seattle"), Text("Aug 22 - Sep 3")))
}
In main.swift, replace everything below import Foundation with
let card = tripCard()
show(card)
print(type(of: card))

Run it (⌘R). The console shows

Console
+----------------+
| Seattle        |
| Aug 22 - Sep 3 |
+----------------+
Bordered<Column<Text, Text>>
Program ended with exit code: 0

The last line of output is the type: Bordered<Column<Text, Text>>. The type records the whole structure of the card. That is useful to the compiler, and unpleasant for you: every time you change what is inside the card, you must rewrite the return type to match. With ten nested widgets it would be unreadable.

Step 5

some Widget: let the compiler keep track of the type

Change one thing: the return type.

In Widgets.swift, replace tripCard() with this
func tripCard() -> some Widget {
    Bordered(Column(Text("Seattle"), Text("Aug 22 - Sep 3")))
}
In main.swift, replace everything below import Foundation with
let card = tripCard()
show(card)
print(type(of: card))

Run it (⌘R). The console shows

Console
+----------------+
| Seattle        |
| Aug 22 - Sep 3 |
+----------------+
Bordered<Column<Text, Text>>
Program ended with exit code: 0

The output is identical, including the type on the last line. some Widget did not change what the function returns. It still returns exactly a Bordered<Column<Text, Text>>, and the compiler still knows that. You simply no longer have to write it.

This is called an opaque type. The function promises: "I return one specific type that is a Widget. I know which. You do not need to." Code that calls the function cannot depend on the hidden type:

Try this in main.swift instead, below import Foundation
let card: Bordered<Column<Text, Text>> = tripCard()
show(card)

It does not compile. Xcode reports

cannot convert value of type 'some Widget' to specified type 'Bordered<Column<Text, Text>>'

Undo that change (⌘Z), so that main.swift is back to the three lines that ran. The point is made: the type is hidden from callers, not erased.

Step 6

The rule you will meet in SwiftUI: one type only

Now write a function that returns a different widget depending on a condition:

Add to Widgets.swift
func badge(isNew: Bool) -> some Widget {
    if isNew {
        return Text("NEW")
    } else {
        return Bordered(Text("seen"))
    }
}

It does not compile. Xcode reports

function declares an opaque return type 'some Widget', but the return statements in its body do not have matching underlying types

Read that message carefully, because you will see it in SwiftUI with the word View in place of Widget. some Widget means one type. This function tries to return a Text on one path and a Bordered<Text> on the other. Those are two types, and the promise is broken.

There are three ways out. Each one is a real technique that SwiftUI uses, and the next three steps try them in turn.

Step 7

Way out 1: any Widget

Change some to any:

In Widgets.swift, replace badge(isNew:) with this
func badge(isNew: Bool) -> any Widget {
    if isNew {
        return Text("NEW")
    } else {
        return Bordered(Text("seen"))
    }
}
In main.swift, replace everything below import Foundation with
show(badge(isNew: true))
show(badge(isNew: false))
print(type(of: badge(isNew: true)), type(of: badge(isNew: false)))

Run it (⌘R). The console shows

Console
NEW
+------+
| seen |
+------+
Text Bordered<Text>
Program ended with exit code: 0

It compiles, and the last line shows why: the two calls really did return values of two different types. any Widget is a box that can hold a value of any type that conforms to Widget. Which type is inside is only known while the program runs.

That flexibility has a price. Try to put the result in a border:

Try this in main.swift instead, below import Foundation
show(Bordered(badge(isNew: true)))

It does not compile. Xcode reports

type 'any Widget' cannot conform to 'Widget'

Bordered is generic: it needs to know the exact type of its content when the code is compiled. A box whose contents are only known later cannot tell it. (Calling show worked because Swift can open the box for the length of a single function call. It cannot do that when the type would have to become part of another type.)

Step 8

Way out 2: erase the type

You can write a concrete type whose job is to hide another widget inside it. It holds on to the widget's render function and forgets everything else:

Add to Widgets.swift
struct AnyWidget: Widget {
    private let renderer: () -> [String]
    init(_ widget: some Widget) { renderer = widget.render }

    func render() -> [String] { renderer() }
}
In Widgets.swift, replace badge(isNew:) with this
func badge(isNew: Bool) -> AnyWidget {
    if isNew {
        return AnyWidget(Text("NEW"))
    } else {
        return AnyWidget(Bordered(Text("seen")))
    }
}
In main.swift, replace everything below import Foundation with
show(Bordered(badge(isNew: true)))
print(type(of: badge(isNew: true)), type(of: badge(isNew: false)))

Run it (⌘R). The console shows

Console
+-----+
| NEW |
+-----+
AnyWidget AnyWidget
Program ended with exit code: 0

Now Bordered is happy, because AnyWidget is one concrete type. But look at the last line: both badges report the type AnyWidget. The information about what is inside has been thrown away.

This is `AnyView`

SwiftUI has exactly this type, called AnyView, and its documentation advises against using it freely. Now you can see why: SwiftUI compares the type of your view tree before and after a change to work out what to update. Erase the types and it can no longer tell what changed.

Step 9

Way out 3: one type that can hold either

The third way keeps all the type information. Define a single type with two cases, one for each possibility:

Add to Widgets.swift
enum Either<First: Widget, Second: Widget>: Widget {
    case first(First)
    case second(Second)

    func render() -> [String] {
        switch self {
        case .first(let widget):  widget.render()
        case .second(let widget): widget.render()
        }
    }
}
In Widgets.swift, replace badge(isNew:) with this
func badge(isNew: Bool) -> some Widget {
    if isNew {
        return Either<Text, Bordered<Text>>.first(Text("NEW"))
    } else {
        return Either<Text, Bordered<Text>>.second(Bordered(Text("seen")))
    }
}
In main.swift, replace everything below import Foundation with
show(badge(isNew: true))
show(Bordered(badge(isNew: false)))
print(type(of: badge(isNew: true)))

Run it (⌘R). The console shows

Console
NEW
+----------+
| +------+ |
| | seen | |
| +------+ |
+----------+
Either<Text, Bordered<Text>>
Program ended with exit code: 0

The function returns some Widget again, and the promise holds, because both branches return the same type: Either<Text, Bordered<Text>>. The type spells out both possibilities, so nothing is lost.

SwiftUI does precisely this. Its version is called _ConditionalContent, and it is what an if/else inside a view's body turns into. You never write it yourself — tomorrow you will build the feature that writes it for you.

Step 10

some in a parameter

Look back at the first function you wrote today: func show(_ widget: some Widget). That is some used in a parameter. There it is shorthand for a generic function, func show<W: Widget>(_ widget: W). The two mean the same thing.

So, in one sentence each. In a return type, some means "one specific type, chosen by the function, which the caller is not told". In a parameter, it means "any one type the caller chooses". And any means "a box that can hold a value of any conforming type, decided while the program runs".

Keep today's project. Tomorrow's builds on Widgets.swift.

What you built today

Tomorrow you give TinyUI SwiftUI's syntax: blocks of widgets with no commas, if and for inside them, and a body property. Then you run the same card in real SwiftUI.

The finished project

Stuck, or want to compare with your own? This is the project as it stands at the end of today, built and run before it was put here.

Flash cards

13 cards for today, each labelled with its topic. 7 ask how you would write something; the other 6 are trick questions, written to catch a misunderstanding. Answer in your head first, then tap the card.

Syntax · Opaque typesWrite a function whose return type is hidden from callers but fixed.
func makeCard() -> some Widget {
    Bordered(Text("Seattle"))
}

some Widget: one concrete type, chosen by the function and known to the compiler.

Syntax · ExistentialsWrite a function that can return values of different conforming types.
func pick(framed: Bool) -> any Widget {
    if framed {
        return Bordered(Text("Seattle"))
    } else {
        return Text("Seattle")
    }
}

any Widget: a box that holds a value of some conforming type, decided while the program runs.

Syntax · Opaque typesWrite some in a parameter — and the longer generic form it stands for.
func display(_ widget: some Widget) {
    print(widget.render())
}

func displayLonghand<W: Widget>(_ widget: W) {
    print(widget.render())
}

In a parameter, some Widget is shorthand for a generic parameter constrained to Widget. The two functions are equivalent.

Syntax · GenericsDeclare a generic container whose content must be a Widget.
struct Indented<Content: Widget>: Widget {
    let content: Content

    func render() -> [String] {
        content.render().map { "    " + $0 }
    }
}

Write <Content: Widget> after the type's name. The container's full type then includes the type of its content: Indented<Text>.

Syntax · Type erasureWrite a type-erasing wrapper.
struct AnyWidget: Widget {
    private let renderer: () -> [String]
    init(_ widget: some Widget) { renderer = widget.render }

    func render() -> [String] { renderer() }
}

Keep a closure that captures what the original value does, and forget what type it was. SwiftUI's version is AnyView.

Syntax · ExistentialsStore widgets of different types in one array.
let widgets: [any Widget] = [Text("Seattle"), Bordered(Text("Vancouver"))]
for widget in widgets {
    show(widget)
}

An array has a single element type, so use any Widget. This is a case where any is the right tool.

Syntax · Opaque typesPrint the concrete type of a value.
print(type(of: tripCard()))

Use type(of:). It reports the real type, even for a value returned as some Widget. Here it prints Bordered<Column<Text, Text>>.

Trick question · Opaque typesa() and b() are two functions, each declared -> some Widget, and each returns a Text. Does [a(), b()] compile?
func a() -> some Widget { Text("A") }
func b() -> some Widget { Text("B") }

let both = [a(), b()]
print(both.count)

No. heterogeneous collection literal could only be inferred to '[Any]'; add explicit type annotation if this is intentional Every function has its own opaque type. The compiler treats the two as different types even though both are secretly Text, because it has promised callers nothing about what is inside. [a(), a()] does compile.

Trick question · Opaque typesDoes some Widget mean "any type that conforms to Widget"?

No — that describes any Widget. some Widget means one particular type, picked by the function and fixed when the code is compiled. The caller simply is not told which one.

Trick question · ExistentialsA value of type any Widget holds a widget, so Bordered(thatValue) should compile. Does it?

No. type 'any Widget' cannot conform to 'Widget' Bordered has to know the concrete type of its content when the code is compiled, and an any Widget box hides it. Calling show(thatValue) does work, because Swift can open the box for the length of a single function call.

Trick question · Opaque typesA function returns some Widget. One branch returns Text("NEW"), the other Text("old"). Does it compile?
func label(isNew: Bool) -> some Widget {
    if isNew {
        return Text("NEW")
    } else {
        return Text("old")
    }
}

show(label(isNew: false))

Yes. The rule is one type, not one value. Both branches return a Text.

Trick question · Opaque typesWhat does type(of:) print for a value returned as some Widget — the words "some Widget"?

No. It prints the real type: Bordered<Column<Text, Text>>. An opaque type hides the type from the code that calls the function. It does not erase it. While the program runs, the value is exactly what it always was.

Trick question · Type erasureAfter you wrap a value in AnyWidget, can type(of:) still tell you what was inside?

No. It prints AnyWidget. That is what erasing a type means — and it is why SwiftUI discourages AnyView: once the type is gone, SwiftUI can no longer see what changed.

Further reading