Youll
Architecture

Coordinator Pattern

How navigation works in Youll apps, from the base protocol to the full coordinator hierarchy, deep link routing, and inter-coordinator communication.

Why Coordinators

Every screen transition in a Youll app flows through a coordinator. Coordinators own the navigation logic that view controllers and view models should not know about: which screen comes next, how it is presented, and what happens when it is dismissed.

The coordinator pattern solves three problems in the Youll architecture:

  1. Decoupled navigation. View controllers never reference each other. A view model calls a closure or delegate method, and the coordinator decides what to show next. This lets the same screen appear in different contexts (a journey detail screen reached from the Home tab, Explore tab, or a deep link) without any changes to the screen itself.

  2. Module boundary enforcement. The base Coordinator protocol lives in Core, making it available to every module. The parent-child coordinator infrastructure lives in Client, keeping orchestration logic centralized. This split prevents feature modules from making navigation decisions that belong to the app layer.

  3. Customer-driven structure. The coordinator tree is built dynamically from what makeTabItems() returns. Different customer apps can have completely different tab layouts without touching coordinator code. Coordinators are created only for the tabs the customer selects.

Protocol Definitions

The coordinator type system is split across two modules by design.

Base Protocol (Core)

The Coordinator protocol in Core is intentionally minimal. It provides just enough surface for any module to reference a coordinator without pulling in the orchestration layer:

// Core/Content/Coordinator/Coordinator.swift

@MainActor
public protocol Coordinator: AnyObject {
  var isPresenter: Bool { get }
  var rootController: UIViewController? { get }
  func start()
}

The protocol extension provides default implementations for common presentation tasks:

MethodPurpose
tryPresent(_:animated:completion:)Presents a view controller modally. If another presentation is in progress, retries after a 1-second delay.
tryPresentIfNotAlreadyOnScreen(_:animated:completion:)Same as above, but first checks if the view controller is already in the window hierarchy. Prevents duplicate presentations of singletons like SuperPaywallViewController.
rootNavigationControllerForPushWalks the view controller hierarchy from the topmost VC to find a suitable UINavigationController for push navigation.
showAlert(title:text:button:)Convenience for presenting a UIAlertController.
showShareActivityController(with:completion:)Presents a UIActivityViewController for content sharing.

All coordinator work is annotated with @MainActor to ensure navigation happens on the main thread.

Parent-Child Infrastructure (Client)

The Client module adds the parent-child relationship system that the coordinator tree is built on:

// Client/Coordinators/Coordinator.swift

public enum CoordinatorFlow {
  case auth, main, home, explore, exploreShowcase, showcase,
       contentDetails, search, events, library, favorites,
       profile, subscription, journeys, contentReview, quiz,
       meals, activity, biometrics, community
}

@MainActor
public protocol ChildCoordinatorDelegate: AnyObject {
  func getChildCoordinator(_ flow: CoordinatorFlow, from fromFlow: CoordinatorFlow) -> Coordinator
}

@MainActor
public protocol ParentCoordinator: Coordinator {
  var childCoordinators: [CoordinatorFlow: Coordinator] { get set }
  var childCoordinatorDelegate: ChildCoordinatorDelegate? { get set }
}

The ParentCoordinator extension provides:

  • addChildCoordinator(_:flow:) stores a child by its flow key.
  • removeChildCoordinator(flow:) removes a child, allowing ARC to deallocate it.
  • hasChild(for:) checks if a child exists for a given flow.

The CoordinatorFlow enum is the central registry of all coordinator types in the platform. Adding a new product module that needs its own coordinator requires adding a case here.

Why the split matters. The Coordinator protocol lives in Core so that any module (Player, Biometrics, Community) can define types that reference coordinators. The ParentCoordinator, ChildCoordinatorDelegate, and CoordinatorFlow types live in Client because only the orchestration layer should manage the coordinator tree. Feature modules never add or remove children.

BaseCoordinator (Shared Tab Base Class)

Tab coordinators that need shared functionality inherit from BaseCoordinator rather than implementing ParentCoordinator directly:

// Client/Coordinators/Tabs/BaseCoordinator.swift

class BaseCoordinator: NSObject, ParentCoordinator {
  var rootController: UIViewController?
  var navigationController = AppNavigationController()
  var childCoordinators = [CoordinatorFlow: Coordinator]()
  weak var childCoordinatorDelegate: ChildCoordinatorDelegate?

  func start() {}
  func presentQuiz(quizConfig: BNQuizFlowConfig, skipIntroScreen: Bool) {}
  func displayRegisterScreen() {}
}

