Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Design navigation logic #3662

Closed Unanswered
winnisx7 asked this question in Q&A
Apr 18, 2025 · 2 comments · 2 replies
Discussion options

Hi everyone, and thanks in advance for your help.

⚠️ English is not my first language, so please forgive any mistakes.
Also, I am already familiar with patterns such as MVVM‑C, Routers, and Coordinators, so I do not need conceptual explanations of those ideas.

• Project context
– iOS 17 minimum target
– Xcode 16.3, Swift 6.0
– TCA 1.19.1 (I have also read through parts of the TCA and swift‑navigation source code)

Despite that, I’m struggling to keep my navigation code manageable.
The current approaches feel either too verbose or too tightly coupled, and turning the logic into something modular for a future micro‑feature (micro‑service) architecture looks painful.

After weeks of experimentation my options seem to boil down to two patterns:

1️⃣ Delegate from child → handle in parent
Each child feature emits a delegate action, the parent inspects it and pushes / pops on StackState.

2️⃣ @Shared(.path)
Treat the shared StackState almost like a @Binding, allowing child features to push / pop directly.

I’m currently using pattern ①, but the boilerplate of writing delegate actions and parent‑side switch statements for every simple navigation is becoming a burden.
Pattern ② is much more pleasant, yet I worry it will block me later when I break the project into independent modules (micro‑feature architecture).

Could you point me to a real‑world example or best‑practice repo that scales to a large codebase while keeping navigation maintainable?
Any guidance or experience reports would be greatly appreciated.

import ComposableArchitecture
import SwiftUI
import UIKit

@Reducer
struct MypageScreenFeature {

  // MARK: State
  @ObservableState
  struct State: Equatable {
    var path = StackState<Path.State>()
    @Presents var destination: Destination.State?
  }

  // MARK: Action
  enum Action: BindableAction {
    enum Delegate { case presentMembership }

    case binding(BindingAction<State>)
    case delegate(Delegate)
    case path(StackActionOf<Path>)
    case destination(PresentationAction<Destination.Action>)
  }

  // MARK: Navigation
  @Reducer
  enum Path {
    case settings(SettingsScreenFeature)
    case profileSetting(ProfileSettingScreenFeature)
    case changePassword(ChangePasswordScreenFeature)
    case myMembership(MyMembershipScreenFeature)
    case membershipManage(MembershipManageScreenFeature)
    case membershipManagePayment(MembershipManagePaymentScreenFeature)
    case membershipPaymentHistory(MembershipPaymentHistoryScreenFeature)
    case membershipPaymentHistoryDetail(MembershipPaymentHistoryDetailScreenFeature)
    case myBadge(MyBadgeScreenFeature)
    case customerService(CustomerServiceFeature)
    case languageSetting(LanguageSettingScreenFeature)
    case notificationSetting(NotificationSettingScreenFeature)
    case accountDelete(AccountDeleteScreenFeature)
    case courseHistory(HistoryCourseScreenFeature)
    case learningCourse(LearningCourseFeature)
    case learningUnit(LearningUnitScreenFeature)
    case learningClassPattern(LearningClassPatternScreenFeature)
    case learningClassWord(LearningClassWordScreenFeature)
    case learningClassVideo(LearningClassVideoScreenFeature)
    case learningClassSpeaking(LearningClassSpeakingScreenFeature)
    case learningClassQuiz(LearningClassQuizScreenFeature)
    case learningClassCompleted(LearningClassCompletedScreenFeature)
    case classHistory(HistoryClassScreenFeature)
    case notification(NotificationScreenFeature)
    case notificationDetail(NotificationDetailScreenFeature)
    case terms(TermsScreenFeature)
  }

  @Reducer
  enum Destination {
    case courseSelection(LearningCoursesScreenFeature)
  }

  // MARK: Body
  var body: some Reducer<State, Action> {
    CombineReducers {
      EmptyReducer()
        .forEach(\.path, action: \.path)
        .ifLet(\.$destination, action: \.destination)

      // LearningUnit → class‑level screens
      Reduce { state, action in
        guard case .path(.element(id: _, action: .learningUnit(.delegate(let action)))) = action
        else { return .none }

        switch action {
        case let .navigateToLearningClassPattern(id):
          state.path.append(.learningClassPattern(.init(learningClassId: id)))
        case let .navigateToLearningClassWord(id):
          state.path.append(.learningClassWord(.init(learningClassId: id)))
        case let .navigateToLearningClassVideo(id):
          state.path.append(.learningClassVideo(.init(learningClassId: id)))
        case let .navigateToLearningClassSpeaking(id):
          state.path.append(.learningClassSpeaking(.init(learningClassId: id)))
        case let .navigateToLearningClassQuiz(id):
          state.path.append(.learningClassQuiz(.init(learningClassId: id)))
        }
        return .none
      }
    }
  }
}

