24

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?

iSpain17
  • 2,502
  • 3
  • 17
  • 26

3 Answers3

9

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")
                }
            })
        }
    }
}
Unpunny
  • 162
  • 3
  • 3
    But I have 20 optional parameters like `showXValues` and I don't want to modify them all at all times, I want something like a builder pattern - as my example func shows. I do not want to write a 20 parameter initializer. E.g. user can tell me if they want to show X Values, but I hide them by default. – iSpain17 Dec 18 '19 at 10:48
  • 1
    you could pack all those variables in an @ObservedObject? And give them a standard value @iSpain17 – Unpunny Dec 18 '19 at 10:55
  • 11
    I find this approach unsatisfying. Only `Chart` should be concerned with `showXValues`, not `ContentView`. I don't want to push the definitions of a view's states up into its parent. – Chuck Batson Aug 05 '20 at 00:47
  • 1
    Even I don't feel this is the correct way, if a child is not updating the value then the parent should not pass it as Binding. SwiftUI takes care of re-rendering and passing the updated value to child after you update the state in parent. – Jay Mehta Jul 03 '21 at 08:25
3

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])
iSpain17
  • 2,502
  • 3
  • 17
  • 26
  • 6
    This answer has nothing to do with your original question. StackOverflow is also here to help other that may stumble on your question, you shouldn't accept answers that don't answer your question. – Tiebe Groosman Oct 17 '22 at 13:42
  • My question could be understood in two ways: 1. assigning a binding, that dynamically allows one to show/hide the xValues - this answer is the one from Unpunny. Or, 2. specifying an always standing setting, that I never intend to change - I was just a beginner in SwiftUI, and thought I always needed to add `@State` to values I want to change after the init(), via a builder pattern. Which is not true, and I didn't even need builder pattern. Both answers have upvotes, so they obviously both had a lot to do with the original question. – iSpain17 Oct 17 '22 at 14:38
0
func showXValues(show: Bool) -> Chart {
    var copy = self
    copy._showXValues = .init(wrappedValue: show)
    return copy
}
Jinwoo Kim
  • 440
  • 4
  • 7