pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.22k stars 1.42k forks source link

The child of IfLetStore doesn't update together with the store. #3242

Closed IvanMukticcc closed 1 month ago

IvanMukticcc commented 1 month ago

Description

I I have an iPad Split View with a list of items that trigger changes in an IfLetStore, and the state changes as expected. However, the child view inside the IfLetStore does not update accordingly.

Checklist

Expected behavior

When selecting a new store, both the store and its children should update.

Actual behavior

  1. Selecting the first store updates both the store and the children.
  2. Selecting another store updates just the store.
  3. When the store is set to nil and selected again, both the store and the children are updated.

https://github.com/user-attachments/assets/fd8d27a8-898d-44fc-a4bd-7ae74f019382

Steps to reproduce

I made simple app to reproduce the bug, here is the code:

import ComposableArchitecture import SwiftUI

struct MainReducer: Reducer { struct State: Equatable { var firstChildState: FirstChildReducer.State? }

enum Action: Equatable {
    case firstChildAction(FirstChildReducer.Action)
    case selectFirstChildState(FirstChildReducer.State?)
}

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case let .selectFirstChildState(newState):
            state.firstChildState = newState
            return .none

        case .firstChildAction(.secondChildAction(.playButtonTapped)):
            return .send(.selectFirstChildState(nil))

        case .firstChildAction:
            return .none
        }
    }
    .ifLet(\.firstChildState, action: /Action.firstChildAction) {
        FirstChildReducer()
    }
}

}

struct MainView: View { var store: StoreOf

var body: some View {
    WithViewStore(store, observe: { $0 }) { viewStore in
        HStack {
            List {
                ForEach(1 ..< 5, id: \.self) { num in
                    Button {
                        viewStore.send(.selectFirstChildState(.init(number: num)))
                    } label: {
                        Text("Num: \(num)")
                    }
                }
            }
            if viewStore.firstChildState != nil {
                IfLetStore(
                    store
                        .scope(state: \.firstChildState, action: MainReducer.Action.firstChildAction)
                ) { store in
                    FirstChildView(store: store)
                        .frame(width: UIScreen.main.bounds.width * 0.66)
                }
            } else {
                Text("Select State")
                    .frame(width: UIScreen.main.bounds.width * 0.66)
            }
        }
    }
}

}

struct FirstChildView: View { var store: StoreOf

var body: some View {
    WithViewStore(store, observe: { $0 }) { viewStore in
        VStack {
            SecondChildView(
                store: store
                    .scope(state: \.secondChildState, action: FirstChildReducer.Action.secondChildAction)
            )
            Spacer()
            Text("Hello, World! \(viewStore.number)")
                .font(.system(size: 30, weight: .bold))
            Spacer()
        }
    }
}

}

struct FirstChildReducer: Reducer { struct State: Equatable, Identifiable { var id: UUID = .init() var number: Int

    var secondChildState = SecondChildReducer.State()
}

enum Action: Equatable {
    case secondChildAction(SecondChildReducer.Action)
}

var body: some ReducerOf<Self> {
    Scope(state: \.secondChildState, action: /Action.secondChildAction) {
        SecondChildReducer()
    }

    Reduce { _, action in
        switch action {
        case .secondChildAction:
            return .none
        }
    }
}

}

struct SecondChildView: View { var store: StoreOf

var body: some View {
    WithViewStore(store, observe: { $0 }) { viewStore in
        ZStack {
            Rectangle()
                .frame(height: UIScreen.main.bounds.height * 0.33)
            Button {
                viewStore.send(.playButtonTapped)
            } label: {
                Image(systemName: "play")
                    .resizable()
                    .frame(width: 36, height: 40)
                    .foregroundStyle(Color.white)
                    .rotationEffect(.degrees(viewStore.angle))
                    .animation(.easeInOut(duration: 1), value: viewStore.angle)
            }
            Spacer()
        }
        .onAppear {
            viewStore.send(.angleChanged(to: 0))
        }
    }
}

}

struct SecondChildReducer: Reducer { struct State: Equatable { var angle = 90.0 }

enum Action: Equatable {
    case playButtonTapped
    case angleChanged(to: Double)
}

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .playButtonTapped:
            return .none
        case let .angleChanged(newAngle):
            state.angle = newAngle
            return .none
        }
    }
}

}

Preview {

MainView(store: Store(initialState: MainReducer.State(), reducer: {}))

}

The Composable Architecture version information

from: 1.5.0

Destination operating system

iOS 16.0

Xcode version information

15.2

Swift Compiler version information

swift-driver version: 1.87.3 Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
Target: arm64-apple-macosx14.0
mbrandonw commented 1 month ago

Hi @IvanMukticcc, can you please provide a project that demonstrates the behavior? The code you provided has formatting problems.

Also it is not clear from your video what exactly is not working correctly. Can you please clearly state the steps to reproduce the problem, and describe precisely what the problem is.

Since it's not clear that this is a problem with the library I am going to convert it to a discussion. Please feel free to continue the conversation over there.