keegan3d
keegan3d

Reputation: 11275

SwiftUI: Best way to update a @Binding variable made up of other @State variables

I've run into my first complex control in SwiftUI. It's a date frequency picker where the user can choose the day of the week for weekly recurring events, or the day of the month for monthly recurring events. I have @State variables to keep track of weekly vs monthly in my segment controller and also which day of the week or month the user has selected. When these values change I want to update my @Binding frequency. The code below is working, but feels dirty to me to update the frequency in the body call.

enum Frequency {
    case weekly(on: Int)
    case monthly(on: Int)
}

struct FrequencyView : View {
    @Binding var frequency: Frequency

    @State var segment: Int
    @State var weekDay: Int
    @State var monthDay: Int
    var displayFrequency: Frequency {
        if segment == 0 {
            return .weekly(on: weekDay)
        } else {
            return .monthly(on: monthDay)
        }
    }

    var body: some View {
        frequency = displayFrequency
        return NavigationView {
            VStack {
                Text(displayFrequency.long).fontWeight(.bold)
                SegmentedControl(selection: $segment) {
                    Text("Weekly").tag(0)
                    Text("Monthly").tag(1)
                }.padding()

                if segment == 0 {
                    Text("Day of the week").foregroundColor(Acorns.stone)
                    ExpensesWeeklyPickerView(day: $weekDay).padding()
                } else {
                    Text("Day of the month").foregroundColor(Acorns.stone)
                    ExpensesMonthlyPickerView(day: $monthDay).padding()
                }

                Spacer()
            }.navigationBarTitle(Text("Frequency"))
        }
    }
}

Was curious if anyone has found a better way to do this?

Upvotes: 4

Views: 3494

Answers (1)

rob mayoff
rob mayoff

Reputation: 385500

First, add accessors for weekDay and monthDay to Frequency:

extension Frequency {
    var weekDay: Int {
        get {
            if case .weekly(on: let day) = self { return day }
            else { return 0 }
        }
        set { self = .weekly(on: newValue) }
    }

    var monthDay: Int {
        get {
            if case .monthly(on: let day) = self { return day }
            else { return 0 }
        }
        set { self = .monthly(on: newValue) }
    }
}

Now you can use these accessors to create bindings for your subviews:

if segment == 0 {
    Text("Day of the week").foregroundColor(Acorns.stone)
    ExpensesWeeklyPickerView(day: $frequency.weekDay).padding()
} else {
    Text("Day of the month").foregroundColor(Acorns.stone)
    ExpensesMonthlyPickerView(day: $frequency.monthDay).padding()
}

To change the frequency when a segment is clicked, you need to give the SegmentedControl a binding that changes the frequency. I would introduce another (private) enum to use for the segment:

fileprivate enum Segment: Int, Hashable {
    case weekly
    case monthly
}

Then add an extension to Frequency to translate to/from Segment:

extension Frequency {
    fileprivate var segment: ContentView.Segment {
        get {
            switch self {
            case .weekly(on: _): return .weekly
            case .monthly(on: _): return .monthly
            }
        }
        set {
            switch newValue {
            case .weekly: self = .weekly(on: 0)
            case .monthly: self = .monthly(on: 0)
            }
        }
    }
}

Finally, get rid of your segment property and use frequency.segment instead:

        SegmentedControl(selection: $frequency.segment) {
            Text("Weekly").tag(Segment.weekly)
            Text("Monthly").tag(Segment.monthly)
        }.padding()
        if frequency.segment == .weekly {
           ...

Upvotes: 2

Related Questions