BaseCoordinator provides shared Tuya device screen presentation methods (showTuyaController, showTuyaPairController, showTuyaMaintenanceController) that multiple tab coordinators need. HomeCoordinator, ShowcaseCoordinator, and ProfileCoordinator all extend BaseCoordinator.

Coordinator Hierarchy

The coordinator tree forms at runtime based on user authentication state and the customer app's tab configuration.

graph TD
    subgraph "Parent-Child Tree"
        AC[AppCoordinator<br/><i>owns UIWindow</i>]
        AUTH[AuthCoordinator<br/><i>.auth</i>]
        MC[MainCoordinator<br/><i>.main, owns UITabBarController</i>]

        AC -->|"pre-login"| AUTH
        AC -->|"post-login"| MC

        MC --> HOME[HomeCoordinator<br/><i>.home</i>]
        MC --> SHOW[ShowcaseCoordinator<br/><i>.showcase / .exploreShowcase</i>]
        MC --> EXP[ExploreCoordinator<br/><i>.explore</i>]
        MC --> JOUR[JourneysCoordinator<br/><i>.journeys</i>]
        MC --> LIB[LibraryCoordinator<br/><i>.library</i>]
        MC --> EVT[EventsCoordinator<br/><i>.events</i>]
        MC --> ACT[ActivityCoordinator<br/><i>.activity</i>]
        MC --> MEAL[MealsCoordinator<br/><i>.meals</i>]
        MC --> PROF[ProfileCoordinator<br/><i>.profile</i>]
        MC --> BIO[Biometrics coordinator<br/><i>.biometrics</i>]
        MC --> COMM[Community coordinator<br/><i>.community</i>]
    end

    subgraph "Static Coordinators (Singletons)"
        CD[ContentDetailsCoordinator.shared]
        SC[SearchCoordinator.shared]
        SUB[SubscriptionCoordinator.shared]
        QC[QuizCoordinator.shared]
    end

    CD -.->|"publishers"| HOME
    CD -.->|"publishers"| MC
    SC -.->|"called from"| HOME
    SC -.->|"called from"| EXP
    SUB -.->|"called from"| PROF
    QC -.->|"called from"| HOME

    style AC fill:#f9f,stroke:#333
    style MC fill:#bbf,stroke:#333
    style CD fill:#ffa,stroke:#333
    style SC fill:#ffa,stroke:#333
    style SUB fill:#ffa,stroke:#333
    style QC fill:#ffa,stroke:#333

Which Tab Coordinators Are Created

Not all coordinators in the diagram exist in every app. MainCoordinator.setupTabController() iterates the customer's tab list and creates only the coordinators that appear:

func setupTabController() {
    let items = ScreenConfig.generic.makeTabItems()
    var viewControllers: [UIViewController] = []

    for item in items {
        let coordinator = createCoordinator(item: item)
        viewControllers.append(coordinator.rootController!)
    }

    tabController.setupTabBarItems(viewControllers, tabItems: items)
}

The createCoordinator(item:) method maps each TabItem to its coordinator:

TabItemCoordinatorCoordinatorFlow
.homeHomeCoordinator.home
.exploreExploreCoordinator.explore
.showcaseShowcaseCoordinator.showcase
.exploreShowcaseShowcaseCoordinator (with isUsedForExplore: true).exploreShowcase
.eventsEventsCoordinator.events
.libraryLibraryCoordinator.library
.journeysJourneysCoordinator (with useSwiftUIView: false).journeys
.linearJourneysJourneysCoordinator (with useSwiftUIView: true).journeys
.profileProfileCoordinator.profile
.lightProfileProfileCoordinator (with lightVersion: true).profile
.mealsMealsCoordinator.meals
.activityActivityCoordinator.activity
.biometricsCreated via biometricsModuleInterface.biometrics
.communityCreated via communityModuleInterface.community
.custom(name)Created via ScreenConfig.makeCustomCoordinator(name:)N/A

Note the tab aliasing: ShowcaseCoordinator serves both .showcase and .exploreShowcase (differentiated by an isUsedForExplore flag). JourneysCoordinator handles both .journeys and .linearJourneys (differentiated by a useSwiftUIView parameter). The .custom(name) tab item allows customer apps to inject entirely custom coordinators not defined in Bricks.

On-Demand Child Creation

When a tab coordinator needs a child coordinator that was not created during tab setup (for example, HomeCoordinator needs a JourneysCoordinator to display an embedded journey), it requests one through the ChildCoordinatorDelegate:

