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.
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:
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.
Xcode 27 shows a short list of project kinds. Click Choose Template.
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.
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.
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.
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 itimport Foundation
print("Hello, World!")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:
Hello, World!
Program ended with exit code: 0The 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).
Now add a second file. With main.swift still selected, choose File ▸ New ▸ File from Template… (⌘N). Under Source, select Swift File. Click Next.
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.
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.
Widgets.swiftprotocol 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"))
}
main.swift, replace everything below import Foundation withshow(Text("Seattle"))
Run it (⌘R). The console shows
Seattle
Program ended with exit code: 0Column and BorderedA 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.
Widgets.swiftstruct 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() }
}
Widgets.swiftstruct 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]
}
}
main.swift, replace everything below import Foundation withshow(Bordered(Column(Text("Seattle"), Text("Aug 22 - Sep 3"))))
Run it (⌘R). The console shows
+----------------+
| Seattle |
| Aug 22 - Sep 3 |
+----------------+
Program ended with exit code: 0Notice 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.
Move the card into a function. A function has to declare what type it returns, so write it out:
Widgets.swiftfunc tripCard() -> Bordered<Column<Text, Text>> {
Bordered(Column(Text("Seattle"), Text("Aug 22 - Sep 3")))
}
main.swift, replace everything below import Foundation withlet card = tripCard()
show(card)
print(type(of: card))
Run it (⌘R). The console shows
+----------------+
| Seattle |
| Aug 22 - Sep 3 |
+----------------+
Bordered<Column<Text, Text>>
Program ended with exit code: 0The 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.
some Widget: let the compiler keep track of the typeChange one thing: the return type.
Widgets.swift, replace tripCard() with thisfunc tripCard() -> some Widget {
Bordered(Column(Text("Seattle"), Text("Aug 22 - Sep 3")))
}
main.swift, replace everything below import Foundation withlet card = tripCard()
show(card)
print(type(of: card))
Run it (⌘R). The console shows
+----------------+
| Seattle |
| Aug 22 - Sep 3 |
+----------------+
Bordered<Column<Text, Text>>
Program ended with exit code: 0The 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:
main.swift instead, below import Foundationlet 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.
Now write a function that returns a different widget depending on a condition:
Widgets.swiftfunc 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.
any WidgetChange some to any:
Widgets.swift, replace badge(isNew:) with thisfunc badge(isNew: Bool) -> any Widget {
if isNew {
return Text("NEW")
} else {
return Bordered(Text("seen"))
}
}
main.swift, replace everything below import Foundation withshow(badge(isNew: true))
show(badge(isNew: false))
print(type(of: badge(isNew: true)), type(of: badge(isNew: false)))
Run it (⌘R). The console shows
NEW
+------+
| seen |
+------+
Text Bordered<Text>
Program ended with exit code: 0It 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:
main.swift instead, below import Foundationshow(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.)
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:
Widgets.swiftstruct AnyWidget: Widget {
private let renderer: () -> [String]
init(_ widget: some Widget) { renderer = widget.render }
func render() -> [String] { renderer() }
}
Widgets.swift, replace badge(isNew:) with thisfunc badge(isNew: Bool) -> AnyWidget {
if isNew {
return AnyWidget(Text("NEW"))
} else {
return AnyWidget(Bordered(Text("seen")))
}
}
main.swift, replace everything below import Foundation withshow(Bordered(badge(isNew: true)))
print(type(of: badge(isNew: true)), type(of: badge(isNew: false)))
Run it (⌘R). The console shows
+-----+
| NEW |
+-----+
AnyWidget AnyWidget
Program ended with exit code: 0Now 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.
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.
The third way keeps all the type information. Define a single type with two cases, one for each possibility:
Widgets.swiftenum 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()
}
}
}
Widgets.swift, replace badge(isNew:) with thisfunc 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")))
}
}
main.swift, replace everything below import Foundation withshow(badge(isNew: true))
show(Bordered(badge(isNew: false)))
print(type(of: badge(isNew: true)))
Run it (⌘R). The console shows
NEW
+----------+
| +------+ |
| | seen | |
| +------+ |
+----------+
Either<Text, Bordered<Text>>
Program ended with exit code: 0The 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.
some in a parameterLook 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.
Widget protocol, and Text, Column and Bordered.AnyWidget, a type-erasing wrapper — TinyUI's AnyView.Either, one type that can hold either of two widgets — TinyUI's _ConditionalContent.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.
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.
Widgets.xcodeproj in Xcode.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.
func makeCard() -> some Widget {
Bordered(Text("Seattle"))
}some Widget: one concrete type, chosen by the function and known to the compiler.
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.
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.
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>.
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.
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.
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>>.
a() 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.
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.
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.
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.
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.
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.