// MARK: NavigationStackController
final class MypageNavigationStackController: NavigationStackController {

  convenience init(store: StoreOf<MypageScreenFeature>) {
    @UIBindable var store = store

    self.init(
      path: $store.scope(state: \.path, action: \.path)
    ) {
      UIHostingController(rootView: MypageScreen(store: store))
    } destination: { store in
      switch store.case {
      case .settings(let s):                       return host(SettingsScreen(store: s))
      case .profileSetting(let s):                 return host(ProfileSettingScreen(store: s))
      case .changePassword(let s):                 return host(ChangePasswordScreen(store: s))
      case .myMembership(let s):                   return host(MyMembershipScreen(store: s))
      case .membershipManage(let s):               return host(MembershipManageScreen(store: s))
      case .membershipManagePayment(let s):        return host(MembershipManagePaymentScreen(store: s))
      case .membershipPaymentHistory(let s):       return host(MembershipPaymentHistoryScreen(store: s))
      case .membershipPaymentHistoryDetail(let s): return host(MembershipPaymentHistoryDetailScreen(store: s))
      case .myBadge(let s):                        return MyBadgeScreenVC(rootView: MyBadgeScreen(store: s), store: s)
      case .customerService(let s):                return host(CustomerServiceScreen(store: s))
      case .languageSetting(let s):                return host(LanguageSettingScreen(store: s))
      case .notificationSetting(let s):            return host(NotificationSettingScreen(store: s))
      case .accountDelete(let s):                  return host(AccountDeleteScreen(store: s))
      case .courseHistory(let s):                  return host(HistoryCourseScreen(store: s))
      case .learningCourse(let s):                 return LearningCourseViewController(store: s)
      case .learningUnit(let s):                   return host(LearningUnitScreen(store: s))
      case .learningClassPattern(let s):           return host(LearningClassPatternScreen(store: s))
      case .learningClassWord(let s):              return host(LearningClassWordScreen(store: s))
      case .learningClassVideo(let s):             return LearningClassVideoScreenVC(rootView: LearningClassVideoScreen(store: s), store: s)
      case .learningClassSpeaking(let s):          return LearningClassSpeakingScreenVC(rootView: LearningClassSpeakingScreen(store: s), store: s)
      case .learningClassQuiz(let s):              return LearningClassQuizScreenVC(rootView: LearningClassQuizScreen(store: s), store: s)
      case .learningClassCompleted(let s):         return host(LearningClassCompletedScreen(store: s))
      case .classHistory(let s):                   return host(HistoryClassScreen(store: s))
      case .notification(let s):                   return host(NotificationScreen(store: s))
      case .notificationDetail(let s):             return host(NotificationDetailScreen(store: s))
      case .terms(let s):                          return host(TermsScreen(store: s))
      }
    }
  }

  private func host<V: View>(_ view: V) -> UIViewController {
    let vc = UIHostingController(rootView: view)
    vc.hidesBottomBarWhenPushed = true
    return vc
  }
}

Honestly, I don’t consider my app to be that large, yet the navigation code is already becoming hard to read and reason about.
Any help or pointers would be greatly appreciated!

You must be logged in to vote

Replies: 2 comments · 2 replies

Comment options

@mbrandonw @stephencelis
Hi, and sorry to ping you directly. I’ve been struggling to keep navigation manageable in a medium‑sized TCA codebase (details above in #3662).
After digging through the docs, episodes, and previous discussions I still haven’t found a scalable pattern.

If you have a moment, could you point me to a real‑world example or share any guidance on best practices for large‑scale navigation?
Thank you so much for your time and for everything you do for the community! 🙏

You must be logged in to vote
0 replies
Comment options

@winnisx7 have you ever tried swiftfulthinking's router? https://github.com/SwiftfulThinking/SwiftfulRouting

You must be logged in to vote
2 replies
@winnisx7
Comment options

It doesn't look good at all.

@captadoh
Comment options

have you used it before?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
2 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.