// Tab coordinator requests a child:
let journeys = childCoordinatorDelegate?.getChildCoordinator(.journeys, from: .home)

// MainCoordinator fulfills the request:
extension MainCoordinator: ChildCoordinatorDelegate {
    public func getChildCoordinator(_ flow: CoordinatorFlow, from fromFlow: CoordinatorFlow) -> Coordinator {
        if let coordinator = childCoordinators[flow] {
            return coordinator
        }
        return createCoordinator(flow: flow,
            parentNavigationController: childCoordinators[fromFlow]?.rootController as? UINavigationController)
    }
}

This pattern allows coordinators to be created lazily, only when actually needed.

Static Coordinators

Four coordinators use the singleton pattern instead of participating in the parent-child tree. These are cross-cutting services that any coordinator can invoke from anywhere in the app.

CoordinatorPurpose
ContentDetailsCoordinator.sharedAll content playback. The central hub for playing audio, video, viewing content details, badges, facilitator profiles, and journey screens.
SearchCoordinator.sharedContent search. Creates and manages search view controllers.
SubscriptionCoordinator.sharedPaywall and subscription flows. Handles SuperPaywall presentation.
QuizCoordinator.sharedInteractive quiz flows.

Lifecycle Differences

Static coordinators have a fundamentally different lifecycle from tab coordinators:

  1. Setup, not init. They use a static func setup(with:) class method called once during AppCoordinator.start():
// In AppCoordinator.start()
SubscriptionCoordinator.setup(with: config)
SearchCoordinator.setup(with: config)
QuizCoordinator.setup(with: config)
ContentDetailsCoordinator.setup(with: config, customSubscriptionsManager: customSubscriptionsManager)
  1. No rootController. Accessing rootController on a static coordinator triggers an assertionFailure. They do not own a navigation stack. Instead, they create view controllers on demand and present or push them from whatever context they are called.

  2. App-lifetime scope. Static coordinators are never deallocated. They persist from AppCoordinator.start() until the process exits. This is intentional: they hold no user-specific state that would need cleanup on logout.

ContentDetailsCoordinator

ContentDetailsCoordinator deserves special mention as the global content consumption hub. Any coordinator that needs to play content calls ContentDetailsCoordinator.shared.didSelectSession(...), which resolves the current top-most view controller and presents the player.

It communicates results back to the rest of the app through Combine publishers:

PublisherTypePurpose
didClosePlayerPublisherPassthroughSubject<PlayedContent?, Never>Fired when the player is dismissed. Tab coordinators subscribe to refresh their content.
didLikeContentPublisherPassthroughSubject<(String, Bool), Never>Fired when content is liked/unliked. Updates favorites across tabs.
mediaPlayerWasDisplayedPassthroughSubject<Bool, Never>Signals when the media player appears or disappears.
reloadHomeContentPublisherPassthroughSubject<Void, Never>Triggers a Home tab content reload.

Coordinator Lifecycle

Creation and Start

Coordinators follow a two-phase initialization:

  1. init(...) stores configuration, dependencies, and references. No UI is created.
  2. start() creates the root view controller, sets rootController, and the coordinator is ready for use.

For tab coordinators, both phases happen during MainCoordinator.setupTabController():

let homeCoordinator = HomeCoordinator(config: config, delegate: self, ...)
homeCoordinator.childCoordinatorDelegate = self
homeCoordinator.start()
addChildCoordinator(homeCoordinator, flow: .home)

The displayFirstScreen Convergence

AppCoordinator does not show app content immediately on start(). Multiple asynchronous conditions must all be met first:

  1. Splash video finished (or no splash video configured)
  2. Splash screen ended (if the customer app provides a SplashScreenController that needs time)
  3. App configuration fetched (the appConfig is not empty)
  4. Initial feature flags refreshed (Firebase Remote Config has returned at least once, OR the user is already logged in, OR the app is configured to skip waiting)

Each of these conditions independently calls displayFirstScreen() when it completes. The method uses guard conditions to return early if not all conditions are met yet. Only when everything has converged does it proceed to displayApp(), which decides between:

  • Showing the email verification screen (if the user has not verified their email)
  • Showing content via MainCoordinator (if logged in or anonymous)
  • Starting the guest/onboarding flow (if not authenticated)

This convergence pattern ensures the app never shows content before it has the configuration and flags it needs, while allowing each async operation to complete independently.

Cleanup

There is no formal finish() method. Coordinators are cleaned up through:

  1. removeChildCoordinator(flow:) removes the coordinator from the parent's dictionary.
  2. ARC deallocation. Once no strong references remain, the coordinator and its entire subtree are deallocated.
  3. Combine cleanup. Subscriptions are stored in Set<AnyCancellable> properties on each coordinator, automatically cancelled when the coordinator is deallocated.

On logout, AppCoordinator calls removeChildCoordinator(flow: .main), which drops the reference to MainCoordinator. ARC deallocates MainCoordinator and its entire tree of tab coordinators. A fresh MainCoordinator is created on the next login.

Four distinct navigation patterns are used across the coordinator system.

Push Navigation

Each tab coordinator owns an AppNavigationController (a custom UINavigationController subclass). Screen transitions within a tab use standard push navigation:

navigationController.pushViewController(detailVC, animated: true)

This is the most common navigation pattern. Users see a back button and can swipe back to return.

The base Coordinator protocol provides tryPresent(_:animated:completion:) for modal presentation. If a view controller is already being presented (common in async flows where multiple events might trigger presentation), it retries after a 1-second delay:

func tryPresent(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) {
    guard let root = UIApplication.shared.topMostViewController() else { return }
    if root.presentedViewController != nil {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
            self?.tryPresent(vc, animated: animated, completion: completion)
        }
    } else {
        DispatchQueue.main.async {
            root.present(vc, animated: animated, completion: completion)
        }
    }
}

For singleton view controllers (like the SuperPaywall), tryPresentIfNotAlreadyOnScreen adds a guard that checks vc.view.window != nil before attempting presentation.

Tab Switching

Cross-tab navigation goes through MainCoordinator, which implements the NavigationDelegate protocol:

@MainActor
protocol NavigationDelegate: AnyObject {
    func openScreen(_ destination: NavigationDestination)
}

Tab coordinators call delegate?.openScreen(.profile) (or another destination), and MainCoordinator responds by calling tabController.selectTab(...) to switch tabs, then optionally navigating within the target tab.

Top Banner

The showTopBannerVC(_:topHeight:) method presents a banner overlay at the top of the screen. This is used for confirmation messages, toast notifications, and similar transient UI. The method has special handling for the mini player (LNPopupController): if the player is open full-screen, the banner is added to the player's content view rather than the main tab bar controller.

Deep links flow through a centralized publisher and are handled by the appropriate coordinator based on the app's authentication state.

DeeplinkDestination

All deep link targets are defined in the DeeplinkDestination enum in Core:

public enum DeeplinkDestination {
    case login
    case subscription
    case homescreen
    case explore(tagId: String?)
    case category(id: String)
    case subcategory(id: String)
    case journey(id: String)
    case player(contentId: String)
    case profile
    case quiz(skipIntro: Bool, quizID: String)
    case events, event(id: String)
    case activities, activityNext, activityCompleted
    case meals, meal(id: String)
    case library, favorites, downloads, history, playlist(id: String)
    case badge(id: String)
    case externalUrl(url: URL)
    case joinCohorts(ids: [String], shouldRedirectHome: Bool)
    case tuyaMaintenance(type: String)
    // ... and more
}

Routing Flow

  1. Entry point. DeeplinkManager.shared.redirectPublisher is a Combine PassthroughSubject<DeeplinkDestination, Never>. URL parsing happens elsewhere; the coordinator system only sees typed destinations.

  2. AppCoordinator subscribes for pre-authentication deep links (e.g., .login). If the deep link requires authentication and the user is not logged in, AppCoordinator can trigger the auth flow first.

  3. MainCoordinator subscribes for all post-authentication deep links. Its redirect(to:) method handles each destination by:

    • Calling resetNavigation(for:) to dismiss any presented modals and pop to root on the relevant tab
    • Selecting the appropriate tab via tabController.selectTab(...)
    • Delegating to the target tab coordinator for the final navigation

resetNavigation Pattern

Before navigating to a deep link target, MainCoordinator resets the navigation state of the target tab. This ensures deep links work reliably regardless of what screen the user is currently viewing. The reset dismisses any presented modal and pops the tab's navigation controller to its root.

HomeNavigable Protocol

Deep links for content-related destinations (category, subcategory, tag, content, event) need to work regardless of whether the app uses a Home tab or Showcase tab. The HomeNavigable protocol abstracts this:

@MainActor
protocol HomeNavigable {
    var rootController: UIViewController? { get set }
    func navigateToCategory(id: String?)
    func navigateToSubcategory(id: String?)
    func navigateToTag(id: String?, sortInfo: (sortBy: SliceSortBy, sortDirection: SliceSortDirection)?)
    func navigateToContent(id: String?)
    func navigateToEvent(id: String?)
}

