TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: A strange behaviour of a @State variable

Forums > SwiftUI

I've got a simple app with two views. When one of them is tapped, I want to display in a sheet some information appropriate for that view. To distinguish between the views, I have an enum, and a couple of variables: to control which view was tapped, and whether to display the sheet. Here is the whole code:

import SwiftUI

struct InfoView: View {
    @Environment(\.dismiss) var dismiss

    let text: String

    var body: some View {
        NavigationView {
            VStack {
                Text(text)
                    .font(.title3)
                Spacer()
            }
            .navigationTitle("Info")
            .toolbar {
                Button("Dismiss") {
                    dismiss()
                }
            }
        }
    }
}

struct ContentView: View {
    @State private var showingInfo = false
    @State private var infoKind: InfoKind = .one

    enum InfoKind {
        case one
        case two
    }

    var body: some View {
        VStack {
            ZStack {
                Color.green
                Text("One")
            }
            .frame(maxWidth: .infinity, maxHeight: 200)
            .onTapGesture {
                infoKind = .one
                showingInfo = true
            }
            ZStack {
                Color.blue
                Text("Two")
            }
            .frame(maxWidth: .infinity, maxHeight: 200)
            .onTapGesture {
                infoKind = .two
                showingInfo = true
            }
        }
        .sheet(isPresented: $showingInfo) {
            if infoKind == .one {
                InfoView(text: "ONE")
            } else if infoKind == .two {
                InfoView(text: "TWO")
            }
        }
    }
}

When I run the app and tap the second view first, it behaves wrongly by displaying the sheet with ONE in it. I can tap the second view any number of times, and I'll keep getting the wrong result. But...

Once I tap the first view, after that the app will behave as expected: tapping the second view will display TWO, tapping the first — ONE.

But what is the reason for the initial hiccup? It appears that this line of code:

infoKind = .two

has no effect right after the app was launched.

2      

Hello, I am also relatively new to the whole swiftui declarative thing, but as I've been troubleshooting and debuggins similar issues, watching the code actually step through it finally clicked for me that the way swiftui is working is that it runs the code to render the view and then doesn't redraw the view unless something changed in the actual views, and it runs the stuff sequentially so the last thing it built was two, so if you keep tapping the two frame, nothing is changing. Not until you tap the first frame is something new in the view and it actually redraws the Content View one. I observed it when I was using a list with two different sets of data depending on row selected .

Not sure fix for your scenario, but the way I fixed mine was with a Bool flag that I passed between the views togging on/off and adding a Text(flag).hidden in the view I wanted to refresh. Will play a little more with yours

Hope this helps.

2      

Thank you, but in this particular example I am changing a @State variable when tapping on the second view. That should signal SwiftUI that the app's state has changed and so there is a need to re-create it.

2      

Hello, I am also relatively new to the whole swiftui declarative thing, but as I've been troubleshooting and debuggins similar issues, watching the code actually step through it finally clicked for me that the way swiftui is working is that it runs the code to render the view and then doesn't redraw the view unless something changed in the actual views, and it runs the stuff sequentially so the last thing it built was two, so if you keep tapping the two frame, nothing is changing. Not until you tap the first frame is something new in the view and it actually redraws the Content View one. I observed it when I was using a list with two different sets of data depending on row selected .

Not sure fix for your scenario, but the way I fixed mine was with a Bool flag that I passed between the views togging on/off and adding a Text(flag).hidden in the view I wanted to refresh. Will play a little more with yours

Hope this helps.

2      

I was using both @State and @Binding variables and the key was using Observable and Observed wrappers. Got it from a Paul Hudson tutorial explaing why @State does/does not work. https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject

2      

hi Alexander,

the problem is that the InfoView itself does not depend on the state.

