How to correctly open existential of an `any` type so its @ViewBuilder function can be run in `body` of a View?

Hi community, I have run into a compiler error when implementing a feature, and then ran into a compiler crash while trying to fix the error. I am not sure how to resolve the problem, so I hope I can get some help here.

The feature basically is to have a protocol requirement in ModuleA that requires any type conforms to this protocol to provide an implementation of a specific @ViewBuilder function that generates Views. The concrete implementation will be deployed in the main App, and an instance of this concrete type will be assigned to ModuleA by the main App during runtime. Here is what it looks like:

Requirement in ModuleA:

enum TestViewType: Hashable, Sendable {
    case empty
    case color
}

protocol TestViewCreatorProtocol {
    associatedtype TestView: View
    @ViewBuilder
    func view(for viewType: TestViewType) -> TestView
}

Implementation in main App:

struct TestViewCreator: TestViewCreatorProtocol {

    @ViewBuilder
    func view(for viewType: TestViewType) -> some View {
        switch viewType {
        case .empty:
            EmptyView()
        case .color:
            Color.red
        }
    }
}

Usage in ModuleA:

struct TestView: View {
    let viewCreator: any TestViewCreatorProtocol
    var body: some View {
        self.viewCreator.view(for: .color)
    }
}

At first, the compiler complains:

Type 'any View' cannot conform to 'View'

So I tried to open existential of the viewCreator of any TestViewCreatorProtocol to avoid the viewCreator.view(for:) returns an any View:

struct TestView: View {
    let viewCreator: any TestViewCreatorProtocol
    var body: some View {
+        _openExistential(self.viewCreator) {
+            $0.view(for: .color)
+        }
    }
}

The previous warning goes away but the complier seems crashed this time:

