Type-Erasing in Swift | AnyView behind the scenes

What I’m trying to achieve

Hi! I’m currently working on a project that requires type-erasing, similar to SwiftUI’s AnyView. And I’ve been wondering how AnyView works behind the scenes and how that could help me with my project.

Limitations

I have tried to solve the problem of creating a type-erasing Type with many different approaches but I seem to always run into problems of type-checking. The main limitation is that I cannot use generics in my struct, because I want it to be type-erased, so that the following:

struct TypeErased<Value: MyProtocol> {
    let value: Value

    init(value: Value) {
        self.value = value
    }
}

does not create a type-erased Type. Instead, it creates something like a wrapper Type for Value. I think that I need generics but only in the initializer.

If anyone can help me or knows something about how AnyView works behind the scenes, then please let me know; I would really appreciate it! Thanks in advance!

If you have protocol that looks like this

protocol MyProtocol {
    associatedtype A
    func requirement() -> A
}

then there are two ways you can type-erase it that I know

the easier one:

struct TypeErased {
    typealias A = Any
    init<T: MyProtocol>(_ value: T) {
        self._requirement = value.requirement
    }

    private let _requirement: () -> Any
    func requirement() -> Any {
        return self._requirement()
    }
}

and the more complicated, but I think this one is used by standard library (sorry for meaningless names)

private protocol TypeErasedBox {
    var base: Any { get }
    func requirement() -> Any
}
private struct ConcreteTypeErased<Base: MyProtocol>: TypeErasedBox {
    let baseProto: Base
    var base: Any {
        return self.baseProto
    }
    func requirement() -> Any {
        return self.baseProto.requirement()
    }
}

struct TypeErased: MyProtocol {
    typealias A = Any
    private let box: TypeErasedBox
    init<T: MyProtocol>(_ value: T) {
        self.box = ConcreteTypeErased(baseProto: value)
    }
    var base: Any {
        self.box.base
    }
    func requirement() -> Any {
        return self.box.requirement()
    }
}
7 Likes

Oh sorry... I forgot to specify something important. Let’s assume that my protocol is called ‘Block’ I would want it to be something like this:

protocol Block {
    associatedtype Content: Block
    var content: Content { get }
}

struct AnyBlock: Block { 
    init<B: Block>(_ block: Block) {
        //... 
    }
    
    var content: //...
}

In other words I want the type-erased struct AnyBlock to be a block. Like SwiftUI’s AnyView.

I’m sorry about that. I’m new to the forums and I wasn’t specific enough. Thanks for the quick response by the way!

1 Like

Oh.
AnyView has Never as the type of the body which means that SwiftUI never grabs that directly, and instead uses some kind of black magic to unwrap the type-erased view. My sorcery level is too low to guess how they're doing it. :(

2 Likes

After doing some more testing I discovered that SwiftUI’s AnyView uses a property of type (random letters).Storage, so you are right. Thanks for telling me that. I’d been stack trying to solve this problem for a couple of days.

1 Like

@filip-sakel Were you ever successful creating your type eraser? I have run into a similar situation and was looking around when I found this

There's a more type-safe and robust way of implementing a type-erasing wrapper:

public protocol Variable {

    associatedtype Value

    var value: Value { get set }
}

public struct AnyVariable<Value>: Variable {

    public init<Other>(_ other: Other) 
    where Other: Variable, Other.Value == Value { 
        box = AnyVariableBox(other)
    }

    private var box: AnyVariableAnyBox<Value>   

    private mutating func prepareToMutate() { 
        guard isKnownUniquelyReferenced(&box) else {
            return
        }
        box = box.makeDeepCopy()
    }

    public var underlying: Any {
        box.underlying
    }

    public var value: Value {
        
        get {
            box.value
        }

        set {
            prepareToMutate()
            box.value = newValue
        }
    }
}

internal class AnyVariableAnyBox<Value> {

    internal var underlying: Any { 
            fatalError("execution has reached a routine that should have been overridden")
    }

    internal func makeDeepCopy() -> AnyVariableAnyBox<Value> { 
            fatalError("execution has reached a routine that should have been overridden")
    }
    
    internal var value: Value {
    
        get {
            fatalError("execution has reached a routine that should have been overridden")
        }
    
        set { 
            fatalError("execution has reached a routine that should have been overridden")
        }
    }
}

internal class AnyVariableBox<Other>: AnyVariableAnyBox<Other.Value>
where Other: Variable {

    internal init(_ other: Other) {
        self.other = other
    }

    private var other: Other

    internal override var underlying: Any { 
        other
    }

    internal override func makeDeepCopy() -> AnyVariableAnyBox<Other.Value> { 
        Self(other)
    }

    internal override var value: Value {

        get { 
            other.value
        }

        set { 
            other.value = newValue
        }
    }
}

TL;DR

SwiftUI’s black magic (as @cukr points out upthread) is underscore-prefixed View requirements that Xcode hides from clients. To see all the requirements, you have to go to the interface at:

path/to/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64.swiftinterface

As for achieving type erasure, you can follow the ‘AnyHashable’, standard-library approach (as shown upthread) by treating the hidden requirements as the only requirements.


I managed to solve this, by first understanding that my protocol needs other requirements than the recursive body property. That’s because, this approach lacks genuine functionality — e.g. “What purpose does a protocol with a body property constrained to itself serve?” The more fundamental problem, though, is that, well, type-erasing erases type information. So, if your protocol’s requirements don’t rely on some lower-level types for their functionality, type erasing removes type information, and by extension this requirement’s functionality.

