P Kuijpers
P Kuijpers

Reputation: 1653

SwiftUI set state variables through another view instance

In SwiftUI I've created a struct that should create different overlay views depending on some state variables. If any of the state booleans is true, then it should return custom view (either ErrorOverlay or LoadingOverlay or else an EmptyView) like this:

struct OverlayContainer: View {
    @State var isLoading: Bool = false
    @State var isErrorShown: Bool = false

    func setIsLoading(isLoading: Bool) {
        self.isLoading = isLoading
    }

    func setIsErrorShown(isErrorShown: Bool) {
        self.isErrorShown = isErrorShown
    }

    var body: some View {
        Group {
            if(isErrorShown) {
                ErrorOverlay()
            }
            else if(isLoading) {
                LoadingOverlay()
            }
            else {
                EmptyView()
            }
        }
    }
}

Now I've implemented the overlay on some content in the Home view with buttons that should change the state and show the correct overlay, like this:

struct Home: View {

    var body: some View {
        let overlayContainer = OverlayContainer()

        return HStack {
            // Some more content here

            Button(action: {
                overlayContainer.setIsLoading(isLoading: true)
            }) {
               Text("Start loading")
            }
            Button(action: {
                overlayContainer.setIsErrorShown(isErrorShown: true)
            }) {
               Text("Show error")
            }
        }.overlay(overlayContainer)
    }
}

This isn't working: when I click the button nothing happens. Why and how to solve this? (without using binding, see below)


ps. I've been able to get a working solution by doing the following:

  1. extracting the state booleans to the Home view
  2. pass these through the constructor of the OverlayContainer
  3. change the state booleans instead of calling the set methods when clicking the buttons
  4. change the OverlayContainer so it implements an init method with both booleans
  5. change the state booleans in the OverlayContainer to bindings.

However, I'd like to implement the states in the OverlayContainer to be able to re-use that in different screens, without implementing state variables in all of these screens. Firstly because there will probably be more cases than just these 2. Secondly because not all screens will need to access all states and I haven't found out a simple way to implement optional bindings through the init method.

To me it feels that all these states belong to the OverlayContainer, and changing the state should be as short and clean as possible. Defining states everywhere feels like code duplication. Maybe I need a completely different architecture?

Upvotes: 3

Views: 3427

Answers (2)

Kuhlemann
Kuhlemann

Reputation: 3396

To make it the way you want, use Binding:

struct OverlayContainer: View {

    @Binding var isLoading: Bool
    @Binding var isErrorShown: Bool

    func setIsLoading(isLoading: Bool) {
        self.isLoading = isLoading
        self.isErrorShown = !isLoading
    }

    func setIsErrorShown(isErrorShown: Bool) {
        self.isErrorShown = isErrorShown
        self.isLoading = !isErrorShown
    }

    var body: some View {
        Group {
            if(isErrorShown) {
                ErrorOverlay()
            }
            else if(isLoading) {
                LoadingOverlay()
            }
            else {
                EmptyView()
            }
        }
    }
}

struct Home: View {

    @State var isLoading = false
    @State var isErrorShown = false

    var body: some View {

        let overlayContainer = OverlayContainer(isLoading: $isLoading, isErrorShown: $isErrorShown)

        return HStack {
            // Some more content here

            Button(action: {
                overlayContainer.setIsLoading(isLoading: true)

            }) {
                Text("Start loading")
            }
            Button(action: {
                overlayContainer.setIsErrorShown(isErrorShown: true)
            }) {
                Text("Show error")
            }
        }.overlay(overlayContainer)
    }
}

Upvotes: 0

Asperi
Asperi

Reputation: 257563

It should be used Binding instead. Here is possible solution.

struct OverlayContainer: View {
    @Binding var isLoading: Bool
    @Binding var isErrorShown: Bool

    var body: some View {
        Group {
            if(isErrorShown) {
                ErrorOverlay()
            }
            else if(isLoading) {
                LoadingOverlay()
            }
            else {
                EmptyView()
            }
        }
    }
}

struct Home: View {
    @State var isLoading: Bool = false
    @State var isErrorShown: Bool = false

    var body: some View {
        HStack {
            // Some more content here

            Button(action: {
                self.isLoading = true
            }) {
               Text("Start loading")
            }
            Button(action: {
                self.isErrorShown = true
            }) {
               Text("Show error")
            }
        }.overlay(OverlayContainer(isLoading: $isLoading, isErrorShown: $isErrorShown))
    }
}

Upvotes: 2

Related Questions