Last active
July 12, 2024 21:44
-
-
Save wickwirew/86fa815febd9491c501233b5e20e1aef to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import ComposableArchitecture | |
@Reducer | |
struct AppFeature { | |
@ObservableState | |
struct State { | |
var numbers: IdentifiedArrayOf<NumberFeature.State> = [.init(number: 2), .init(number: 3)] | |
@Presents var form: NumberFeature.State? | |
var numbersTotal: Int { numbers.reduce(0) { $0 + $1.number } } | |
} | |
enum Action { | |
case addNumberTapped | |
case form(PresentationAction<NumberFeature.Action>) | |
case incrementTapped(NumberFeature.State) | |
case itemTapped(NumberFeature.State) | |
case numbers(IdentifiedActionOf<NumberFeature>) | |
case saveFormTapped | |
} | |
var body: some Reducer<State, Action> { | |
Reduce { state, action in | |
switch action { | |
case .addNumberTapped: | |
state.numbers.append(NumberFeature.State()) | |
return .none | |
case .itemTapped(let number): | |
state.form = number | |
return .none | |
case .incrementTapped(var number): | |
// This is updating the state in the same way as | |
// `saveFormTapped` when it does not work, but this works. | |
number.number += 1 | |
state.numbers[id: number.id] = number | |
return .none | |
case .saveFormTapped: | |
guard let form = state.form else { return .none } | |
state.form = nil | |
// Doesn't work ❌ | |
state.numbers[id: form.id] = form | |
// Works weirdly enough ✅ | |
// state.numbers[id: form.id]?.number = form.number | |
return .none | |
case .numbers, .form: | |
return .none | |
} | |
}.forEach(\.numbers, action: \.numbers) { | |
NumberFeature() | |
}.ifLet(\.$form, action: \.form) { | |
NumberFeature() | |
} | |
} | |
} | |
@Reducer | |
struct NumberFeature { | |
@ObservableState | |
struct State: Identifiable { | |
let id = UUID() | |
var number = 0 | |
} | |
enum Action { | |
case incrementTapped | |
} | |
var body: some Reducer<State, Action> { | |
Reduce { state, action in | |
switch action { | |
case .incrementTapped: | |
state.number += 1 | |
return .none | |
} | |
} | |
} | |
} | |
struct SumView: View { | |
@Bindable var store: StoreOf<AppFeature> | |
// Local state value just to force a rerender so | |
// we don't have to edit the state via TCA | |
@State var forceUpdateCount = 0 | |
var body: some View { | |
List { | |
Text("Sum: \(store.numbersTotal)") | |
}.toolbar { | |
ToolbarItem(placement: .confirmationAction) { | |
Button("Force Update \(forceUpdateCount)") { | |
forceUpdateCount += 1 | |
} | |
} | |
} | |
} | |
} | |
struct NumbersListView: View { | |
@Bindable var store: StoreOf<AppFeature> | |
var body: some View { | |
List { | |
Text("Sum: \(store.numbersTotal)") | |
Section("Numbers") { | |
ForEach(store.numbers) { subItem in | |
HStack { | |
Button { | |
store.send(.itemTapped(subItem)) | |
} label: { | |
HStack { | |
Text("\(subItem.number)") | |
Spacer() | |
} | |
} | |
Button("Increment") { | |
store.send(.incrementTapped(subItem)) | |
} | |
}.buttonStyle(.borderless) | |
} | |
} | |
}.toolbar { | |
ToolbarItem(placement: .topBarTrailing) { | |
Button("Add", systemImage: "plus") { | |
store.send(.addNumberTapped) | |
} | |
} | |
}.sheet(item: $store.scope(state: \.form, action: \.form)) { formStore in | |
NavigationStack { | |
NumberFormView(store: formStore) | |
.toolbar { | |
ToolbarItem(placement: .confirmationAction) { | |
Button("Save") { | |
store.send(.saveFormTapped) | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
struct NumberFormView: View { | |
@Bindable var store: StoreOf<NumberFeature> | |
var body: some View { | |
Form { | |
Text("\(store.number)") | |
Button("Increment") { | |
store.send(.incrementTapped) | |
} | |
} | |
} | |
} | |
/// Issue was originally found in `NavigationSplitView` but I've simplified it | |
/// to a custom `SplitView` to help with clarity | |
struct SplitView: View { | |
@Bindable var store: StoreOf<AppFeature> | |
var body: some View { | |
HStack { | |
NavigationStack { | |
SumView(store: store) | |
}.frame(width: 350) | |
NavigationStack { | |
NumbersListView(store: store) | |
} | |
} | |
} | |
} | |
#Preview { | |
SplitView(store: Store(initialState: .init()) { AppFeature() }) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment