I am trying to use Redux as you’ve shown in a small app, but I haven’t found a workable way to pass part of my store’s state as a binding and capture the selection change made by the Picker.
Edit:
Thought a little harder after posting:
struct PickerWrapperView: View {
@EnvironmentObject var store: AppStore
var body: some View {
let selectionBinding = Binding(
get: {self.store.state.selectedEntry},
set: {
self.store.dispatch(.selectionChanged($0.id))
}
)
Picker("pick", selection: selectionBinding) {
ForEach(store.state.possibleEntries, id: \.self) { entry in
Text(entry.name)
}
}
}
}
Using a custom binding seems to be working, but I would be happy to hear if there was some improvement to be made.
First of all, thanks for the awesome tutorial! I found a small bug in your final project and I can’t figure out how to (properly) fix it. Here’s how to reproduce it:
Flip a card
Flip a non-matching card
Start spamming taps on a 3rd card while the other two are still flipped and keep going until they are unflipped.
Observed: as soon as the two cards are unflipped, the third card will flip briefly then unflip.
It seems that every attempt to flip a 3rd card before the other 2 are unflipped triggers a ThreeDucksAction.unFlipSelectedCards that piles up on the sync queue of the Store. Some of these unFlipSelectedCards are then executed AFTER the third card has been flipped.
I’ve managed to prevent this by only unflipping cards if there are exactly 2 flipped cards, but this seems like a hacky fix rather than one that addresses the concurrency issue. Any ideas for a better solution?
Firstly, I don’t think your solution is hacky at all! In fact the reducer makes a similar check, that there are less than 2 selected cards, before flipping the selected card.
Other than using the reducer logic to handle this, the other end of the transaction are the taps.
In the example the reducer tries to be as rigid as possible handling actions serially and each one operating on the state as produced by the previous action.
As you’ve noticed touches will always queue up an action:
So the tap just sends the card id to the debouncer, and the debouncer removes duplicates from the stream of IDs it receives. The view watches the debouncer’s signal and sends the action.
Thanks for looking into this! Very interesting solution. I just have one question: wouldn’t the .removeDuplicates() operator prevent consecutive taps on the same card even when they are intended to work? For example:
@pduemlein Ah yes you’re right. OK we can introduce some more data to our debouncer. The selected card count varies between 0 and 2 so we could use that in our debouncer, and that would guard against it.
class TapDebounce: ObservableObject {
private struct Model: Equatable {
let uuid: UUID
let count: Int
}
var signal: AnyPublisher<UUID, Never>
private var uuidSubject = PassthroughSubject<Model, Never>()
init() {
signal = uuidSubject
.removeDuplicates()
.map { $0.uuid }
.eraseToAnyPublisher()
}
func send(_ uuid: UUID, selectedCount: Int) {
uuidSubject.send(Model(uuid: uuid, count: selectedCount))
}
}
Thanks for this great tutorial!!! My appreciation that there were no external libraries. It was easy for understanding.
I have found another bug. When there are already selected matched cards on the next step you can select the flipped card. The route cause is in Reducer:
Thanks For the great tutorial. It’s a very simple and clean way to implement Redux in the iOS projects!
However, I have one concern about putting the middleware logic within queue.sync.
Middleware handles side effects asynchronously. Execute it inside a synchronous closure seems may cause deadlock sometimes.
Do you think it would be better to put the middlewares execution just under queue.sync?
It’s been fine for me in other projects. When you use the Combine operator .receive(on: DispatchQueue.main) it’s performing a DispatchQueue.main.async { } call for you.
Nice tutorial! Everything worked well, except the GameScreenView did not show when I tapped on New Game the first time. I had to Give Up and tap it again to see it, but it was all okay when I tapped Go Again when I won. What’s causing this?
But how exactly run async operation (for example, network request) using this approach? In .sync context it actually freezes whole interface, in .async approach I cannot got error in private func dispatch(_ currentState: State, _ action: Action) at last row of function that state can be only updated from main thread. I’m dead stuck and got no idea how to fix this