Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to set @State based on Flux state #11

Open
ghost opened this issue May 8, 2020 · 6 comments
Open

How to set @State based on Flux state #11

ghost opened this issue May 8, 2020 · 6 comments

Comments

@ghost
Copy link

ghost commented May 8, 2020

For example, in order to display a list of movie timeline via a Segmented Tab View, I can a list of timeline via store.state.moviesState.movies, and also I can get the nearest future one as active/selected tab item.

My question then is how I can set this value to my @State. Seem Flux like state is not designed for @State and @binding

@State var activeSegmentedControlTabIndex: Int = 0

MySegmentedControl(tabData: timelineData, activeSegmentedControlTabIndex: $activeSegmentedControlTabIndex)

image

@d4rkd3v1l
Copy link

Same/similar issue here, using DatePicker.

When using a DatePicker I have to provide a @State/@Binding property in order to make it work in SwiftUI. So basically as expected, it'll display the initial value and also change it accordingly, when the picker is being used.

But how do I make this play nicely with a SwiftUIFlux? Problem is, that I can indeed kind of set the @State property via props (which is not quite nice, but works so far), and also I can try to change (dispatch) my state, using .onChange of the DatePicker. However .onChange is triggered immediately once one starts changing the date. And therefore the "redux update cycle" gets triggered, the View refreshed, and the "editing view of the picker" is being directly dismissed.
Unfortunately there doesn't seem to be another callback like "onFinished" or "onEditingEnded" on DatePicker.

I spent almost the whole day, but didn't find a solution for that issue. But I have do admit, that I'm pretty new to SwiftUI, so may be there is a solution out there 🤞

Any clues, help are highly appreciated!

@danhalliday
Copy link

danhalliday commented Dec 27, 2020

Hi @d4rkd3v1l, probably the way you want to handle this is to embrace SwiftUI’s built-in components’ use of Bindings, and just provide one directly. Here’s an example for a boolean I happened to have in an open project:

var body: some View {
    Text("My View").sheet(isPresented: debugSheetBinding) {
        DebugScreen()
    }
}

var debugSheetBinding: Binding<Bool> {
    .init(
        get: { store.state.debugSheetIsVisible },
        set: { store.dispatch(action: $0 ? ShowDebugSheet() : HideDebugSheet()) }
    )
}

Your method of accessing the app state and dispatching actions may vary (here I just have the store as an @EnvironmentObject in the view), but basically you want to dispatch an action in the Binding’s set closure that will cause the value read in get to match what has just been set in the view, whether by text input, or a picker, or in my case here by dismissal of a sheet.

@d4rkd3v1l
Copy link

d4rkd3v1l commented Feb 17, 2021

Thx @danhalliday this seems to work pretty well using the "@EnvironmentObject approach". Actually I went with the "ConnectedView approach" as this really feels more redux and is the "new preferred way" according to the docs here.

I was just wondering, what would be a proper way to solve it using that approach. My problem here is, that I basically only have the "props" in the body function, but not for the binding. For sure I could just access the global store instance, but that's even less redux, and also I will try to encapsulate it somehow to not be able to globally reference it.

Any ideas for that approach as well?

@danhalliday
Copy link

I think that works, though I haven’t played with ConnectedViews myself — can you just add a Binding<Bool> to your props?

@d4rkd3v1l
Copy link

d4rkd3v1l commented Feb 17, 2021

Thx @danhalliday (again) 🤣👌

Seems indeed to be working like follows. Not sure if I like the Binding init in map, but that would be quite easy to pull it out.

struct Props {
    let timeEntryBinding: (Int) -> Binding<TimeEntry>
}

func map(state: AppState, dispatch: @escaping DispatchFunction) -> Props {
    return Props(timeEntryBinding: { index in Binding<TimeEntry>(get: { state.timeState.timeEntries.forDay(self.day)[index] },
                                                                 set: { dispatch(UpdateTimeEntry(timeEntry: $0)) }) })
}

@skywalkerlw
Copy link

I give an example with possible clearer way with Combine

struct SystemState: FluxState {
    var checker = SystemChecker()
}

// @Published is available in class, so we created this
class SystemChecker: Codable {
    
    // MARK: - Input
    @Published var shouldShowLoginPopover = false

    // MARK: - Output and will be called on onReceive

    var shouldShowLoginPopoverPublisher: AnyPublisher<Bool?, Never> {
        $shouldShowLoginPopover
            .removeDuplicates()
            .map { value -> Bool? in
                value
            }.eraseToAnyPublisher()
    }
}

shouldShowLoginPopover can be set by reducer or directly in view via props.systemChecker.shouldShowLoginPopover
shouldShowLoginPopoverPublisher is called in a way like

 ZStack {
            buildHomeView().edgesIgnoringSafeArea(.top)
        }
        .onReceive(props.systemChecker.shouldShowLoginPopoverPublisher, perform: { value in
            self.showLoginPopover = value ?? false
        })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants