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:
-
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.
-
Module boundary enforcement. The base
Coordinatorprotocol 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. -
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:
| Method | Purpose |
|---|---|
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. |
rootNavigationControllerForPush | Walks 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:#333Which 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:
| TabItem | Coordinator | CoordinatorFlow |
|---|---|---|
.home | HomeCoordinator | .home |
.explore | ExploreCoordinator | .explore |
.showcase | ShowcaseCoordinator | .showcase |
.exploreShowcase | ShowcaseCoordinator (with isUsedForExplore: true) | .exploreShowcase |
.events | EventsCoordinator | .events |
.library | LibraryCoordinator | .library |
.journeys | JourneysCoordinator (with useSwiftUIView: false) | .journeys |
.linearJourneys | JourneysCoordinator (with useSwiftUIView: true) | .journeys |
.profile | ProfileCoordinator | .profile |
.lightProfile | ProfileCoordinator (with lightVersion: true) | .profile |
.meals | MealsCoordinator | .meals |
.activity | ActivityCoordinator | .activity |
.biometrics | Created via biometricsModuleInterface | .biometrics |
.community | Created 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.
| Coordinator | Purpose |
|---|---|
ContentDetailsCoordinator.shared | All content playback. The central hub for playing audio, video, viewing content details, badges, facilitator profiles, and journey screens. |
SearchCoordinator.shared | Content search. Creates and manages search view controllers. |
SubscriptionCoordinator.shared | Paywall and subscription flows. Handles SuperPaywall presentation. |
QuizCoordinator.shared | Interactive quiz flows. |
Lifecycle Differences
Static coordinators have a fundamentally different lifecycle from tab coordinators:
- Setup, not init. They use a
static func setup(with:)class method called once duringAppCoordinator.start():
// In AppCoordinator.start()
SubscriptionCoordinator.setup(with: config)
SearchCoordinator.setup(with: config)
QuizCoordinator.setup(with: config)
ContentDetailsCoordinator.setup(with: config, customSubscriptionsManager: customSubscriptionsManager)-
No rootController. Accessing
rootControlleron a static coordinator triggers anassertionFailure. 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. -
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:
| Publisher | Type | Purpose |
|---|---|---|
didClosePlayerPublisher | PassthroughSubject<PlayedContent?, Never> | Fired when the player is dismissed. Tab coordinators subscribe to refresh their content. |
didLikeContentPublisher | PassthroughSubject<(String, Bool), Never> | Fired when content is liked/unliked. Updates favorites across tabs. |
mediaPlayerWasDisplayed | PassthroughSubject<Bool, Never> | Signals when the media player appears or disappears. |
reloadHomeContentPublisher | PassthroughSubject<Void, Never> | Triggers a Home tab content reload. |
Coordinator Lifecycle
Creation and Start
Coordinators follow a two-phase initialization:
init(...)stores configuration, dependencies, and references. No UI is created.start()creates the root view controller, setsrootController, 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:
- Splash video finished (or no splash video configured)
- Splash screen ended (if the customer app provides a
SplashScreenControllerthat needs time) - App configuration fetched (the
appConfigis not empty) - 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:
removeChildCoordinator(flow:)removes the coordinator from the parent's dictionary.- ARC deallocation. Once no strong references remain, the coordinator and its entire subtree are deallocated.
- 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.
Navigation Patterns
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.
Modal Presentation with Retry
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 Link Routing
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
-
Entry point.
DeeplinkManager.shared.redirectPublisheris a CombinePassthroughSubject<DeeplinkDestination, Never>. URL parsing happens elsewhere; the coordinator system only sees typed destinations. -
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. -
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
- Calling
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:
| Protocol | Direction | Key Methods |
|---|---|---|
MainCoordinatorDelegate | MainCoordinator -> AppCoordinator | didLogOut() |
AuthCoordinatorDelegate | AuthCoordinator -> AppCoordinator | didAuthenticateWithSuccess(isSubscribed:), dismissAction() |
NavigationDelegate | Tab coordinators -> MainCoordinator | openScreen(_: NavigationDestination) |
SubscriptionCoordinatorDelegate | SubscriptionCoordinator -> caller | shouldCloseSubscribtionScreen(), didSubscribeWithSuccess(_:) |
PlayerDelegate | Player -> coordinator | minimisePlayer(), 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:
| Publisher | Source | Subscribers |
|---|---|---|
didClosePlayerPublisher | ContentDetailsCoordinator | Home, Showcase (to refresh content) |
didLikeContentPublisher | ContentDetailsCoordinator | Home, Library (to update favorites) |
reloadHomeContentPublisher | ContentDetailsCoordinator | Home, Showcase |
didChangeMeasurePublisher | ProfileCoordinator | Home, Showcase (to update biometric displays) |
quizDismissPublisher | QuizCoordinator | Home (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 onEach 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:
Core/Content/Coordinator/Coordinator.swiftfor the base protocol and presentation helpersClient/Coordinators/Coordinator.swiftforParentCoordinator,ChildCoordinatorDelegate, and theCoordinatorFlowenumClient/Coordinators/AppCoordinator.swiftfor the root coordinator,start(), anddisplayFirstScreen()Client/Coordinators/Main/MainCoordinator.swiftfor the post-login coordinator and delegate protocolsClient/Coordinators/Main/MainCoordinators+TabSetup.swiftfor the tab creation factory andChildCoordinatorDelegateimplementationClient/Coordinators/Tabs/HomeCoordinator.swiftas a representative tab coordinator (~1100 lines, includesNavigationDestinationenum)Client/Coordinators/Static Coordinators/ContentDetails/ContentDetailsCoordinator.swiftfor the singleton pattern and publisher communication
Key Files
| Concept | File Path |
|---|---|
Base Coordinator protocol | Bricks/modules/Core/Core/Content/Coordinator/Coordinator.swift |
ParentCoordinator, CoordinatorFlow | Bricks/modules/Client/Client/Coordinators/Coordinator.swift |
| Root coordinator | Bricks/modules/Client/Client/Coordinators/AppCoordinator.swift |
| Auth flow coordinator | Bricks/modules/Client/Client/Coordinators/AuthCoordinator.swift |
| Post-login coordinator | Bricks/modules/Client/Client/Coordinators/Main/MainCoordinator.swift |
| Tab creation factory | Bricks/modules/Client/Client/Coordinators/Main/MainCoordinators+TabSetup.swift |
| Shared tab base class | Bricks/modules/Client/Client/Coordinators/Tabs/BaseCoordinator.swift |
| Deep link abstraction | Bricks/modules/Client/Client/Coordinators/Tabs/HomeNavigable.swift |
| Content playback singleton | Bricks/modules/Client/Client/Coordinators/Static Coordinators/ContentDetails/ContentDetailsCoordinator.swift |
| Search singleton | Bricks/modules/Client/Client/Coordinators/Static Coordinators/SearchCoordinator.swift |
| Subscription/paywall singleton | Bricks/modules/Client/Client/Coordinators/Static Coordinators/SubscriptionCoordinator.swift |
| Quiz singleton | Bricks/modules/Client/Client/Coordinators/Static Coordinators/QuizCoordinator.swift |
| Screen config factory | Bricks/modules/Client/Client/App/Screen Config/ScreenConfigFactory.swift |
| Deep link destinations | Bricks/modules/Core/Core/Content/Deeplinking/DeeplinkDestination.swift |
Tips
- To add a new tab coordinator: Add a case to
CoordinatorFlow, add aTabItemcase, add acreateXxxCoordinator()method inMainCoordinators+TabSetup.swift, and add the case to thecreateCoordinator(item:)switch. - To trace a deep link: Start at
DeeplinkDestination, then look atMainCoordinator.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 whatScreenConfigsub-factory it calls. - Static coordinators are called from anywhere. Do not look for them in the parent-child tree. Search for
ContentDetailsCoordinator.sharedorSearchCoordinator.sharedusage instead.
Design Token System
How Figma design tokens flow from JSON files through ConfigParser to runtime colors, sizes, and typography in every customer app.
Module Interfaces
How optional modules are defined as protocols in Core and injected at runtime, enabling customer apps to activate features without modifying shared code.