iSpain17
iSpain17

Reputation: 3063

Changing @State variable does not update the View in SwiftUI

I have a following View (took out irrelevant parts):

struct Chart : View {
    var xValues: [String]
    var yValues: [Double]
    @State private var showXValues: Bool = false

    var body = some View {
        ...
        if showXValues {
            ...
        } else {
            ...
        }
        ...
    }
}

then I wanted to add a way to modify this value from outside, so I added a function:

func showXValues(show: Bool) -> Chart {
    self.showXValues = show
    return self
}

so I build the Chart view from the outside like this:

Chart(xValues: ["a", "b", "c"], yValues: [1, 2, 3])
    .showXValues(true)

but it works as if the value was still false. What am I doing wrong? I thought updating an @State variable should update the view. I am pretty new to Swift in general, more so to SwiftUI, am I missing some kind of special technique that should be used here?

Upvotes: 28

Views: 43266

Answers (4)

LiLi Kazine
LiLi Kazine

Reputation: 223

Modify @State property ouside of your view will not update the displaying UI.

There's quite a few approches to achieve this optional chain like pattern.

  1. Mark those optional params as your view's id.

    struct Chart: View {

        let param: Param    

        init(param: Param) {...}    

        var body: some View {    
            ...    
                .id(param)    
        }    
    }
    
    extension Chart {
    
        func update(value: Value) -> Self {    
            param.value = value    
            return self    
        }    
    }
     

ref: https://github.com/onevcat/Kingfisher See how they do about KFOptionSetter

  1. Warp up your view an extra layer

    struct Chart: View {

        let value: Value    

        init(value: Value) {...}    

        var body: some View {    
            ...    
        }    
    }
    
    extension Chart {
    
        func update(value: Value) -> Self {    
            return Chart(value: value)
        }    
    }
     

If your view request seveal params, you can always provide convenient methods.

Upvotes: 0

Jinwoo Kim
Jinwoo Kim

Reputation: 621

func showXValues(show: Bool) -> Chart {
    var copy = self
    copy._showXValues = .init(wrappedValue: show)
    return copy
}

Upvotes: 2

iSpain17
iSpain17

Reputation: 3063

There is no need to create func-s. All I have to do is not mark the properties as private but give them an initial value, so they're gonna become optional in the constructor. So user can either specify them, or not care. Like this:

var showXLabels: Bool = false

This way the constructor is either Chart(xLabels:yLabels) or Chart(xLabels:yLabels:showXLabels).

Question had nothing to do with @State.

Edit, a few years later

Actually, there is an even cleaner way to solve this for binary options, like show/hide stuff. For more than 2-way configurations, distinct arguments are still the way.

The point is that OptionSets are intended for exactly this use case, and they can be found throughout Apple frameworks like Foundation etc.

So, we are just gonna add a Chart.Options struct:

extension Chart {
    struct Options: OptionSet {
        let rawValue: Int

        static var showXValueLabels = Self(rawValue: 1 << 0)
        static var showYValueLabels = Self(rawValue: 1 << 1)
        [... add more options if you want]
    }
}

Then, the View itself remains more compact, because it will only have a single variable for binary settings:

struct Chart: View {
    var xValues: [String]
    var yValues: [Int]
    var options: Chart.Options = []

    var body: some View {
        VStack {
            Text("A chart")

            if options.contains(.showXValueLabels) {
                [... whatever xValue label specific view]
            }

            if options.contains(.showYValueLabels) {
                [... whatever yValue label specific view]
            }
        }
    }
}

And you can construct a Chart like this:

Chart(xValues: ["a", "b", "c"],
      yValues: [1, 2, 3],
      options: [.showXValueLabels, .showYValueLabels])

or

Chart(xValues: ["a", "b", "c"],
      yValues: [1, 2, 3],
      options: [.showXValueLabels])

or just use the default options:

Chart(xValues: ["a", "b", "c"],
      yValues: [1, 2, 3])

Upvotes: 3

Unpunny
Unpunny

Reputation: 172

As in the comments mentioned, @Binding is the way to go.

Here is a minimal example that shows the concept with your code:

struct Chart : View {
    var xValues: [String]
    var yValues: [Double]
    @Binding var showXValues: Bool

    var body: some View {
        if self.showXValues {
            return Text("Showing X Values")
        } else {
            return Text("Hiding X Values")
        }
    }
}

struct ContentView: View {
    @State var showXValues: Bool = false

    var body: some View {
        VStack {
            Chart(xValues: ["a", "b", "c"], yValues: [1, 2, 3], showXValues: self.$showXValues)
            Button(action: {
                self.showXValues.toggle()
            }, label: {
                if self.showXValues {
                    Text("Hide X Values")
                }else {
                    Text("Show X Values")
                }
            })
        }
    }
}

Upvotes: 10

Related Questions