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.3k stars 1.43k forks source link

Too many cases in path enum #3218

Closed jaanussiim closed 3 months ago

jaanussiim commented 3 months ago

Description

This is related to following discussion https://github.com/pointfreeco/swift-composable-architecture/discussions/3162

Currently in my project, have to run in release mode on local device while developing.

Sample code to reproduce the issue

import ComposableArchitecture
import SwiftUI

@main
struct TheComposablePathApp: App {
  @Bindable var store = Store(initialState: Application.State(), reducer: Application.init)

  var body: some Scene {
    WindowGroup {
      NavigationStack(
        path: $store.scope(state: \.path, action: \.path),
        root: {
          RootView(store: store.scope(state: \.rootState, action: \.root))
        },
        destination: { store in
          switch store.case {
          case .child(let store):
            ChildView(store: store)
          default:
            EmptyView()
          }
        }
      )
    }
  }
}

@Reducer
public struct Application {
  @ObservableState
  public struct State: Equatable {
    internal var rootState = Root.State()
    internal var path = StackState<Path.State>()

    public init() {

    }
  }

  public enum Action {
    case path(StackAction<Path.State, Path.Action>)
    case root(Root.Action)

    case removeElement
  }

  public init() {

  }

  @Dependency(\.continuousClock) var clock

  public var body: some ReducerOf<Self> {
    Reduce {
      state, action in

      switch action {
      case .path(.element(let id, action: .child(.drop))):
        return .none

      case .path(.element(let id, action: .child(.push))):
        state.path.append(.child(Child.State(number: state.path.ids.count)))
        return .none

      case .root(.push):
        state.path.append(.child(Child.State(number: state.path.ids.count)))
        return .none

      case .removeElement:
        let removed = state.path.ids[state.path.ids.count - 2]
        guard let index = state.path.ids.firstIndex(of: removed) else {
          return .none
        }

        state.path.removeSubrange(index...index)
        return .none

      case .path:
        return .none

      case .root:
        return .none
      }
    }
    ._printChanges()
    .forEach(\.path, action: \.path)
    Scope(state: \.rootState, action: \.root) {
      Root()
    }
  }

  @Reducer(state: .equatable)
  public enum Path {
    case child(Child)
    case child2(Child)
    case child3(Child)
    case child4(Child)
    case child5(Child)
    case child6(Child)
    case child7(Child)
    case child8(Child)
    case child9(Child)
    case child10(Child)
    case child11(Child)
    case child12(Child)
    case child13(Child)
    case child14(Child)
    case child15(Child)
    case child16(Child)
    case child17(Child)
    case child18(Child)
    case child19(Child)
    case child110(Child)
    case child21(Child)
    case child22(Child)
    case child23(Child)
    case child24(Child)
    case child25(Child)
    case child26(Child)
    case child27(Child)
    case child28(Child)
    case child29(Child)
    case child210(Child)
    case child31(Child)
    case child32(Child)
    case child33(Child)
    case child34(Child)
    case child35(Child)
    case child36(Child)
    case child37(Child)
    case child38(Child)
    case child39(Child)
    case child310(Child)
    case child41(Child)
    case child42(Child)
    case child43(Child)
    case child44(Child)
    case child45(Child)
    case child46(Child)
    case child47(Child)
    case child48(Child)
    case child49(Child)
    case child410(Child)
    case featureCat(FeatureCat)
    case featureDog(FeatureDog)
    case featureFish(FeatureFish)
    case featureBird(FeatureBird)
    case featureTree(FeatureTree)
    case featureLeaf(FeatureLeaf)
    case featureStar(FeatureStar)
    case featureMoon(FeatureMoon)
    case featureSun(FeatureSun)
    case featureSky(FeatureSky)
    case featureHill(FeatureHill)
    case featureRock(FeatureRock)
    case featureRiver(FeatureRiver)
    case featureLake(FeatureLake)
    case featureBoat(FeatureBoat)
    case featureShip(FeatureShip)
    case featureWave(FeatureWave)
    case featureWind(FeatureWind)
    case featureRain(FeatureRain)
    case featureSnow(FeatureSnow)
    case featureCloud(FeatureCloud)
    case featureStorm(FeatureStorm)
    case featureFog(FeatureFog)
    case featureMist(FeatureMist)
    case featureDew(FeatureDew)
    case featureFrost(FeatureFrost)
    case featureIce(FeatureIce)
    case featureFire(FeatureFire)
    case featureAsh(FeatureAsh)
    case featureEmber(FeatureEmber)
    case featureFlame(FeatureFlame)
    case featureGlow(FeatureGlow)
    case featureSpark(FeatureSpark)
    case featureBolt(FeatureBolt)
    case featureFlash(FeatureFlash)
    case featureBlaze(FeatureBlaze)
    case featureLava(FeatureLava)
    case featureSmoke(FeatureSmoke)
    case featureSteam(FeatureSteam)
    case featureSand(FeatureSand)
    case featureClay(FeatureClay)
    case featureStone(FeatureStone)
    case featureGem(FeatureGem)
    case featureJewel(FeatureJewel)
    case featureGold(FeatureGold)
    case featureIron(FeatureIron)
    case featureCopper(FeatureCopper)
    case featureBronze(FeatureBronze)
    case featureSilver(FeatureSilver)
    case featurePlatinum(FeaturePlatinum)
    case featureMercury(FeatureMercury)
    case featureZinc(FeatureZinc)
    case featureBrass(FeatureBrass)
    case featureSteel(FeatureSteel)
    case featureTin(FeatureTin)
  }
}

@Reducer
public struct Root {
  public struct State: Equatable {
    public init() {

    }
  }

