How does Swift prevent SwiftUI `body: Never` properties from being called?

It's been asked a couple times before (1, 2) without an answer, so does anyone know what prevents the following from compiling?

Text("Hello").body // πŸ›‘ 

:stop_sign: Value of type 'Text' has no member 'body'

Text (and many other SwiftUI views) have Body = Never, and while it makes sense that invoking .body isn't useful, it's not clear what compiler affordance is preventing it.

The closest I've found to an explanation is @mayoff's post that finds the underscored _UnaryView protocol: ios - How do view types like Text or Image conform to the View protocol in SwiftUI? - Stack Overflow

I haven't been able to reproduce the behavior myself, though, by employing an underscored protocol, so is it something else? Is there special compiler logic involved? An underscored attribute that I'm overlooking in the Swift interface?

3 Likes

Isn't simpler than that? Text("Hi there").body is requesting that a value be materialized, and that's simply not possible for Never types, by definition.

Unfortunately not. The following as noted in one of the previous threads compiles just fine:

struct MyView: View {
  var body: Never { fatalError() }
}

MyView().body // βœ… Compiles fine.

It just fatal-errors at runtime.

So there does appear to be specific compiler "magic" at work here.

3 Likes

SwiftUI doesn't call the body of views directly, it invokes a hidden requirement of the View protocol. The default implementation of that requirement, which all user-defined views call, is defined in terms of body. Primitive views provided by the framework have implementations that don't use the body. No compiler magic necessary.

3 Likes

Some magic is necessary to make the compiler issue the value of type 'Text' has no member 'body' error.

A Text value has a body property, as all Views do. Text's body calls fatalError:

import SwiftUI

func body<V: View>(of view: V) -> V.Body { view.body }

body(of: Text("hello"))

// runtime output:

SwiftUI/View.swift:94: Fatal error: body() should not be called on Text.

However, if we attempt to access the body property directly, the compiler rejects the program:

import SwiftUI

Text("hello").body

Output:

In fact, SwiftUI's swiftinterface file doesn't declare a body property for Text. Perhaps there is no compiler magic, and instead the SwiftUI framework simply has a (magic) build step that strips it from the swiftinterface file.

4 Likes

Great detective work! And I like your "body(of: Text("hello"))" (so we can still call it even if it is "hidden").

I totally understand the desire to know the relevant ins and outs of how compiler doing this. That aside, I'm curious why would anyone want to call View's body directly?

While I'm sure there are folks who can come up with some interesting use cases, I'm more interested from a library design perspective. Principles of SwiftUI's design can be applied to a lot of other problem domains where you compose types together using result builders, and the Body = Never pattern is a nice way to express primitives that aren't directly composed out of other units. I was curious if the compiler magic that prevented the concrete body properties from being invoked was more broadly available, but if @mayoff's right about the Swift interface itself being stripped, I guess that's not the case :smile:

4 Likes

Maybe out of topic but I am curious: How do you hide a protocol requirement (outside of the module I suppose ?) ?

It's just a convention - Xcode hides methods with leading underscore from the interface, but actually they are public methods in the protocol and you can implement them yourself, but to provide a meaningful implementation you need access to private types.

See How does SwiftUI hides private protocol requirements of SwiftUI.View protocol

3 Likes

By default, Swift tooling hides any identifier that starts with an underscore and is declared in a swiftinterface file. The compiler only generates a swiftinterface file for a module built with library evolution enabled, so this doesn't normally apply to your own modules, only to system modules and third-party binary modules.

From the Underscored Attributes Reference:

@_show_in_interface

Shows underscored protocols from the standard library in the generated interface.

By default, SourceKit hides underscored protocols from the generated swiftinterface (for all modules, not just the standard library), but this attribute can be used to override that behavior for the standard library.

1 Like

Worth pointing out that there's a difference between "the generated interface", which is what Xcode shows when you command-click a module and displays all the declarations in a module, and the swiftinterface file, which is a stable textual format that fully specifies the ABI of a module, and while it more-or-less human readable, full human readability is not required for this format the way that it is for the "generated interface"

2 Likes

We can use @_spi to omit a declaration from a .swiftinterface file. No magic build step needed.

I put the following into MyView.swift:

import SwiftUI

public struct MyView: View {
    @_spi(Private)
    public var body: Never { fatalError() }
}

Then I compiled it as follows, based on the β€œDirectly invoking the compiler” instructions found here:

swiftc MyView.swift -module-name MyLib -emit-module -emit-library -emit-module-interface -enable-library-evolution

The compiler writes MyLib.swiftinterface as follows, omitting the body declaration:

// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.7 (swiftlang-5.7.0.123.7 clang-1400.0.29.50)
// swift-module-flags: -target arm64-apple-macosx12.0 -enable-objc-interop -enable-library-evolution -module-name MyLib
import Swift
import SwiftUI
import _Concurrency
import _StringProcessing
public struct MyView : SwiftUI.View {
  public typealias Body = Swift.Never
}

And it writes MyLib.private.swiftinterface as follows, containing the body declaration:

// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.7 (swiftlang-5.7.0.123.7 clang-1400.0.29.50)
// swift-module-flags: -target arm64-apple-macosx12.0 -enable-objc-interop -enable-library-evolution -module-name MyLib
import Swift
import SwiftUI
import _Concurrency
import _StringProcessing
public struct MyView : SwiftUI.View {
  @_spi(Private) @_Concurrency.MainActor(unsafe) public var body: Swift.Never {
    get
  }
  public typealias Body = Swift.Never
}
5 Likes