swiftswiftui

type(of: self) in View extension


I use FirebaseAnalytics with View.analyticsScreen(type(of: self)) to log my Views. That works fine: when I have a View called MyBeautifulView, then exactly that is being logged. However instead of me having to import FirebaseAnalytics every time, I went ahead and created an extension on View

func analyticsScreen() -> some View {
   analyticsScreen(name: "\(type(of: self))")
}

I noticed that when using the method above, the output of type(of: self) is something like

_ConditionalContent<_ConditionalContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<List<Never, TupleView<(Optional<Section<Text, ForEach<Array, Optional, ModifiedContent<ModifiedContent...

I expected it to be the View's name instead. Why is it not showing the name and how can I get my desired behaviour? Is that a feature? (of course I could just pass the struct name via a parameter, but I would like to know if there is a better way).

Also this is not a Firebase issue, which is why I did not label it as such. It also happens if I just do print(type(of: self)).


Solution

  • self means different things depending on the context. In

    func analyticsScreen() -> some View {
       analyticsScreen(name: "\(type(of: self))")
    }
    

    self refers to the value on which you have called analyticsScreen(). So if you do

    Text("Foo").analyticsScreen()
    

    self will be Text("Foo").

    If you have a bunch of modifiers before .analyticsScreen(), it will be the ModifiedContent<...> mess that you see.

    When you use self in the body of your view, it refers to whatever value body has been called on. Obviously, this will always be an instance of your view struct.

    To put it simply, this boils down to:

    protocol P {
        
    }
    
    extension P {
        func f() {
            print(self)
        }
    }
    
    struct Bar: P {
        
    }
    
    struct Foo: P {
        func g() {
            print(self) // "self" here refers to a different thing from the "self" when calling Bar().f()
            Bar().f()
        }
    }
    

    There is no way around this. A function cannot know where (as in, which type) it has been called from. You need to pass self as a parameter:

    func analyticsScreen(_ view: some View) -> some View {
        analyticsScreen(name: "\(type(of: view))")
    }
    
    // usage: SomeView().analyticsScreen(self)
    

    I'd also suggest passing the meta type directly so you don't need type(of:).

    func analyticsScreen(_ type: (some View).Type) -> some View {
       analyticsScreen(name: "\(type)")
    }
    
    // usage: SomeView().analyticsScreen(Self.self)
    

    If you really hate that parameter, a compromise could be using the file ID:

    func analyticsScreen(_ file: String = #fileID) -> some View {
       analyticsScreen(name: "\(file)")
    }