SwiftUI’s solution to his problem is hidden requirements. These are simply underscore-prefixed requirements that Xcode hides from clients of Apple frameworks. They are used as the View protocol's actual requirements. This way, clients can conveniently declare views that relay functionality to built-in views, and built-in views can actually implement these requirements.

Another option is to define an internal protocol (i.e. _PrimitiveView) where you have your actual requirements. Then you can dynamic cast the root view to that protocol. If the cast fails, you can recursively cast the body properties of your views until you find a primitive view. This method is probably less performant compared to the first one; however, I haven’t tested it.

SwiftUI's Hidden Requirements

If you’re interested in learning more about View’s hidden requirements you can go to the SwiftUI interface file (the path is specified above). If you search for protocol View you’ll find this definition:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@_typeEraser(AnyView) public protocol View {
  // [My comment] *Hidden* requirements:
  static func _makeView(view: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs
  static func _makeViewList(view: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewListInputs) -> SwiftUI._ViewListOutputs
  @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
  static func _viewListCount(inputs: SwiftUI._ViewListCountInputs) -> Swift.Int?

  // [My comment] "Normal" requirements:
  associatedtype Body : SwiftUI.View
  @SwiftUI.ViewBuilder @_Concurrency.MainActor(unsafe) var body: Self.Body { get }
}

The reason clients don’t have to interact with these underscore-prefixed methods is that SwiftUI provides default implementations for them. These default implementations call the body’s respective hidden requirements. For example, _makeView(view:inputs:) is probably implemented as: Body._makeView(view: view.body, inputs: inputs). Primitive views, of course, provide their own implementations, as described above.

Failable Initializer

One improvement to the aforementioned type-erasing strategy is offering a failable initializer accepting a value of type Any. It’s a simple modification. To achieve it, you have to conditionally conform ConcreteTypeErased to TypeErasedBox where the Base conforms to MyProtocol. This enables dynamic casting when you package a generic value into the box, as is done here:

extension TypeErased {
  private init?<T>(_genericValue value: T) {
    let box = ConcreteTypeErased(baseProto: value)

    guard let properBox = box as? TypeErasedBox else {
      return nil
    }

    self.box = properBox
  }
}

Finally, to accept values of type Any — which is you can imagine as a box containing this generic value — you have to open the value:

extension TypeErased {
  init?(_ value: Any) {
    func openValue<T>(value: T) -> T { value }
    let opened = _openExistential(value, openValue)

    self.init(_genericValue: opened)
  }
}

SE-0309 Implications

I know all of this is quite complicated, but once SE-0309 is implemented, this process will be significantly simplified, since you won’t have to create TypeErasedBox- and ConcreteTypeErased-style protocols; instead, you’ll be able to just use View as your box with some extensions.

6 Likes

Technically, this is type-erasing and is akin to the standard library’s implementation of AnyCollection. However, it’s not that useful, since — at least in the case of SwiftUI — AnyView must not have generic type parameters to be used in the implementation of TupleView. Of course, I imagine that this kind of type erasing could be useful for not exposing internal types to the public API.

Aside from using underscored requirements, SwiftUI might also be using a technique for promoting certain implementations of a protocol to a "special" status without exposing that fact to the client code:

public protocol View {
    associatedtype Body: View
    var body: Body { get }
}

extension View {
    internal func updateView(in viewGraph: inout ViewGraph) {
        if let intrinsic = self as? IntrinsicView {
            intrinsic.updateView(in: &viewGraph)
        } else {
            body.updateView(in: &viewGraph)
        }
    }
}

internal protocol IntrinsicView {
    func updateView(in viewGraph: inout ViewGraph)
}

extension Text: View, IntrinsicView {
    public typealias Body = Never
    public var body: Body { fatalError() }
    internal func updateView(in viewGraph: inout ViewGraph) {
        // Do some magic stuff that no client code will ever be able to do.
    }
}

This way, the UI renderer with use the updateView(in:) method of the View protocol regardless of whether it's a "magic" view or a user view and sill leave room for "magic" views to do magic things by having direct access to the underlying machinery.

I highly doubt that all of SwiftUI's standard views and modifiers are implemented in "user space". Many of them have to be magical and this is how I think SwiftUI permits magical views to exist without exposing them as such.

2 Likes

Thank you for going into more detail on this. I briefly explored this approach with the dynamic-casting/_PrimitiveView option in my reply to @maxwellpirtle, but wasn't very clear.

Could you elaborate on what you mean by "user space"? If you're talking about exposing internal types to public API, I think SwiftUI can already achieve that with concrete types, possibly type-erasing containers, as opposed to associated types constrained to underscore-prefixed protocols. One such type is _ViewOutputs which doesn't expose its implementation to clients.


Have you done any testing on the performance characteristics of the IntrinsicView/_PrimitiveView approach? If type erasing and dynamic casting are equally performant, then I'd rather use dynamic casting in a project of mine, as it avoids exposing underscore-prefixed methods altogether.

Without having underscored public requirements (which is a hack, in my opinion), I don't see a good way of supporting "special" implementations without using an internal protocol and performing a dynamic cast, so it might not even be a choice at this point. In this context, type-erasing wrapper serves a different purpose altogether.

By saying "implementing something in user space" I mean "implementing something as if it was implemented in a separate module by a separate author" that is, with no access to or knowledge of the internals.

I haven't, but I think that the type casting approach will be faster, because in the case of a type-erasing wrapper, we're looking at a non-final class instantiation overhead, but in case of type casting, we're looking at an existential container allocation. Existential container allocation can easily be optimized away (e.g. promoted to stack allocation and even inlined), but the non-final class instantiation can not (if the class was final, it's memory layout would be known at compile time and it too could've been promoted to stack allocation, but because of inheritance, the calls would still not be inlined).