  public enum Action {
    case push
  }

  public init() {

  }

  public var body: some ReducerOf<Self> {
    Reduce {
      state, action in

      switch action {
      case .push:
        return .none
      }
    }
  }
}

public struct RootView: View {
  private let store: StoreOf<Root>

  public init(store: StoreOf<Root>) {
    self.store = store
  }

  public var body: some View {
    Button(action: { store.send(.push) }, label: {
      Text("Push")
    })
  }
}

import ComposableArchitecture

@Reducer
public struct Child {
  @ObservableState
  public struct State: Equatable {
    let number: Int
    public init(number: Int) {
      self.number = number
    }
  }

  public enum Action {
    case drop
    case push
  }

  public init() {

  }

  public var body: some ReducerOf<Self> {
    Reduce {
      state, action in

      switch action {
      case .drop:
        return .none

      case .push:
        return .none
      }
    }
  }
}

import ComposableArchitecture
import SwiftUI

public struct ChildView: View {
  private let store: StoreOf<Child>

  public init(store: StoreOf<Child>) {
    self.store = store
  }

  public var body: some View {
    VStack {
      Text(String(describing: store.number))
      Button(action: { store.send(.push) }, label: {
        Text("Push")
      })
      Button(action: { store.send(.drop) }, label: {
        Text("Drop")
      })
    }
  }
}

@Reducer
public struct FeatureOne {

}

public struct FeatureOneView: View {
  public var body: some View {
    Text("FeatureViewOne")
  }
}

@Reducer
public struct FeatureCat {

}

@Reducer
public struct FeatureDog {

}

@Reducer
public struct FeatureFish {

}

@Reducer
public struct FeatureBird {

}

@Reducer
public struct FeatureTree {

}

@Reducer
public struct FeatureLeaf {

}

@Reducer
public struct FeatureStar {

}

@Reducer
public struct FeatureMoon {

}

@Reducer
public struct FeatureSun {

}

@Reducer
public struct FeatureSky {

}

@Reducer
public struct FeatureHill {

}

@Reducer
public struct FeatureRock {

}

@Reducer
public struct FeatureRiver {

}

@Reducer
public struct FeatureLake {

}

@Reducer
public struct FeatureBoat {

}

@Reducer
public struct FeatureShip {

}

@Reducer
public struct FeatureWave {

}

@Reducer
public struct FeatureWind {

}

@Reducer
public struct FeatureRain {

}

@Reducer
public struct FeatureSnow {

}

@Reducer
public struct FeatureCloud {

}

@Reducer
public struct FeatureStorm {

}

@Reducer
public struct FeatureFog {

}

@Reducer
public struct FeatureMist {

}

@Reducer
public struct FeatureDew {

}

@Reducer
public struct FeatureFrost {

}

@Reducer
public struct FeatureIce {

}

@Reducer
public struct FeatureFire {

}

@Reducer
public struct FeatureAsh {

}

@Reducer
public struct FeatureEmber {

}

@Reducer
public struct FeatureFlame {

}

@Reducer
public struct FeatureGlow {

}

@Reducer
public struct FeatureSpark {

}

@Reducer
public struct FeatureBolt {

}

@Reducer
public struct FeatureFlash {

}

@Reducer
public struct FeatureBlaze {

}

@Reducer
public struct FeatureLava {

}

@Reducer
public struct FeatureSmoke {

}

@Reducer
public struct FeatureSteam {

}

@Reducer
public struct FeatureSand {

}

@Reducer
public struct FeatureClay {

}

@Reducer
public struct FeatureStone {

}

@Reducer
public struct FeatureGem {

}

@Reducer
public struct FeatureJewel {

}

@Reducer
public struct FeatureGold {

}

@Reducer
public struct FeatureIron {

}

@Reducer
public struct FeatureCopper {

}

@Reducer
public struct FeatureBronze {

}

@Reducer
public struct FeatureSilver {

}

@Reducer
public struct FeaturePlatinum {

}

@Reducer
public struct FeatureMercury {

}

@Reducer
public struct FeatureZinc {

}

@Reducer
public struct FeatureBrass {

}

@Reducer
public struct FeatureSteel {

}

@Reducer
public struct FeatureTin {

}

Checklist

Expected behavior

No response

Actual behavior

No response

Steps to reproduce

No response

The Composable Architecture version information

1.11.2

Destination operating system

17.5.1

Xcode version information

15.4

Swift Compiler version information

No response

mbrandonw commented 3 months ago

Hi @jaanussiim, I'm sorry you are running into issues, but also I'm not really sure there is much we can do about this. No matter what there are always going to be limitations of what Swift can handle. An enum with 105 cases is quite extreme, and if Swift is having problems with it I'm not sure there is much we can do. We do have some plans in the future that will help eliminate some stack frames from highly composed features, but even then you will still be able to run into the problem if you add even more cases to your enum.

I'd highly recommend simplifying this domain. Are all 105 features truly distinct from each other, or could some perhaps be combined and hold onto configuration state that controls their behavior? And if all 105 features are truly distinct, then you may need to just not use our stack navigation tools. You may be better off just using a vanilla NavigationStack with NavigationPath.

Another idea that comes to mind is to mark the State and Action enums as indirect so that they are heap allocated instead of stack allocated. In order to test that you would need to copy and paste the code generated by @Reducer and then prefix the enums with indirect. If that works then we could consider building support for that into @Reducer, but also this should be considered a bit of a hack. The real problem is that your domain is just not modeled in a way that scales.

I am going to convert this to a discussion because there really isn't much we can do in the near term about this.