Skip to content

Instantly share code, notes, and snippets.

@wickwirew
Last active July 12, 2024 21:44
Show Gist options
  • Save wickwirew/86fa815febd9491c501233b5e20e1aef to your computer and use it in GitHub Desktop.
Save wickwirew/86fa815febd9491c501233b5e20e1aef to your computer and use it in GitHub Desktop.
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