the solution is to make the InfoView depend on the state variable with a Binding. the suggested syntax would be

  • move the enum definition outside the ContentView so that it is visible to both the ContentView and the InfoView.
  • "pass in" the infoKind enum from the ContentView to the InfoView and, importantly, do that using a Binding. be sure the InfoView then draws based on the bound enum.

    struct InfoView: View {
    @Environment(\.dismiss) var dismiss
    
    @Binding var infoKind: InfoKind
    //  let text: String
    
    var body: some View {
        NavigationView {
            VStack {
                Text(infoKind == .one ? "ONE" : "TWO")
                    .font(.title3)
                Spacer()
            }
            .navigationTitle("Info")
            .toolbar {
                Button("Dismiss") {
                    dismiss()
                }
            }
        }
    }
    }
  • rewrite the sheet modifier:
        .sheet(isPresented: $showingInfo) {
            InfoView(infoKind: $infoKind)
        }

this is a little bit difficult to explain ... when showingInfo is changed, the sheet modifier doesn't right then evaluate the logic of if ... else to decide what will be shown in quite the way you think.

BTW: if you remove the Binding in var infoKind: InfoKind (and the $ syntax needed in the call to InforView()), you'll see the behaviour you described. the Binding makes sure that when the infoKind changes the InfoView is rendered with the right title.

hope that helps,

DMG

3      

There's a similar discussion over at stackoverflow. See -> Selecting a Target Sheet View

Using guidance from that article I propose:

struct InfoView: View {
    var viewTitle: String  // <- Use better variable names!
    var body: some View {
        Text(viewTitle).font(.headline)
    }
}

struct SelectionView: View {
    @State private var selectedSheet : InfoKind?

    enum InfoKind: Identifiable {
        case one
        case two

        var id: Int { hashValue } // it's identifiable
    }

    var body: some View {
        VStack {
            // Populate the selectedSheet var to trigger the sheet.
            Button { selectedSheet = .one } label: {Text("Show First Sheet")}
                .buttonStyle(.plain)
                .frame(maxWidth: 400, maxHeight: 200)
                .background(.mint)
                .padding(.bottom)

             // Populate the selectedSheet var to trigger the sheet.
            Button { selectedSheet = .two } label: {Text("Show Second Sheet")}
                .buttonStyle(.plain)
                .frame(maxWidth: 400, maxHeight: 200)
                .background(.teal)
        }
         // Show a sheet when this item changes!
        .sheet(item: $selectedSheet) { item in
            if item == .one { InfoView(viewTitle: "View One" ) }
            else { InfoView(viewTitle: "View Two") }
        }
    }
}

2      

Thank you, @Obelix, but I am a bit puzzled by the solution you've suggested, to be honest. Also, using buttons isn't something I can do in the reall app, I have to resort to onTapGesture.

2      

Alexander follows up with:

I am a bit puzzled by the solution you've suggested, to be honest

Sorry! I used a different approach to invoking the sheet.

You can open a sheet by providing it with an object, rather than toggling a binding. You can see that I defined selectedSheet as an optional. Most of the time this optional var will contain nil.

@State private var selectedSheet : InfoKind?  // <-- Most of the time this is nil.

enum InfoKind: Identifiable {
    case one
    case two

    var id: Int { hashValue } // it's identifiable
}

However, as soon as you give the selectedSheet var a value, it will invoke the sheet. Cool!

// Show a sheet when the selectedSheet contains some data.
        .sheet(item: $selectedSheet) { item in
            if item == .one { InfoView(viewTitle: "View One" ) }
            else { InfoView(viewTitle: "View Two") }
        }

Alexander clarifies:

Also, using buttons isn't something I can do in the real app, I have to resort to onTapGesture.

How you set the selectedSheet var is up to you. Use an onTapGesture, or use a Button. Your choice. When I was testing a solution, the button provided the same mechanism without the ZStack and other excess formatting code.

I was just trying to get to the heart of the actions based on the binding value. So forgive me please, if I simplified your solution to focus on the troublesome code.

3      

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.