In SwiftUI.View body, accidentally put `padding()` (all by itself, no dot in front): it compiles fine, but preview doesn't work/run show blank screen or crash

By accident, I put padding() all by itself inside my view body like this:

import SwiftUI

struct PaddingAccident: View {
    var body: some View {
        Text("Hello, World!")
        padding()               // I did this by accident, what's this?
    }
}

struct PaddingAccident_Previews: PreviewProvider {
    static var previews: some View {
        PaddingAccident()
    }
}

it seems I can put any View modifier with no dot in front:

        Text("Hello, World!")
        font(.title)
        padding()               // I did this by accident, what's this?

or just all modifiers, not any view:

        font(.title)
        padding()               // I did this by accident, what's this?

Why does it compile at all? Option-click it shows it's View.padding(). Maybe due to the new @​ViewBuilder body inference?

But preview doesn't work. However, I can run in simulator in one case but blank screen, in another case, crash with EXC_BAD_ACCESS

Should the compiler show error?

You're right, this is a side effect of @ViewBuilder inference. This is equivalent to if you wrote self.padding(), which is an interesting way of writing an infinitely recursive View.

I'm not quite sure how the compiler would warn about this. Presumably you could make a method on your View that doesn't return a modified self. The compiler would have no way of knowing which of these are going to recurse and which aren't...

2 Likes

The same also compile in Xcode 11.5:

struct ModifierAccident: View {
    var body: some View {
        Group {
            padding()
            frame(width: 100, height: 100)
            font(.largeTitle)
        }
    }
}

So this appears to be a problem with ViewBuilder. Ideally the compiler should flag this. It was baffling for me: my view was working fine, then I added a few more modifiers and the preview stops working.

Consider this code:

struct ModifierAccident: View {
    func myCustomSubview() -> some View {
        Text("Hello")
    }
    var body: some View {
        Group {
            myCustomSubview()
        }
    }
}

What is the difference between calling myCustomSubview() and padding() here? They're both instance methods, but one of them (padding()) uses self in its body, and the other one does not. This isn't really something the compiler can detect in the general case without some sort of annotation on methods like padding(). And such an annotation would be specifically tailored to SwiftUI's implementation details.

4 Likes

I got bitten by this myself, I tried to make an empty spacer, so I wrote frame, hit autocomplete and Xcode found a func for me! I was intrigued, did they add a global function that is a shortcut for EmptyView().frame(...)? Nope. I got greeted with a weird error without a clear message, because swift doesn't detect stack overflows.

This problem isn't something related to SwiftUI or ViewBuilder, it exists everywhere you can use implicit self. I am a fan of explicitly writing self everywhere, but it seems most people don't like it, and requiring it now would be massively source breaking. :(

I'd say the problem consists of 2 parts, to be able to omit self, and to have the same function name between self method and the target method. The latter of which has been sparse until SwiftUI.

Can you show example of each of these? Is

self method: like SwiftUI view modifier like padding()?
Target method: like myCustomSubview() above?

Here is a short sample code to show what is happening. Note that the only difference between bar() and baz() is the dot before foo(). The solution for catching such errors is using @warn_unqualified_access attribute as indicated in the comments:

protocol P {
    func foo()
}

struct S1: P { func foo() { print("S1 foo()") } }

struct S2: P {

    var s1 = S1()
    
    // Uncomment the following to get a warning:
    //@warn_unqualified_access
    func foo() { print("S2 foo()") }
    
    func bar() {
        s1
        .foo()
    }
    
    func baz() {
        s1
        foo() // Warns here when you uncomment @warn_unqualified_access
              // To silence the warning you should write `self.foo()` if that is what you mean.
    }

}

var s2 = S2()

s2.bar()
s2.baz()

I think SwiftUI should use this attribute on such methods.

I also think we may need to enhance @warn_unqualified_access with some arguments to give API authors more control over the error message and even let them provide fix-it for it.

It is also possible to teach the compiler to detect this particular pattern (when an unqualified method call follows a statement with an ignored result that would accept the same method signature)

It would also be nice if we could use this attribute on a protocol definition to make it warn for all types that conform to it:

// This does compile, but has no effect:
protocol P {
    @warn_unqualified_access func foo()
}
1 Like

A pre-SwiftUI example would be a type erasure:

protocol Fooable { func foo1() }

struct AnyFoo<Base: Fooable> {
  var base: Base
  func foo1() { base.foo1() }
}

In this case a self method AnyFoo.foo1 coincide with the method you meant to use Base.foo1. You don't otherwise wrap types with the same functionality often, and functions with different functionality usually have different names. Then SwiftUI came along and have View wraps another View which is specified using yet another View.

@warn_unqualified_access is probably a good solution here if we get it to work like @hooman said.

1 Like

It works, got this warning:

Use of 'foo' treated as a reference to instance method in struct 'S2'
Use 'self.' to silence this warning

In Xcode Playground. At first where was no warning. I close/re-open the whole Playground and it show this warning.

I think SwiftUI should mark modifier func with the warning since it doesn't make any sense to call modifier as a standalone func. If you really want it, then require self. prefix.

The fact that SwiftUI is using function builders might complicate things and make it harder to use @warn_unqualified_access on SwiftUI view modifiers. @Douglas_Gregor should be able to provide more insight on the effect of function builders on @warn_unqualified_access.

I wouldn't expect function builders to change anything here. I say, go ahead and try it!

Adding @warn_unqualified_access works. Here is a sample playground:

import SwiftUI
import PlaygroundSupport

extension View {
    @warn_unqualified_access
    func testModifier() -> some View { self.modifier(EmptyModifier()) }
}

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello,")
            Text("World!")
                // Remove the dot to get a warning:
                .testModifier()
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

Once you remove the dot, you will need to stop and re-run the playground to see the warning.

Note that since we only get a warning, playground will run and lock up (as we discussed, with infinite recursion) and ultimately crash with stack overflow. (EXC_BAD_ACCESS)

I think you should file an enhancement request with Apple (since SwiftUI is not part of Swift language) for detection of the case of forgetting the dot on a modifier.

I will be adding @warn_unqualified_access to my custom view modifiers until we get a better solution from Apple.

1 Like
Terms of Service

Privacy Policy

Cookie Policy