UIPager
UIPager — единственный элемент навигации по экранам: вкладки-сиблинги, нативно свайпающиеся в
стороны, каждая со своим стеком навигации. Передай массив экранов — они встанут рядом друг с
другом; push открывает экран поверх текущей вкладки (внутри пейджера), а нативный краевой свайп
назад (или pop) его разматывает. Он мапится на нативные контейнеры каждой платформы (iOS
UIPageViewController + UINavigationController на вкладку), так что свайпы, пуши и жест «назад»
— настоящие, а не переизобретённые.
const pager = UIPager(HomeScreen(), SearchScreen(), ProfileScreen())
// позже, откуда угодно внутри хостимого экрана — без проброса ссылки:
UIPager.push(PostScreen(id)) // въезжает поверх текущей вкладки
UIPager.pop()Пейджер с одной вкладкой — это просто стек навигации: отдельного элемента-стека нет. Хосты
понимают это буквально: с одной вкладкой iOS вовсе пропускает пейджинговый контейнер и встраивает
только UINavigationController, так что накладных расходов против выделенного компонента-стека —
ноль.
UITabs — стандартная оболочка с нижними вкладками
Для типовой формы — пейджер + нижняя панель — используй UITabs вместо ручной сборки. Это чистая
SDK-композиция (UIScreen из [ UIPager (flexGrow: 1), темированная панель ], ноль нового ABI)
со встроенными синхронизацией выбора, подсветкой активной вкладки, отступом под безопасную зону и
бейджами. Ключи — id вкладок, в порядке вкладок; кнопки называются tab-<id> (flow-тесты могут
тапать их по имени):
const tabs = UITabs({
home: { label: "Home", icon: assetIcon("lucide:house"), screen: homeScreen },
profile: { label: "Profile", icon: assetIcon("lucide:user"), screen: profileScreen },
})
Router.init(tabs) // UITabs И ЕСТЬ UIScreen / Presentable
tabs.select("profile") // переключить вкладку (мгновенно; `true` — слайд)
tabs.tab // id активной вкладки (геттер)
tabs.onSelect((id, i) => ...) // тап по панели, свайп или select()
tabs.badge("home", 3) // пилюля со счётчиком; `true` = точка; false/null/0 очищает
tabs.pager // пейджер под панелью (push/pop/depth)Панель стилизует себя через переменные темы, с фолбэками, чтобы нетемированное
приложение всё равно выглядело правильно: активное var(--primaryColor), неактивное
var(--mutedColor), поверхность var(--tabbarBg), волосяная линия var(--tabbarBorder), бейдж
var(--badgeColor) — один вызов theme({ primaryColor, tabbarBg }) перестилизует её на всё
приложение (те же переменные читает панель defineTabs дизайн-скаффолда). Активная кнопка также
несёт класс стиля active (каскадирует на её контент). Для более тонкого контроля перестилизуй
tabs.bar; для полностью кастомной панели спустись к UIPager + собственной строке.
Навигация внутри вкладки не меняется: UIPager.push(detail) с любого экрана сохраняет панель;
Router.push(...) открывается над всей оболочкой (панель закрыта).
Конструирование
UIPager(root) // одна вкладка = простой стек с корнем `root`
UIPager(a, b, c) // три вкладки рядом (выбрана a)
// Вкладки вариадичны, как дети контейнера: аргумент-массив расплющивается, falsy-элементы
// пропускаются — UIPager(home, isAdmin && adminTab) работает. Стили — через чейнящийся .style({...}).Дети — только UIScreenы — каждая страница несёт полный контракт экрана (onOpen/onClose,
onBackPressed) и раскладывается в бокс пейджера. Чтобы хостить простой элемент, оберни его:
UIPager(UIScreen(el)). Вкладки фиксируются при конструировании — динамические части — это
выбор вкладки и стек каждой из них.
Пейджер заполняет свой слот как любой элемент — дай ему flexGrow: 1 (или размер), чтобы он не
схлопнулся (flexShrink: 1 уже по умолчанию, как у UIScrollable, так что он оставляет место
таб-бару):
UIScreen(
UIPager(HomeTab(), ProfileTab()).style({ flexGrow: 1 }),
tabBar,
)Вкладки
pager.select(2) // мгновенное переключение (no-op вне диапазона)
pager.select(2, true) // анимированный слайд с учётом направления
pager.index // выбранная вкладка (геттер)
pager.onSelect(i => { ... }) // срабатывает на устоявшийся свайп или select()select мгновенен по умолчанию — платформенная конвенция для таб-баров, где вкладки —
параллельные режимы, а не последовательность слева направо. Передай animated: true для
скользящего переключения top-tab-интерфейсов (ощущение Material). Свайп всегда анимирован — это
физическое перетаскивание.
Вкладки хранят свои стеки: свайпни прочь с вкладки глубиной в три экрана, свайпни назад — и она ровно там, где ты её оставил. Корни вкладок живут постоянно — переключение вкладок никогда их не пересобирает.
Свайп между вкладками работает только на корне вкладки. Как только текущая вкладка углублена
(depth > 1), горизонтальный жест принадлежит краевому свайпу назад; пейджинг снова включается,
когда вкладка вернулась к корню. Это нативный арбитраж — настраивать нечего.
Стек (на вкладку)
pager.push(screen) // въезжает новой вершиной на ТЕКУЩУЮ вкладку; свайп назад её снимает
pager.pop() // снять вершину текущей вкладки (no-op на корне вкладки)
pager.popToRoot() // размотать текущую вкладку до корня
pager.replace(screen) // подменить вершину текущей вкладки (на глубине 1 подменяет корень вкладки)
pager.stack // UIScreen[] текущей вкладки, корень → вершина (геттер)
pager.depth // 1 = только корень (геттер)
pager.onChange(depth => { ... }) // срабатывает на push, pop, replace или свайп назадОпции перехода на каждый push нет — это фиксированный нативный push/pop, ровно то, что позволяет жесту «назад» обратимо тянуть его.
Доступ откуда угодно — UIPager.current
UIPager — глобал, так что ссылку в дочерние экраны не пробрасываешь. Амбиентные вызовы действуют
на пейджер, владеющий видимым сейчас экраном:
UIPager.current // пейджер, чей экран видим, или null
UIPager.push(screen) // → current.push(screen)
UIPager.pop() // → current.pop()
UIPager.popToRoot()const HomeTab = () => UIScreen(
row("Open the first post", () => UIPager.push(PostScreen(posts[0]))),
)С пейджерами, вложенными в страницы, UIPager.current разрешается во внутреннейший — push
изнутри страницы попадает в пейджер, на который пользователь реально смотрит. Если у видимого
экрана нет объемлющего пейджера, current — null, и амбиентные вызовы — no-op с предупреждением.
Таб-бары
UITabs даёт стандартную нижнюю панель. Для
кастомной панели поверх сырого пейджера — собери свою и веди её в обе стороны:
const pager = UIPager(HomeTab(), ProfileTab())
const tabBar = UIRow(
UIButton(UIText("Home")).onClick(() => pager.select(0)),
UIButton(UIText("Profile")).onClick(() => pager.select(1)),
)
pager.onSelect(i => highlight(tabBar, i)) // свайпы тоже двигают подсветку
Router.init(UIScreen(pager.style({ flexGrow: 1 }), tabBar))Кнопка «назад» (Android) и свайп назад
Интерактивный краевой свайп назад снимает верх текущей вкладки автоматически. Для аппаратной/программной кнопки «назад» сначала сними верх текущей вкладки, затем провались в собственную обработку:
screen.onBackPressed(() => {
if (UIPager.current && UIPager.current.depth > 1) {
UIPager.current.pop()
return
}
// ...твоё собственное поведение «назад» (выход, подтверждение и т.д.)
})Связь с Router
Router навигирует между цельноэкранными пунктами назначения и кросс-типовыми переходами
(экран ↔ сцена ↔ нативная вью ↔ видео) на корне приложения. UIPager — навигационный регион
внутри экрана. Они сосуществуют, и выбор из любой области — это просто глагол: UIPager.push
открывает внутри региона (панели остаются), Router.push открывает поверх всего
(накрывает весь экран, в котором живёт пейджер). См.
screen-router.md. UIPager сам не пункт назначения — ты не вызываешь
на нём open() и не передаёшь его в Router.push; встраивай его в экран.