Both HomeCoordinator and ShowcaseCoordinator conform to HomeNavigable. MainCoordinator's deep link handler uses this protocol to navigate without caring which coordinator type is active.

Inter-Coordinator Communication

Coordinators communicate through three mechanisms, each suited to different relationships.

Delegate Protocols (Parent-Child)

The primary communication pattern for parent-child relationships:

ProtocolDirectionKey Methods
MainCoordinatorDelegateMainCoordinator -> AppCoordinatordidLogOut()
AuthCoordinatorDelegateAuthCoordinator -> AppCoordinatordidAuthenticateWithSuccess(isSubscribed:), dismissAction()
NavigationDelegateTab coordinators -> MainCoordinatoropenScreen(_: NavigationDestination)
SubscriptionCoordinatorDelegateSubscriptionCoordinator -> callershouldCloseSubscribtionScreen(), didSubscribeWithSuccess(_:)
PlayerDelegatePlayer -> coordinatorminimisePlayer(), playerViewRequiresClosing(...)

Cross-Sibling Delegates

Sibling coordinators can communicate through delegates that the parent wires up. After creating tab coordinators, MainCoordinator connects them:

// In setupTabController()
if let homeCoordinator = childCoordinators[.home] as? HomeCoordinator,
   let journeyCoordinator = childCoordinators[.journeys] as? JourneysCoordinator {
    journeyCoordinator.journeyCompletionDelegate = homeCoordinator
}

The JourneyCompletionDelegate protocol allows JourneysCoordinator to notify HomeCoordinator when a user views journey content, so the home feed can refresh.

Combine Publishers (Cross-Cutting Events)

Static coordinators and some tab coordinators expose publishers for events that multiple coordinators might care about:

PublisherSourceSubscribers
didClosePlayerPublisherContentDetailsCoordinatorHome, Showcase (to refresh content)
didLikeContentPublisherContentDetailsCoordinatorHome, Library (to update favorites)
reloadHomeContentPublisherContentDetailsCoordinatorHome, Showcase
didChangeMeasurePublisherProfileCoordinatorHome, Showcase (to update biometric displays)
quizDismissPublisherQuizCoordinatorHome (to handle quiz completion)

Direct Property Access

For simple, one-off interactions, MainCoordinator sometimes accesses child coordinators directly:

(childCoordinators[.home] as? HomeCoordinator)?.homeViewModel.reload()
(childCoordinators[.showcase] as? ShowcaseCoordinator)?.showcaseViewModel.reload()

This pattern is used sparingly, typically when a Combine publisher or delegate would be overkill for a single reload call.

AuthCoordinator Dual Usage

AuthCoordinator operates in two distinct modes depending on when it is created:

1. Full-screen onboarding (child of AppCoordinator). On first launch or after logout, AppCoordinator creates AuthCoordinator as its child. The auth flow takes over the entire window with onboarding screens (splash video, slider, social proof, quiz, registration/login).

2. Sheet presentation (child of MainCoordinator). When a user is browsing as anonymous and the app uses dismissible onboarding (Settings.onboardingType == .dismissable), MainCoordinator presents AuthCoordinator as a sheet over the existing content. The user can dismiss it and continue browsing, or complete registration.

AuthCoordinator uses an onReadyPublisher (CurrentValueSubject<Bool, Never>) to signal when its initial view controller is ready to display. The parent coordinator subscribes to this before setting the window's root view controller or presenting the sheet. This async-readiness pattern handles cases where the initial screen needs loading time (for example, a portrait video splash).

ScreenConfigFactory Relationship

Coordinators do not hard-code their screen configurations. Instead, they call into the ScreenConfigFactoryType protocol hierarchy to get configuration values defined by the customer app.

ScreenConfig is a global variable that returns the customer-provided ScreenConfigFactoryType implementation. It is set during app launch via Client.setupScreenConfigFactory(with:).

The factory has ~20+ sub-factory properties, each corresponding to a screen area:

ScreenConfig.generic.makeTabItems()         // Which tabs to create
ScreenConfig.home.makeHomeScreenConfig()     // Home screen visual config
ScreenConfig.journey.makeJourneysScreenConfig()  // Journeys screen config
ScreenConfig.player.makePlayerScreenConfig()     // Player screen config
// ... and so on

