superpuccio
superpuccio

Reputation: 12992

Change @State in a @ViewBuilder closure

I'm trying to understand how to change @State variables in @ViewBuilder closures. The following is just a simple example:

struct ContentView: View {
    @State var width = CGFloat(0)

    var body: some View { //Error 1
        GeometryReader { geometry in //Error 2
            self.width = geometry.size.width
            return Text("Hello world!")
        }
    }
}

I'm getting some errors:

Error 1:

Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type

But the return is redundant because there's only one line of code inside the View computed property.

Error 2:

Cannot convert return expression of type 'GeometryReader<_>' to return type 'some View'

Since I explicitly write return Text("...") shouldn't the type be clear?

What's the problem here?

Upvotes: 0

Views: 2101

Answers (1)

Procrastin8
Procrastin8

Reputation: 4513

First, you can't make arbitrary swift statements in a functionBuilder. There is a specific syntax allowed. In SwiftUI's case, it's a ViewBuilder, which under the hood is composing your type. When you make consecutive statements in SwiftUI, you are actually relying on the compiler to compose a new type underneath according to the rules of that DSL.

2nd, SwiftUI is a recipe, it's not an event system. You don't change state variables in the body function, you set up how things should react when state variables change externally. If you want another view to react in some way to that width, you need to define that content with respect to the width of the component you want. Without knowing what your ultimate goal is here, it's hard to answer how to relate the content to each other.

EDIT:

I was asked to elaborate on what exactly is allowed. Each functionBuilder has a different allowable syntax which is defined in the functionBuilder itself. This has a good overview on function builders in Swift 5.1: https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api

As for what SwiftUI is specifically looking for, it's essentially looking for each statement to return an instance of View.

// works fine!
VStack {
  Text("Does this")
  Text("Work?")
}

// doesn't work!
VStack {
  Text("Updating work status...")
  self.workStatus = .ok // this doesn't return an instance of `View`!
}

// roundabout, but ok...
VStack {
  Text("Being clever")
  gimmeView()
}

// fine to execute arbitrary code now, as long as we return a `View`
private func gimmeView() -> some View {
    self.workingStatus = .roundabout 
    return Text("Yes, but it does work.")
}

This is why you got the obtuse error you got:

Cannot convert return expression of type 'GeometryReader<_>' to return type 'some View'

The type system can't construct any View out of View and essentially Void when you execute:

self.width = geometry.size.width

When you do consecutive View statements, underneath, it's still being converted into a new type of View:

// the result of this is TupleView<Text, Text>
Text("left")
Text("right")

Upvotes: 5

Related Questions