Skip to main content

πŸ“‘ Multi-tab hosting (UIKit & SwiftUI)

Navigation

Navigation overview Β· Destination catalogue β€” every screen you can open with sdk.navigate.

When you embed both Nutrition and Training in your app β€” typically as two tabs β€” the SDK gives you a thin host VC per module and a coordinator that wires it to your tab UI. One shared FlutterViewController powers both tabs.

Why this matters: iOS's Flutter engine can only render into one FlutterViewController at a time. NaΓ―ve "two FlutterVCs per engine" embeddings cause freezes when switching tabs. The SDK pattern below avoids it entirely.

UIKit (UITabBarController)​

import UIKit
import AzeooSDK

final class MainTabBarController: UITabBarController {

/// Holding the coordinator alive keeps the SDK's weak ref valid for the
/// lifetime of this tab bar controller.
private var tabCoordinator: AzeooUITabBarCoordinator?

override func viewDidLoad() {
super.viewDidLoad()
setupTabs()
}

private func setupTabs() {
guard let sdk = AzeooSDK.shared else { return }

let status = UINavigationController(rootViewController: StatusViewController())
status.tabBarItem = UITabBarItem(title: "Status",
image: UIImage(systemName: "info.circle"),
selectedImage: nil)

// Tab hosts β€” thin VCs that contain the shared Flutter surface.
let nutrition = sdk.tabHost(for: .nutrition)
nutrition.tabBarItem = UITabBarItem(title: "Nutrition",
image: UIImage(systemName: "leaf"),
selectedImage: nil)

let training = sdk.tabHost(for: .training)
training.tabBarItem = UITabBarItem(title: "Training",
image: UIImage(systemName: "dumbbell"),
selectedImage: nil)

viewControllers = [status, nutrition, training]
selectedViewController = status

// Install the coordinator once. From now on, any sdk.navigate(...)
// from anywhere automatically flips this tab bar.
let coordinator = AzeooUITabBarCoordinator(self, mapping: [
.nutrition: 1,
.training: 2,
])
sdk.setModuleContainer(coordinator)
tabCoordinator = coordinator
}

deinit {
AzeooSDK.shared?.setModuleContainer(nil)
}
}

That's all the host code you need. From any view controller you can now do:

sdk.navigate(to: .nutrition(.plan(id: "abc-123")))
// Tab bar visually switches to Nutrition.
// Flutter switches to nutrition and pushes the plan detail.

SwiftUI (TabView)​

import SwiftUI
import AzeooSDK

struct ContentView: View {
@EnvironmentObject var sdkManager: SDKManager

@State private var selectedTab = 0
@State private var tabCoordinator: AzeooSwiftUITabCoordinator<Int>? = nil

var body: some View {
TabView(selection: $selectedTab) {
StatusView()
.tabItem { Label("Status", systemImage: "info.circle.fill") }
.tag(0)

sdkManager.sdk!.modules.nutrition.getView()
.tabItem { Label("Nutrition", systemImage: "leaf.fill") }
.tag(1)

sdkManager.sdk!.modules.training.getView()
.tabItem { Label("Training", systemImage: "dumbbell.fill") }
.tag(2)
}
.onAppear {
guard let sdk = sdkManager.sdk, tabCoordinator == nil else { return }
let coordinator = AzeooSwiftUITabCoordinator<Int>(
selection: $selectedTab,
mapping: [.nutrition: 1, .training: 2]
)
sdk.setModuleContainer(coordinator)
tabCoordinator = coordinator
}
.onDisappear {
sdkManager.sdk?.setModuleContainer(nil)
tabCoordinator = nil
}
}
}

How it works​

sdk.navigate(to: .training(.workouts))
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ AzeooSDK.navigate(to:) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
coordinator.azeoo_showModule(.training)
β”‚ β”‚
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Pigeon to(
β”‚ AzeooUITabBarCoord. β”‚ "training",
β”‚ tabBar.selectedIndex β”‚ "workout-plans",
β”‚ = 2 β”‚ params: nil)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β–Ό
Flutter switches its
internal tab + pushes
the workouts route

The host writes one line at startup; every navigation after that is end-to-end automatic.

Why one FlutterViewController​

The SDK holds a single shared FlutterViewController. Each tabHost(for:) returns a thin container view controller. When a tab becomes visible, the SDK:

  1. Reparents the shared FlutterVC into the active tab host (one UIKit view move, no engine churn)
  2. Sends Pigeon display(module) so Flutter renders the right tab content

AzeooTabHost overrides shouldAutomaticallyForwardAppearanceMethods to false, so tab switches don't trigger viewWillDisappear β†’ viewWillAppear on the FlutterVC. The engine attaches once and stays attached β€” no flash, no freeze.

Other UIKit containers​

If you use UINavigationController instead of a tab bar:

sdk.setModuleContainer(AzeooUINavigationCoordinator(
navController,
hostsByModule: [
.nutrition: nutritionVC,
.training: trainingVC,
],
))

For anything else (sidebar, page view, drawer, custom): implement AzeooModuleContainer directly β€” it's one method. See Module containers.

See also​