Each coordinator calls its corresponding sub-factory when creating view models and view controllers. This means the customer app controls all visual configuration (colors, typography, layout, feature toggles) without touching coordinator or screen logic.

For a detailed guide on implementing screen config factories, see the Screen Config Factory Guide (coming soon).

Customer App Integration

The customer app's SceneDelegate is the entry point for the entire coordinator tree. It creates AppCoordinator with all dependencies:

// In SceneDelegate.scene(_:willConnectTo:options:)
coordinator = AppCoordinator(
    window: sceneWindow,
    config: config,
    showSplashVideo: true,
    splashScreen: SplashViewController(),
    timerModuleInterface: TimerModule(...),
    tuyaModuleInterface: TuyaModule(...),
    biometricsModuleInterface: BiometricsModule(...),
    communityModuleInterface: nil,  // Not activated for this app
    customSubscriptionsManager: SuperwallManager()
)
coordinator.start()

The AppCoordinator.init accepts optional module interfaces for Timer, Tuya, Biometrics, and Community. Passing nil disables that module. These interfaces are threaded through to MainCoordinator and then to individual tab coordinators that need them.

The customSubscriptionsManager parameter allows customer apps to inject custom paywall logic (like Superwall). This is passed to ContentDetailsCoordinator and ProfileCoordinator for subscription-gated content.

For Agents

Reading Order

When exploring the coordinator system, read files in this order:

  1. Core/Content/Coordinator/Coordinator.swift for the base protocol and presentation helpers
  2. Client/Coordinators/Coordinator.swift for ParentCoordinator, ChildCoordinatorDelegate, and the CoordinatorFlow enum
  3. Client/Coordinators/AppCoordinator.swift for the root coordinator, start(), and displayFirstScreen()
  4. Client/Coordinators/Main/MainCoordinator.swift for the post-login coordinator and delegate protocols
  5. Client/Coordinators/Main/MainCoordinators+TabSetup.swift for the tab creation factory and ChildCoordinatorDelegate implementation
  6. Client/Coordinators/Tabs/HomeCoordinator.swift as a representative tab coordinator (~1100 lines, includes NavigationDestination enum)
  7. Client/Coordinators/Static Coordinators/ContentDetails/ContentDetailsCoordinator.swift for the singleton pattern and publisher communication

Key Files

ConceptFile Path
Base Coordinator protocolBricks/modules/Core/Core/Content/Coordinator/Coordinator.swift
ParentCoordinator, CoordinatorFlowBricks/modules/Client/Client/Coordinators/Coordinator.swift
Root coordinatorBricks/modules/Client/Client/Coordinators/AppCoordinator.swift
Auth flow coordinatorBricks/modules/Client/Client/Coordinators/AuthCoordinator.swift
Post-login coordinatorBricks/modules/Client/Client/Coordinators/Main/MainCoordinator.swift
Tab creation factoryBricks/modules/Client/Client/Coordinators/Main/MainCoordinators+TabSetup.swift
Shared tab base classBricks/modules/Client/Client/Coordinators/Tabs/BaseCoordinator.swift
Deep link abstractionBricks/modules/Client/Client/Coordinators/Tabs/HomeNavigable.swift
Content playback singletonBricks/modules/Client/Client/Coordinators/Static Coordinators/ContentDetails/ContentDetailsCoordinator.swift
Search singletonBricks/modules/Client/Client/Coordinators/Static Coordinators/SearchCoordinator.swift
Subscription/paywall singletonBricks/modules/Client/Client/Coordinators/Static Coordinators/SubscriptionCoordinator.swift
Quiz singletonBricks/modules/Client/Client/Coordinators/Static Coordinators/QuizCoordinator.swift
Screen config factoryBricks/modules/Client/Client/App/Screen Config/ScreenConfigFactory.swift
Deep link destinationsBricks/modules/Core/Core/Content/Deeplinking/DeeplinkDestination.swift

Tips

  • To add a new tab coordinator: Add a case to CoordinatorFlow, add a TabItem case, add a createXxxCoordinator() method in MainCoordinators+TabSetup.swift, and add the case to the createCoordinator(item:) switch.
  • To trace a deep link: Start at DeeplinkDestination, then look at MainCoordinator.redirect(to:) to see how it routes to the correct tab coordinator.
  • To understand what a tab shows: Read the coordinator's start() method to see which view controller it creates, and check what ScreenConfig sub-factory it calls.
  • Static coordinators are called from anywhere. Do not look for them in the parent-child tree. Search for ContentDetailsCoordinator.shared or SearchCoordinator.shared usage instead.

On this page