Modeling Element Features

Let's say we show a list of Items on our home screen.

We can edit any Item by:

  1. long-pressing it (context menu) and tapping "Edit", or
  2. navigating into the details view (ForEachStore, NavigationLink) and tapping "Edit" there.

Both options present the same "Edit" sheet view that lets us modify an item, with changes propagated back up.

How would you model your state and reducers to best support this case?


Among others, I would appreciate your thoughts on this @stephencelis and @mbrandonw. One year into using TCA full-time, I love most aspects of it, but I keep running into structural challenges such as the above. I really feel we, as a community, should work on sharing and discussing more of the complex examples showcasing use-cases not covered by the rather limited examples in the TCA's GitHub repo.

Thank you.

Agreed. I ran into several situations that forced me to think. But I feel that is the part I enjoy the most about using TCA (or any unidirectional architecture for that matter); I constantly feel challenged for the right reasons. Many of the bad habits don’t work quite well or at all in TCA.

Regarding your question, I have something vaguely similar but I am unable to answer your question more concretely without knowing which specific issues you ran into.

If you can share some more details, or better yet some code snippets, I might be able to help.

1 Like

Agree. The opinionated nature of TCA is something I appreciate the most, which is why I think it's even more important to discuss the "right" ways of tackling more complex use-cases, and not push against the grain.

I've gone ahead and pushed a tiny project to GitHub, which showcases the example I explained above. The core of the project is in this file, which also includes few #warning statements that describe the issues/questions I'm running into.

I would appreciate any inputs on this. Let me know if anything is unclear. Thanks!

Yes, that's a lot better!

Indeed this is very similar to a fairly complex app that I have been working on. The way I solved is as follows (not necessarily saying it's the best way, FWIW):

First, I tried to push the sheet as high up in the view hierarchy as possible. You want to be able to bring up the sheet even when the item detail view is not there. In this particular example, you had in the root view and that works pretty well. If you'd rather move it inside the ItemsView you might want to move the list to that component instead.

Second, I define edit actions on both the detail view and on the parent where editing can be triggered from a list. You already had the latter, but the former was missing. Something along these lines:

enum ItemAction: Equatable {
  case startEditing
}

Then you can trigger this action from the button inside the details view:

Button("Edit") {
   viewStore.send(.startEditing)
}

Then, and this is the crucial bit, in your itemsReducer you need to handle this action via the case item(id: Item.ID, action: ItemAction) you already had in place. The idea is to return an effect with .showEditItem so that it follows the same path as the other one triggered from the list view.

case .item(let id, let action):
  guard action == .editItem,
  let itemState = state.itemStates[id: id] 
  else  { return .none }

  return .init(value: .showEditItem(itemState))

This should be all you need to get edit to work from both paths. I tried it and it seems to work. Again, this is how I have been doing this kind of thing, and it feels right to me, but I could be wrong.

For the EditItemState modeling question I don't think I have much to add since that would depend a lot on how this code will evolve etc.

@kaishin, thanks for taking a look and sharing your suggestion!

I've tried your suggestion in the past but was discouraged because it triggers the following warning, when triggering the presentation of the edit item view sheet from the item details view:

Presenting view controller <...> from detached view controller <...> is discouraged.

As I'm sure you know, this happens because we are presenting the sheet from the ContentView, which is not being shown at that moment.

Do you, or anyone else, have any thoughts on this? I'm a bit hesitant about this approach because it breaks the navigation guideline.

I've tried your suggestion in the past but was discouraged because it triggers the following warning, when triggering the presentation of the edit item view sheet from the item details view

That's true, but I think that's a problem even without TCA. You can always modify the view hierarchy to avoid it. For instance by attaching the sheet to the NavigationView instead.

Thanks, @kaishin. True, that would work, but what if there's another action, let's say "Share", which should also show a sheet from both the main list of items and the item details (I actually have this situation). We would then have to show two different sheets from the NavigationView, which I believe is not possible. :thinking:

Any ideas?

Oh, in those situation I make heavy use of enums. Here are some snippets from my app:

// I define `Sheet` inside of `State`
  enum Sheet: Equatable {
    case none
    case edit
    case new
  }

// Then add `currentSheet` to the state
var currentSheet: Sheet = .none

// Then a helper to make the binding cleaner
var isSheetPresented: Bool {
  currentSheet != .none
 }

// Add a `setSheet ` action
case setSheet(State.Sheet)

// Then in the view
.sheet(
  isPresented: viewStore.binding(
    get: \.isSheetPresented,
    send: { _ in .setSheet(.none) } // In reality I have another action that takes a bool but this works too
  )) {
  self.sheetContent(for: viewStore.currentSheet)
}

// Then a view builder to create the sheet content
@ViewBuilder
func sheetContent(for kind: State.Sheet) -> some View {
  switch kind {
  case .edit:
    ...
  case .new:
    ...
  case .none:
    EmptyView()
  }
}

I have used this in many scenarios and never hit any issues, warnings, or bugs.

Oh, this looks super interesting and promising! I'll give it a go, and reply if I run into any blockers.

These are the kinds of discussions I believe many can benefit from, so thank you @kaishin for being so responsive and generous with your feedback. :pray:

2 Likes