1.	Apple Swift version 6.1.2 (swiftlang-6.1.2.1.2 clang-1700.0.13.5)
2.	Compiling with the current language version
3.	While evaluating request ASTLoweringRequest(Lowering AST to SIL for file "/Users/z/Development/Playground/Sources/Run/main.swift")
4.	While silgen emitFunction SIL function "@$s3Run8TestViewV4bodyQrvg".
 for getter for body (at /Users/z/Development/Playground/Sources/Run/main.swift:31:9)
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
0  swift-frontend           0x00000001066aee24 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 56
1  swift-frontend           0x00000001066acc5c llvm::sys::RunSignalHandlers() + 112
2  swift-frontend           0x00000001066af460 SignalHandler(int) + 360
3  libsystem_platform.dylib 0x0000000189348624 _sigtramp + 56
4  swift-frontend           0x0000000101fe8354 swift::InFlightSubstitution::substType(swift::SubstitutableType*, unsigned int) + 48
5  swift-frontend           0x0000000101fede78 (anonymous namespace)::TypeSubstituter::transformGenericTypeParamType(swift::GenericTypeParamType*, swift::TypePosition) + 32
6  swift-frontend           0x0000000101fe9ad0 swift::TypeTransform<(anonymous namespace)::TypeSubstituter>::doIt(swift::Type, swift::TypePosition) + 2244
7  swift-frontend           0x0000000101fedca4 swift::TypeTransform<(anonymous namespace)::TypeSubstituter>::transformSubMap(swift::SubstitutionMap) + 120
8  swift-frontend           0x0000000101fe9800 swift::TypeTransform<(anonymous namespace)::TypeSubstituter>::doIt(swift::Type, swift::TypePosition) + 1524
9  swift-frontend           0x0000000101fe8d1c swift::Type::subst(swift::InFlightSubstitution&) const + 976
10 swift-frontend           0x0000000101fe82c8 swift::Type::subst(llvm::function_ref<swift::Type (swift::SubstitutableType*)>, llvm::function_ref<swift::ProtocolConformanceRef (swift::CanType, swift::Type, swift::ProtocolDecl*)>, swift::SubstOptions) const + 392
11 swift-frontend           0x0000000100e4e064 swift::Lowering::TypeConverter::getContextBoxTypeForCapture(swift::ValueDecl*, swift::CanType, swift::GenericEnvironment*, bool) + 256
12 swift-frontend           0x0000000100fd9988 swift::Lowering::SILGenFunction::emitLocalVariableWithCleanup(swift::VarDecl*, std::__1::optional<swift::MarkUninitializedInst::Kind>, unsigned int, bool) + 940
13 swift-frontend           0x0000000100fd8b28 swift::Lowering::SILGenFunction::emitInitializationForVarDecl(swift::VarDecl*, bool, bool) + 848
14 swift-frontend           0x0000000100fda2b0 swift::Lowering::SILGenFunction::emitPatternBinding(swift::PatternBindingDecl*, unsigned int, bool) + 412
15 swift-frontend           0x0000000100fe1840 swift::ASTVisitor<swift::Lowering::SILGenFunction, void, void, void, void, void, void>::visit(swift::Decl*) + 120
16 swift-frontend           0x00000001010a915c swift::ASTVisitor<(anonymous namespace)::StmtEmitter, void, void, void, void, void, void>::visit(swift::Stmt*) + 6324
17 swift-frontend           0x0000000101024b84 swift::Lowering::SILGenFunction::emitFunction(swift::FuncDecl*) + 496
18 swift-frontend           0x0000000100f6924c swift::Lowering::SILGenModule::emitFunctionDefinition(swift::SILDeclRef, swift::SILFunction*) + 7596
19 swift-frontend           0x0000000100f6a278 swift::Lowering::SILGenModule::emitOrDelayFunction(swift::SILDeclRef) + 232
20 swift-frontend           0x0000000100f67454 swift::Lowering::SILGenModule::emitFunction(swift::FuncDecl*) + 344
21 swift-frontend           0x00000001010c1fd0 (anonymous namespace)::SILGenType::visitFuncDecl(swift::FuncDecl*) + 32
22 swift-frontend           0x00000001010c21d0 (anonymous namespace)::SILGenType::visitAbstractStorageDecl(swift::AbstractStorageDecl*) + 248
23 swift-frontend           0x00000001010c1f40 (anonymous namespace)::SILGenType::visitVarDecl(swift::VarDecl*) + 464
24 swift-frontend           0x00000001010be824 (anonymous namespace)::SILGenType::emitType() + 456
25 swift-frontend           0x0000000100f67098 swift::ASTVisitor<swift::Lowering::SILGenModule, void, void, void, void, void, void>::visit(swift::Decl*) + 100
26 swift-frontend           0x0000000100f6dce0 swift::ASTLoweringRequest::evaluate(swift::Evaluator&, swift::ASTLoweringDescriptor) const + 1568
27 swift-frontend           0x00000001010a6f50 swift::SimpleRequest<swift::ASTLoweringRequest, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule>> (swift::ASTLoweringDescriptor), (swift::RequestFlags)17>::evaluateRequest(swift::ASTLoweringRequest const&, swift::Evaluator&) + 208
28 swift-frontend           0x0000000100f7286c swift::ASTLoweringRequest::OutputType swift::Evaluator::getResultUncached<swift::ASTLoweringRequest, swift::ASTLoweringRequest::OutputType swift::evaluateOrFatal<swift::ASTLoweringRequest>(swift::Evaluator&, swift::ASTLoweringRequest)::'lambda'()>(swift::ASTLoweringRequest const&, swift::ASTLoweringRequest::OutputType swift::evaluateOrFatal<swift::ASTLoweringRequest>(swift::Evaluator&, swift::ASTLoweringRequest)::'lambda'()) + 728
29 swift-frontend           0x0000000100500064 swift::performCompileStepsPostSema(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 968
30 swift-frontend           0x0000000100503654 performCompile(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 1764
31 swift-frontend           0x0000000100501fd8 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 3716
32 swift-frontend           0x00000001004860bc swift::mainEntry(int, char const**) + 5428
33 dyld                     0x0000000188f6eb98 start + 6076

Am I using the _openExistential wrong? What is the correct way to achieve the expected behavior?

Note:

I knew that using generics can solve the problem:

struct TestView<ViewCreator: TestViewCreatorProtocol>: View {
    let viewCreator: ViewCreator
    var body: some View {
        self.viewCreator.view(for: .color)
    }
}

But this TestView example is just an oversimplified version of the actual feature in ModuleA, so the generics actually does not apply here.

Update (07/09/2025)

Our current workaround is to erase the View type by making the func view(for viewType: TestViewType) -> TestView return AnyView, but we prefer not to use it.

I am trying to get rid of using AnyView to avoid efficiency loss where the type-erasure causing the whole view subtree to be re-built and re-drawn unnecessarily when only the specific element wrapped in the AnyView needs to.

I believe there are some open issues tracking for _openExistential… it might still be broken.

1 Like

This error message is correct. The underlying type of an opaque result type cannot dynamically depend on an existential value. It has to be some fixed concrete type. This is because you can reference an opaque result type from another context and form a type from it, without actually invoking the declaring function.

_openExistential() shouldn’t be needed anymore because simply passing an existential value to a local generic function should be sufficient to express everything that is safe today.

4 Likes

The issue in your bug report is that the opened existential type escapes from the closure passed to _openExistential(). This is not supported in the semantic model, but I’d rather remove _openExistential() instead of trying to add a bespoke diagnostic for this case.

3 Likes

IOW, you should replace _openExistential with this:

struct TestView: View {
    let viewCreator: any TestViewCreatorProtocol
    var body: some View {
        func _body<VC: TestViewCreatorProtocol>(_ vc: VC) -> some View {
            vc.view(for: .color)
        }
        return _body(viewCreator)
    }
}

But now you still have a problem — error: type 'any View' cannot conform to 'View'. That's because the some View on the generic could still return different types on different calls. You can fix this using SwiftUI's AnyView to type-erase:

struct TestView: View {
    let viewCreator: any TestViewCreatorProtocol
    var body: some View {
        func _body<VC: TestViewCreatorProtocol>(_ vc: VC) -> AnyView {
            AnyView(vc.view(for: .color))
        }
        return _body(viewCreator)
    }
}

Or if you want to avoid AnyView, you can make TestView itself generic:

struct TestView<VC: TestViewCreatorProtocol>: View {
    let viewCreator: VC
    var body: some View {
        viewCreator.view(for: .color)
    }
}
3 Likes

AnyView is the general solution to this problem but it doesn't need all that ceremony. any Views are opened by its initializer. So, just this:

struct TestView: View {
  let viewCreator: any TestViewCreatorProtocol
  var body: some View {
    AnyView(viewCreator.view(for: .color))
  }
}

They said that's not an option.


Also, empty cases are better covered via Optional:

enum TestViewType: Hashable, Sendable {
  case color
}

protocol TestViewCreatorProtocol {
  associatedtype TestView: View
  @ViewBuilder func view(for viewType: TestViewType?) -> TestView
}

struct TestViewCreator: TestViewCreatorProtocol {
  @ViewBuilder func view(for viewType: TestViewType?) -> some View {
    if let viewType {
      switch viewType {
      case .color: Color.red
      }
    }
  }
}
2 Likes

So the compiler converts expressions of dependent opaque type into expressions of existential type?

No, an opaque return type is parameterized by its owner declaration's generic parameters, just like a generic nominal type:

func f<T: Equatable>(_: T) -> some P { ... }

The return types of f(0) and f("") are the some P of f(), parameterized by Int and String ,respectively. They're not existential.

1 Like

You can fix this using SwiftUI's AnyView to type-erase:

Thanks for the suggestion, but I posted this question initially to avoid using of AnyView. (but obviously I failed to mention it in the original post, my bad, sorry)

I am trying to get rid of using AnyView to avoid efficiency loss where the type-erasure causing the whole view subtree to be re-built and re-drawn unnecessarily when only the specific element wrapped in the AnyView needs to.

So using AnyView is not an option here. But thanks anyway!

Also, empty cases are better covered via Optional

The empty case are just for demo purpose, but this is good to know. Thanks!

This might be unavoidable as long as you have some kind of existential in there, because the type of the view is going to change.

3 Likes

Can I declare an instance property of a type of closure that returns some View? Like this:

struct TestViewCreator {
    var createView: (TestViewType) -> some View
}

So that I can mutate the implementation a runtime to a different function.

But currently I got:

Property declares an opaque return type, but has no initializer expression from which to infer an underlying type

When we say "during runtime"… what exactly are the constraints you are operating under? Could this "runtime" injection actually work as a "compile time" injection?

The more you are "locked in" to a true runtime resolution you are also going to be more locked in to existential types. If you do want static dispatch and opaque types you might have to make some compromises about how you plan to inject your concrete implementations.

I think closures that return opaque return types might run into implementation limitations in practice, but the semantics there are intended to be the same. Your property is required to have an initial value that is a closure expression returning some fixed concrete type. It cannot change dynamically.

2 Likes

Like, fundamentally, you can't have your cake and eat it here. The some View return type from body must always represent a single type known (to the compiler) at compile-time. So there are only two options:

  • The view type is static. Maybe because you used generics, and it's a function of those types, their associated types, etc. Or maybe because there's a finite set of possibilities and you used an enum with a switch statement. Either way, the compiler knows the return type of body and you get to avoid AnyView,
  • The view type is dynamic. Maybe because you used an existential or a subclass or something where the implementation can be swapped at runtime. Either way, you're forced to wrap something up in AnyView so that the compiler knows the return type of body.

I'm not sure that the panic Apple has instilled in developers over AnyView performance is helpful. Sometimes you need it. That's why it exists! Last year, Xcode 16 changed every body in Debug builds to return AnyView and nobody complained about the performance, only the things that it broke.

5 Likes

That's why it exists! Last year, Xcode 16 changed every body in Debug builds to return AnyView and nobody complained about the performance, only the things that it broke.

No, there were definitely major complaints about performance, which is why the preview system was revamped yet again in Xcode 16.3, moving to a hybrid approach between the < 16 and 16.0 systems. But that was when the entire view hierarchy was replaced with AnyView at every point, so it was basically the worst case scenario. And in that case the performance impact was fully felt.

6 Likes