swiftuireactive-programmingswiftui-stateswiftui-picker

Whats the best way to get SwiftUI Picker() selection to update on app state change?


I'm trying to make a UI with SwiftUI that lists a collection of things that can be of different kinds and can each be updated. I'd like to make the type settable in the UI with a Picker, and I also want the view to update when the item is modified some other way (say, from another part of the UI, or over the network.)

I like a "redux"-style setup, and I don't want to jettison that.

Here's a simple example that shows two items, each with a "randomize" button that changes the item at random and a Picker that lets you choose the new item type. The latter works as expected: the Picker changes the @State var, the store gets updated, etc. The 'randomize' button updates the store and the let property label, but the @State and the Picker don't update.

I would love some advice on good ways to get this to work the way I want.

import SwiftUI
import Combine

@main
struct PuffedWastApp: App {
    var store = Store()
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(store)
        }
    }
}

enum ItemState:String, CaseIterable {
    case apple
    case bannana
    case coconut
}

enum Action {
    case set(Int,ItemState)
}

class Store: ObservableObject {

    @Published var state:[ItemState] = [.apple, .apple]

    func reduce(_ action:Action) {
        print("do an action")
        switch (action) {
        case let .set(index,new_state):
            print("set \(index) to \(new_state)")
            state[index] = new_state
            self.objectWillChange.send()
        }
    }
    
}

struct ContentView: View {
    @EnvironmentObject var store: Store

    var body: some View {
        VStack {
            ForEach(store.state.indices) { index in
                ItemContainer(index: index)
                //Text("\(index)")
            }
        }
        .padding()
    }
}

struct ItemContainer: View {
    @EnvironmentObject var store: Store
    let index: Int

    
    var body: some View {
        ItemView(
            index: index,
            label: store.state[index],   // let property:  updates on change in the Store
            localLabel: store.state[index],  //@State variable; doesn't update on change in the Store
            dispatch: store.reduce
        )
    .padding()
    }
}

struct ItemView: View {
    let index: Int
    
    let label: ItemState
    @State var localLabel: ItemState
    
    let dispatch: (Action)->()
    
    var body: some View {
        HStack{
            //Text("\(index)")
            Text(label.rawValue)
            Text(localLabel.rawValue)
            Button("Randomize") { dispatch( .set(index, ItemState.allCases.randomElement() ?? .apple ) ) }
            Picker("Item type", selection: $localLabel ) {
                ForEach( ItemState.allCases , id: \.self ) {
                    Text($0.rawValue).tag($0)
                }
            }.onChange(of: localLabel) { dispatch(.set(index, $0)) }
        }
        .padding()
    }
}

Solution

  • Try changing this line:

    @State var localLabel: ItemState

    to

    @Binding var localLabel: ItemState

    and pass it in your ItemView init as:

    ItemView(
        index: index,
        label: store.state[index],
        localLabel: $store.state[index],
        dispatch: store.reduce
    )