|
// |
|
// ContentView.swift |
|
// DummySelector |
|
// |
|
// Created by makmini kariv on 15/07/2025. |
|
// |
|
|
|
/// Main View (ContentView) has a customizable background color. |
|
/// It has a button that when tapped, opens a chooser bottom sheet. |
|
/// The chooser offers two options - warm colors and cold colors |
|
/// Each option opens a separate color picker with two sample colors |
|
/// The point of this contrived example to see if the chooser sheet should be using its own TCA @Reducer Feature or not |
|
|
|
|
|
import ComposableArchitecture |
|
import Foundation |
|
import SwiftUI |
|
import Tagged |
|
|
|
enum PickerOption { |
|
case warm |
|
case cold |
|
} |
|
|
|
// MARK: - ChoosePickerFeature |
|
@Reducer |
|
struct ChoosePickerFeature { |
|
|
|
// MARK: - State |
|
@ObservableState |
|
struct State: Equatable { |
|
var option: PickerOption? |
|
} |
|
// MARK: - Action |
|
enum Action { |
|
case warmOptionTapped |
|
case coldOptionTapped |
|
case delegate(Delegate) |
|
enum Delegate: Equatable { |
|
case choose(PickerOption) |
|
} |
|
} |
|
// MARK: - dependency dismiss (Removed as parent will handle dismissal) |
|
// @Dependency(\.dismiss) var dismiss // This dependency is no longer needed here |
|
|
|
// MARK: - Reducer |
|
var body: some ReducerOf<Self> { |
|
Reduce { state, action in |
|
switch action { |
|
case .coldOptionTapped: |
|
state.option = .cold |
|
return .run { send in |
|
await send(.delegate(.choose(.cold))) |
|
// IMPORTANT CHANGE: Removed `await self.dismiss()` |
|
// The parent `ColorFeature` will now handle dismissing this sheet |
|
// and presenting the next one. |
|
} |
|
case .warmOptionTapped: |
|
state.option = .warm |
|
return .run { send in |
|
await send(.delegate(.choose(.warm))) |
|
// IMPORTANT CHANGE: Removed `await self.dismiss()` |
|
// The parent `ColorFeature` will now handle dismissing this sheet |
|
// and presenting the next one. |
|
} |
|
case .delegate: |
|
// Delegate actions are sent to the parent, no local effect needed here. |
|
return .none |
|
} |
|
} |
|
} |
|
} |
|
|
|
// MARK: - ColorFeature |
|
@Reducer |
|
struct ColorFeature { |
|
// MARK: - Destination Reducer |
|
@Reducer |
|
enum Destination { |
|
case warmColor(ColorCreateFeature) |
|
case coldColor(ColorCreateFeature) |
|
case choosePicker(ChoosePickerFeature) |
|
} |
|
// MARK: - State |
|
@ObservableState |
|
struct State: Equatable { |
|
var color: Color = .orange |
|
@Presents var destination: Destination.State? |
|
} |
|
// MARK: - Action |
|
enum Action { |
|
case setColor(Color) |
|
case chooseButtonTapped |
|
case destination(PresentationAction<Destination.Action>) // tree navigation |
|
} |
|
// MARK: - Reducer |
|
var body: some ReducerOf<Self> { |
|
Reduce { state, action in |
|
switch action { |
|
case .setColor(let color): |
|
state.color = color |
|
return .none |
|
case let .destination(.presented(.coldColor(.delegate(.save(color))))): |
|
state.color = color |
|
return .none |
|
case let .destination(.presented(.warmColor(.delegate(.save(color))))): |
|
state.color = color |
|
return .none |
|
|
|
case .chooseButtonTapped: |
|
state.destination = .choosePicker(ChoosePickerFeature.State()) |
|
return .none |
|
case let .destination(.presented(.choosePicker(.delegate(.choose(option))))): |
|
// IMPORTANT CHANGE: |
|
// When the delegate action from `choosePicker` is received, |
|
// we directly set the `destination` to the next desired sheet. |
|
// SwiftUI will see this change and transition from the `choosePicker` sheet |
|
// to the new `warmColor` or `coldColor` sheet seamlessly, |
|
// without briefly showing the `ContentView` in between. |
|
if option == .cold { |
|
state.destination = .coldColor(ColorCreateFeature.State(color: state.color)) |
|
} else if option == .warm { |
|
state.destination = .warmColor(ColorCreateFeature.State(color: state.color)) |
|
} else { |
|
print("ERROR: option is unknown") |
|
} |
|
return .none // No additional side effect needed, state is updated. |
|
|
|
case .destination: |
|
// This case handles all other `PresentationAction`s, including dismissals |
|
// of the child sheets when their state becomes nil (e.g., when ColorCreateFeature calls dismiss). |
|
return .none |
|
} |
|
} |
|
// MARK: - Destination actions pass through |
|
.ifLet(\.$destination, action: \.destination) |
|
} |
|
} |
|
|
|
// MARK: - ColorCreateFeature (No changes needed here) |
|
@Reducer |
|
struct ColorCreateFeature { |
|
|
|
// MARK: - State |
|
@ObservableState |
|
struct State: Equatable { |
|
var color: Color = .orange |
|
} |
|
// MARK: - Action |
|
enum Action { |
|
case cancelButtonTapped |
|
case saveButtonTapped |
|
case setColor(Color) |
|
case delegate(Delegate) |
|
enum Delegate: Equatable { |
|
case save(Color) |
|
} |
|
} |
|
// MARK: - dependency dismiss |
|
@Dependency(\.dismiss) var dismiss |
|
// MARK: - Reducer |
|
var body: some ReducerOf<Self> { |
|
Reduce { state, action in |
|
switch action { |
|
case .cancelButtonTapped: return .run { _ in await self.dismiss() } |
|
case .saveButtonTapped: |
|
return .run { [color = state.color] send in |
|
await send(.delegate(.save(color))) |
|
await self.dismiss() |
|
} |
|
case .setColor(let color): |
|
state.color = color |
|
return .none |
|
default: return .none |
|
} |
|
} |
|
} |
|
} |
|
|
|
// MARK: - Color warmer View (No changes needed here) |
|
struct ColorWarmView: View { |
|
@Bindable var store: StoreOf<ColorCreateFeature> |
|
var body: some View { |
|
NavigationView { |
|
VStack { |
|
Button("red") { store.send(.setColor(.red)) } |
|
Button("yellow") { store.send(.setColor(.yellow)) } |
|
Button("Cancel") { store.send(.cancelButtonTapped) } |
|
Button("Save") { store.send(.saveButtonTapped) } |
|
store.color.frame(width: 50, height: 50) |
|
} |
|
} |
|
} |
|
} |
|
|
|
// MARK: - Color cold View (No changes needed here) |
|
struct ColorColdView: View { |
|
@Bindable var store: StoreOf<ColorCreateFeature> |
|
var body: some View { |
|
NavigationView { |
|
VStack { |
|
Button("green") { store.send(.setColor(.green)) } |
|
Button("cyan") { store.send(.setColor(.cyan)) } |
|
Button("Cancel") { store.send(.cancelButtonTapped) } |
|
Button("Save") { store.send(.saveButtonTapped) } |
|
store.color.frame(width: 50, height: 50) |
|
} |
|
} |
|
} |
|
} |
|
|
|
// MARK: - Picker Chooser View (No changes needed here, it correctly uses @Bindable) |
|
struct PickerChooserView: View { |
|
@Bindable var store: StoreOf<ChoosePickerFeature> |
|
var body: some View { |
|
NavigationView { |
|
VStack { |
|
Button("Warm") { store.send(.warmOptionTapped) } |
|
Button("Cold") { store.send(.coldOptionTapped) } |
|
} |
|
} |
|
} |
|
} |
|
|
|
// MARK: - Content (Main) View (No changes needed here, the logic is in the reducer) |
|
struct ContentView: View { |
|
@Bindable var store: StoreOf<ColorFeature> |
|
|
|
var body: some View { |
|
NavigationStack { |
|
VStack { |
|
Button { |
|
store.send(.chooseButtonTapped) |
|
} label: { |
|
Label("Choose Cold or Warm", systemImage: "plus") |
|
.padding().background(.black).foregroundColor(.white) |
|
} |
|
} |
|
.frame(maxWidth: .infinity, maxHeight: .infinity) |
|
.background(store.color) |
|
} |
|
// These sheets will now react to the `destination` state updates |
|
// managed by the `ColorFeature` reducer. |
|
.sheet(item: $store.scope(state: \.destination?.choosePicker, action: \.destination.choosePicker)) { store in |
|
NavigationStack { PickerChooserView(store: store) } |
|
.presentationDetents([.medium]) |
|
|
|
} |
|
|
|
.sheet(item: $store.scope(state: \.destination?.warmColor, action: \.destination.warmColor)) { store in |
|
NavigationStack { ColorWarmView(store: store) } |
|
} |
|
.sheet(item: $store.scope(state: \.destination?.coldColor, action: \.destination.coldColor)) { store in |
|
NavigationStack { ColorColdView(store: store) } |
|
} |
|
} |
|
} |
|
|
|
// MARK: - Fix Destiantion to be Equatable |
|
extension ColorFeature.Destination.State: Equatable {} |
|
|
|
// MARK: - Preview |
|
#Preview("Content View") { |
|
ContentView( |
|
store: Store(initialState: ColorFeature.State(color: .orange)) { |
|
ColorFeature() |
|
} |
|
) |
|
} |
|
|
|
#Preview("Color warmer") { |
|
ColorWarmView( |
|
store: Store(initialState: ColorCreateFeature.State(color: .orange)) { |
|
ColorCreateFeature() |
|
} |
|
) |
|
) |
|
} |