Impossible SwiftUI views
When defining a SwiftUI view, it comes automatic to write struct MyView: View
followed by var body: some View { ... }
:
all our views are composed by other views, but at some point SwiftUI needs to draw something on the screen: when does it end? How does SwiftUI know that it has reached the bottom of a view hierarchy?
In this article, let's continue our exploration of SwiftUI's inner workings.
The View protocol
Every SwiftUI view is a type conforming to the View
protocol:
public protocol View {
associatedtype Body: View
@ViewBuilder var body: Self.Body { get }
}
Since this is a Swift protocol, we can make any type conform to it, for example String
:
extension String: View {
public var body: some View {
Text(self)
}
}
Note: this is just an example, not a suggestion nor a best practice.
Which allows us to write a String
directly into a View
declaration:
struct ContentView: View {
var body: some View {
"Hello world!"
}
}
As we're using the Text
primitive in String
's body
, this will work as expected, but what if we didn't? Let's build a new SwiftUI view which uses none of SwiftUI's primitives:
struct MyView: View {
var body: some View {
self
}
}
In this case we're defining a new View
called MyView
, here we use it in ContentView
:
struct ContentView: View {
var body: some View {
MyView()
}
}
This builds fine, however, as MyView
's' body
is the view itself, we're stuck on an infinite recursion, which will make SwiftUI terminate our app.
Despite the View
protocol letting us conform pretty much anything to it, if we want to use those declarations in SwiftUI, we must use SwiftUI primitives, or be ready to see our app crash.
What do these SwiftUI primitives have that make them special, allowing SwiftUI to break the infinite recursion we've found ourselves in? The answer is in their body
type: Never
.
Never
Swift 3.0 has brought us Never
, an uninhabited type: Never
is a type with no possible values, making it impossible for us to get or create an instance.
We might have met Never
for example:
- in Combine's Publishers, typically to indicate that a given publisher can't throw errors
- as the return type of
preconditionFailure(_:file:line:)
orfatalError(_:file:line:)
, indicating that, once called, there's no way out
Let's declare a view with body type Never
:
struct ImpossibleView: View {
var body: Never
}
This builds! However we have no way to properly use it: we can't instantiate it without passing something like fatalError()
. Regardless, let's implement the body and run our app:
struct ImpossibleView: View {
var body: Never {
fatalError("This will make our app 💥")
}
}
Here's our ContentView
:
struct ContentView: View {
var body: some View {
ImpossibleView()
}
}
Unsurprisingly, our app will crash once again, however the crash reason is Fatal error: ImpossibleView may not have Body == Never: file SwiftUI, line 0
, not our This will make our app 💥
.
Reading throughout the stack trace, we will see that there's an assertionFailure
within an internal SwiftUI's BodyAccessor.makeBody(container:inputs:fields:)
method, which apparently is not happy with our ImpossibleView
body
type.
This is the same method that will crash our app if we pass a
class
instance instead of a value type.
Only views declared within SwiftUI are allowed to have body type Never
: despite not having access to BodyAccessor
's code, it's clear that those views would either pass this assertion, or that they'd take a different, special path.
SwiftUI can't keep asking for view body
s forever: it needs a special set of views, a.k.a. a set of primitives, that it can draw without asking for their body. This is why Text
, ZStack
, Color
, etc have Never
as their body type.
Is Never a View?
A type conforming to View
needs to return a body that is also a View
, therefore, from what we've seen above, this is the case: Never
is a view.
SwiftUI knows to not to ask for the body of views with body
type Never
, either by crashing if it's not a primitive, or do something else otherwise. However, since we must make our code compile, SwiftUI needs to extend Never
to be a View
: the ultimate, impossible view.
To confirm this, we can inspect SwiftUI's headers, where we will find the following declaration (spread in a few places):
extension Never: View {
public typealias Body = Never
public var body: Never { get }
}
The SwiftUI team could have declared another special type to be used instead of Never
, however I find this solution very elegant and perfectly fitting for the use case.
Conclusions
In this article we've explored how SwiftUI breaks the infinite recursion challenge when drawing views, and how it uses the special Swift type Never
in order to elegantly achieve that.
I hope you've found this article useful: please let me know if I've missed anything!
Thank you for reading and stay tuned for more SwiftUI articles!
Bonus track
Since the article is about impossible views, just for fun I want to share another completely legal and 100% crashing way to "build" views: declare nothing but any modifier.
struct ContentView: View {
var body: some View {
border(Color.black)
}
}
// or
struct ContentView: View {
var body: some View {
padding()
}
}
// or
struct ContentView: View {
var body: some View {
ignoresSafeArea()
}
}
// etc
So many possibilities! 💣
Are you aware of any another interesting way to crash SwiftUI? I'd love to know!