From 0e5691a3e4f030351c781051bf5b9cc82d514d63 Mon Sep 17 00:00:00 2001 From: jensvansteen Date: Mon, 6 Apr 2026 15:19:04 +0700 Subject: [PATCH 1/2] feat: rearchitecture library --- ios/HybridScrollEdgeBar.swift | 289 +++++++++++++++++++--------------- 1 file changed, 164 insertions(+), 125 deletions(-) diff --git a/ios/HybridScrollEdgeBar.swift b/ios/HybridScrollEdgeBar.swift index 6c42395..5f3fc97 100644 --- a/ios/HybridScrollEdgeBar.swift +++ b/ios/HybridScrollEdgeBar.swift @@ -98,8 +98,7 @@ class ScrollEdgeBarContainerView: UIView { override func willMove(toWindow newWindow: UIWindow?) { super.willMove(toWindow: newWindow) - // Before the view is removed from the window, restore all reparented - // Fabric views so Fabric's unmount code finds them in the expected parent. + // Restore reparented views before Fabric unmounts them. if newWindow == nil && window != nil { cleanupController() } @@ -116,8 +115,7 @@ class ScrollEdgeBarContainerView: UIView { override func layoutSubviews() { super.layoutSubviews() - // Always re-detect if any view is missing — React Native may mount - // component children AFTER the component wrapper is added to the parent. + // React Native may mount children after the container enters the hierarchy. if detectedScrollView == nil || detectedTopBarView == nil || detectedBottomBarView == nil { detectChildViews() } @@ -128,14 +126,13 @@ class ScrollEdgeBarContainerView: UIView { } private func detectChildViews() { - // In Fabric/Nitro, React children are siblings of our view (subviews of the - // component wrapper), not subviews of our container. Search the parent's children. + // In Fabric/Nitro, React children are siblings of this view inside the + // generated component wrapper, not direct subviews of the container. let searchViews = superview?.subviews ?? subviews for subview in searchViews where subview !== self { if detectedTopBarView == nil, let topBarMarker = findView(ofType: ScrollEdgeBarTopBarView.self, in: subview) { - // Use the component wrapper (superview) which holds both the marker - // and the actual React content (Fabric adds children as siblings). + // Use the wrapper that holds both the marker and the React content. detectedTopBarView = topBarMarker.superview ?? topBarMarker } else if detectedBottomBarView == nil, let bottomBarMarker = findView(ofType: ScrollEdgeBarBottomBarView.self, in: subview) { @@ -146,7 +143,7 @@ class ScrollEdgeBarContainerView: UIView { } } - // If already set up, push newly-detected bar views to the controller + // Bars can appear after initial setup. if #available(iOS 16.0, *), isSetup, let controller = edgeBarController as? ScrollEdgeBarController { if let topBarView = detectedTopBarView { @@ -160,7 +157,7 @@ class ScrollEdgeBarContainerView: UIView { trySetup() } - // Explicit wiring from the component view (preferred over discovery) + // Explicit wiring from the generated component view. func setTopBarView(_ view: UIView?) { detectedTopBarView = view as? ScrollEdgeBarTopBarView trySetup() @@ -228,10 +225,8 @@ class ScrollEdgeBarContainerView: UIView { guard let startVC = nearestVC else { return nil } - // Walk up the VC parent chain and prefer the direct child of UITabBarController - // first, then fall back to the direct child of UINavigationController. - // This keeps the hosting controller close to the system container that owns - // the relevant safe area and scroll-edge behavior. + // Prefer the direct child of the nearest system container so safe-area + // propagation and scroll-edge behavior come from the correct owner. var candidate = startVC while let parentVC = candidate.parent { if parentVC is UITabBarController { @@ -320,8 +315,7 @@ class ScrollEdgeBarContainerView: UIView { } } - /// Called from the generated ObjC++ unmount code to tear down the SwiftUI - /// hosting controller and restore reparented views before Fabric's assertions. + /// Called from generated ObjC++ unmount code before Fabric tears down children. @objc func prepareForFabricUnmount() { cleanupController() } @@ -351,7 +345,7 @@ class ScrollEdgeBarContainerView: UIView { } } -// MARK: - ScrollEdgeBarController (iOS 16+, scroll-edge effect requires iOS 26) +// MARK: - ScrollEdgeBarController @available(iOS 16.0, *) final class ScrollEdgeBarController: UIViewController { @@ -366,10 +360,11 @@ final class ScrollEdgeBarController: UIViewController { private var bottomBarView: UIView? private var hostingController: UIHostingController? private var didSetup = false - private var registeredScrollObservation: NSKeyValueObservation? - private var registeredScrollDebugCount = 0 + private var lastTopInset: CGFloat = 0 + private var lastBottomInset: CGFloat = 0 + private var displayLink: CADisplayLink? - // Track original Fabric parents + subview indices for teardown restore + // Original parents and indices for teardown restore. private weak var scrollViewOriginalParent: UIView? private var scrollViewOriginalIndex: Int = 0 private weak var topBarOriginalParent: UIView? @@ -411,6 +406,19 @@ final class ScrollEdgeBarController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear + scrollView.alpha = 0 + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + stopDisplayLink() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if didSetup { + startDisplayLink() + } } override func viewDidLayoutSubviews() { @@ -437,9 +445,7 @@ final class ScrollEdgeBarController: UIViewController { let wrapperView = ScrollEdgeBarWrapperView( scrollView: scrollView, topBarContent: topContent, - bottomBarContent: bottomContent, - topBarOffset: topBarOffset, - bottomBarOffset: bottomBarOffset + bottomBarContent: bottomContent ) let hosting = UIHostingController(rootView: wrapperView) @@ -460,67 +466,41 @@ final class ScrollEdgeBarController: UIViewController { hosting.didMove(toParent: self) hostingController = hosting hosting.view.layoutIfNeeded() - - // Register the SwiftUI scroll view with the navigation bar so the large - // title collapses naturally as the user scrolls. Done async so SwiftUI - // has finished its first layout pass before we search the hierarchy. - // This API is available on UIViewController and used here so the - // navigation bar tracks the SwiftUI-hosted scroll view instead of the - // original reparented React Native scroll view. - DispatchQueue.main.async { [weak self] in - guard let self else { return } - let swiftuiScrollView = self.findSwiftUIScrollView() - self.logScrollGeometry(label: "rn", scrollView: self.scrollView) - if let swiftuiScrollView { - self.logScrollGeometry(label: "swiftui", scrollView: swiftuiScrollView) + let estimatedInsets = UIEdgeInsets( + top: estimatedTopBarHeight + topBarOffset, + left: 0, + bottom: estimatedBottomBarHeight + bottomBarOffset, + right: 0 + ) + scrollView.contentInset = estimatedInsets + scrollView.verticalScrollIndicatorInsets = estimatedInsets + scrollView.contentOffset = CGPoint(x: 0, y: -estimatedInsets.top) + scrollView.alpha = 1 + + DispatchQueue.main.async { + if #available(iOS 26.0, *) { + // In the RN containment hierarchy, navigation tracking does not + // get inferred automatically. Register the real wrapped scroll view. + self.parent?.setContentScrollView(self.scrollView, for: .top) } - self.startRegisteredScrollDebug(label: "registered", on: self.scrollView) - self.parent?.setContentScrollView(self.scrollView, for: .top) + self.applyInsets() + self.startDisplayLink() } } - private func logScrollGeometry(label: String, scrollView: UIScrollView) { - print("[ScrollEdgeBar][NavTrack][\(label)] class=\(NSStringFromClass(type(of: scrollView))) contentInset.top=\(scrollView.contentInset.top) adjustedTop=\(scrollView.adjustedContentInset.top) offsetY=\(scrollView.contentOffset.y)") - } - - private func startRegisteredScrollDebug(label: String, on scrollView: UIScrollView) { - registeredScrollObservation = nil - registeredScrollDebugCount = 0 - - print("[ScrollEdgeBar][NavTrack][\(label)] register class=\(NSStringFromClass(type(of: scrollView))) contentInset.top=\(scrollView.contentInset.top) adjustedTop=\(scrollView.adjustedContentInset.top) offsetY=\(scrollView.contentOffset.y)") - - registeredScrollObservation = scrollView.observe(\.contentOffset, options: [.new]) { [weak self] sv, _ in - guard let self else { return } - guard self.registeredScrollDebugCount < 20 else { return } - self.registeredScrollDebugCount += 1 - print("[ScrollEdgeBar][NavTrack][\(label)] offsetY=\(sv.contentOffset.y) contentInset.top=\(sv.contentInset.top) adjustedTop=\(sv.adjustedContentInset.top)") - } - } - - private func findSwiftUIScrollView() -> UIScrollView? { - guard let hostingView = hostingController?.view else { return nil } - return firstScrollView(in: hostingView) - } - - private func firstScrollView(in view: UIView) -> UIScrollView? { - if let sv = view as? UIScrollView { return sv } - for sub in view.subviews { - if let found = firstScrollView(in: sub) { return found } - } - return nil - } - private func updateHostingControllerIfNeeded() { guard didSetup else { return } let wrapperView = ScrollEdgeBarWrapperView( scrollView: scrollView, topBarContent: makeBarContent(topBarView, estimatedHeight: estimatedTopBarHeight), - bottomBarContent: makeBarContent(bottomBarView, estimatedHeight: estimatedBottomBarHeight), - topBarOffset: topBarOffset, - bottomBarOffset: bottomBarOffset + bottomBarContent: makeBarContent(bottomBarView, estimatedHeight: estimatedBottomBarHeight) ) hostingController?.rootView = wrapperView + + DispatchQueue.main.async { + self.applyInsets() + } } func setOffsets(top: CGFloat, bottom: CGFloat) { @@ -529,30 +509,107 @@ final class ScrollEdgeBarController: UIViewController { updateHostingControllerIfNeeded() } - /// Restore all reparented views to their original Fabric parents so - /// Fabric's unmount logic finds them in the expected location. + private func applyInsets() { + let (topInset, bottomInset) = findEdgeBarInsets() + let adjustedTop = topInset + topBarOffset + let adjustedBottom = bottomInset + bottomBarOffset + + lastTopInset = adjustedTop + lastBottomInset = adjustedBottom + + scrollView.contentInset = UIEdgeInsets(top: adjustedTop, left: 0, bottom: adjustedBottom, right: 0) + scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: adjustedTop, left: 0, bottom: adjustedBottom, right: 0) + scrollView.contentOffset = CGPoint(x: 0, y: -adjustedTop) + } + + private func startDisplayLink() { + guard displayLink == nil else { return } + let link = CADisplayLink(target: self, selector: #selector(displayLinkFired)) + link.add(to: .main, forMode: .common) + displayLink = link + } + + private func stopDisplayLink() { + displayLink?.invalidate() + displayLink = nil + } + + @objc private func displayLinkFired() { + let (topInset, bottomInset) = findEdgeBarInsets() + let adjustedTop = topInset + topBarOffset + let adjustedBottom = bottomInset + bottomBarOffset + + guard adjustedTop != lastTopInset || adjustedBottom != lastBottomInset else { return } + + lastTopInset = adjustedTop + lastBottomInset = adjustedBottom + + let newInsets = UIEdgeInsets(top: adjustedTop, left: 0, bottom: adjustedBottom, right: 0) + UIView.animate(withDuration: 0.3, delay: 0, options: [.beginFromCurrentState, .curveEaseInOut]) { + self.scrollView.contentInset = newInsets + self.scrollView.verticalScrollIndicatorInsets = newInsets + } + } + + private func findEdgeBarInsets() -> (top: CGFloat, bottom: CGFloat) { + guard let rootView = navigationController?.view ?? view.window?.rootViewController?.view else { + return (estimatedTopBarHeight, estimatedBottomBarHeight) + } + + var topInset: CGFloat = estimatedTopBarHeight + var bottomInset: CGFloat = estimatedBottomBarHeight + let screenHeight = view.bounds.height + + findEdgeBarViews(in: rootView) { barView in + let frameInWindow = barView.convert(barView.bounds, to: nil) + + if frameInWindow.origin.y < screenHeight / 2 { + topInset = frameInWindow.maxY + } + + if frameInWindow.maxY > screenHeight / 2 { + bottomInset = screenHeight - frameInWindow.minY + } + } + + return (topInset, bottomInset) + } + + private func findEdgeBarViews(in view: UIView, handler: (UIView) -> Void) { + for interaction in view.interactions { + let className = String(describing: type(of: interaction)) + if className.contains("ScrollPocketBarInteraction") { + handler(view) + return + } + } + + for subview in view.subviews { + findEdgeBarViews(in: subview, handler: handler) + } + } + + /// Restore all reparented views to their original parents so Fabric unmount + /// finds the expected hierarchy. func cleanup() { - // Tear down hosting controller first hostingController?.willMove(toParent: nil) hostingController?.view.removeFromSuperview() hostingController?.removeFromParent() hostingController = nil didSetup = false - registeredScrollObservation = nil - registeredScrollDebugCount = 0 + stopDisplayLink() - // Restore scroll view at its original index scrollView.removeFromSuperview() scrollView.isScrollEnabled = true scrollView.contentInsetAdjustmentBehavior = .automatic scrollView.showsVerticalScrollIndicator = true scrollView.showsHorizontalScrollIndicator = true + scrollView.alpha = 1 if let parent = scrollViewOriginalParent { let idx = min(scrollViewOriginalIndex, parent.subviews.count) parent.insertSubview(scrollView, at: idx) } - // Restore bar views at their original indices if let topBar = topBarView, let parent = topBarOriginalParent { topBar.removeFromSuperview() let idx = min(topBarOriginalIndex, parent.subviews.count) @@ -564,12 +621,10 @@ final class ScrollEdgeBarController: UIViewController { parent.insertSubview(bottomBar, at: idx) } - // Deregister the scroll view from the navigation bar if #available(iOS 26.0, *) { parent?.setContentScrollView(nil, for: .top) } - // Remove self from parent VC willMove(toParent: nil) view.removeFromSuperview() removeFromParent() @@ -604,7 +659,7 @@ final class PassthroughView: UIView { } } -// MARK: - SwiftUI Views (iOS 16+, scroll-edge effect requires iOS 26) +// MARK: - SwiftUI Views @available(iOS 16.0, *) struct BarViewWrapper: UIViewRepresentable { @@ -637,7 +692,7 @@ struct BarViewWrapper: UIViewRepresentable { func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIView, context: Context) -> CGSize? { let width = proposal.width ?? UIView.layoutFittingExpandedSize.width - // Prefer the bounds height set by React Native's layout engine + // Prefer the height React Native laid out for the wrapped bar view. let height = barView.bounds.height if height > 0 { return CGSize(width: width, height: height) @@ -665,17 +720,12 @@ struct ScrollEdgeBarWrapperView: View { let scrollView: UIScrollView let topBarContent: AnyView? let bottomBarContent: AnyView? - let topBarOffset: CGFloat - let bottomBarOffset: CGFloat - @State private var contentHeight: CGFloat = 5000 var body: some View { - applyBars(to: - ScrollView { - ScrollContentBridge(scrollView: scrollView, contentHeight: $contentHeight) - .frame(height: contentHeight) - } - ) + let base = DirectScrollViewWrapper(scrollView: scrollView) + .ignoresSafeArea(.all) + + applyBars(to: base) } @ViewBuilder @@ -725,50 +775,39 @@ struct ScrollEdgeBarWrapperView: View { } } -/// Embeds the RN scroll view (with scrolling disabled) inside a native SwiftUI -/// ScrollView. SwiftUI handles scrolling so `safeAreaBar` works correctly. -/// The scroll view is kept intact (no content extraction) so it survives -/// rootView updates when bars are detected late. @available(iOS 16.0, *) -struct ScrollContentBridge: UIViewRepresentable { +struct DirectScrollViewWrapper: UIViewRepresentable { let scrollView: UIScrollView - @Binding var contentHeight: CGFloat - func makeCoordinator() -> Coordinator { Coordinator() } + func makeUIView(context: Context) -> UIView { + let container = UIView() - func makeUIView(context: Context) -> UIScrollView { - scrollView.isScrollEnabled = false + scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.contentInsetAdjustmentBehavior = .never - scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false + scrollView.alwaysBounceHorizontal = false + scrollView.isDirectionalLockEnabled = true - // Observe content size changes to update SwiftUI layout - context.coordinator.observation = scrollView.observe(\.contentSize, options: [.new]) { sv, _ in - let newHeight = sv.contentSize.height - if newHeight > 0 && newHeight != contentHeight { - DispatchQueue.main.async { - contentHeight = newHeight - } - } - } + container.addSubview(scrollView) - if scrollView.contentSize.height > 0 { - DispatchQueue.main.async { - contentHeight = scrollView.contentSize.height + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: container.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor) + ]) + + DispatchQueue.main.async { + if let contentView = scrollView.subviews.first(where: { !($0 is UIImageView) }) { + contentView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) + ]) } } - return scrollView - } - - func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIScrollView, context: Context) -> CGSize? { - let width = proposal.width ?? uiView.bounds.width - return CGSize(width: width, height: max(contentHeight, 1)) + return container } - func updateUIView(_ uiView: UIScrollView, context: Context) {} - - class Coordinator: NSObject { - var observation: NSKeyValueObservation? - } + func updateUIView(_ uiView: UIView, context: Context) {} } From a94ec747edd41e4ae7d8473476523f2cd1910fe4 Mon Sep 17 00:00:00 2001 From: jensvansteen Date: Tue, 7 Apr 2026 11:42:17 +0700 Subject: [PATCH 2/2] feat: small improvements --- example/src/App.tsx | 3 +- example/src/components/ExampleMenuScreen.tsx | 95 ++++++++++++-------- example/src/data.ts | 73 ++++++++++++--- example/src/types.ts | 8 ++ 4 files changed, 129 insertions(+), 50 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index b8b71d7..f1eff4a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,7 +7,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { enableScreens } from 'react-native-screens'; import { ExampleMenuScreen } from './components/ExampleMenuScreen'; import { ExampleScreen } from './screens/ExampleScreen'; -import { titleByKey } from './data'; +import { navigationByKey, titleByKey } from './data'; import type { RootStackParamList } from './types'; enableScreens(); @@ -55,6 +55,7 @@ export default function App() { headerTransparent: true, headerLargeTitleEnabled: true, headerBackButtonDisplayMode: 'minimal', + ...navigationByKey[route.params.key], headerTintColor: DynamicColorIOS({ light: 'black', dark: 'white', diff --git a/example/src/components/ExampleMenuScreen.tsx b/example/src/components/ExampleMenuScreen.tsx index 3cbc4a7..e7e440c 100644 --- a/example/src/components/ExampleMenuScreen.tsx +++ b/example/src/components/ExampleMenuScreen.tsx @@ -1,7 +1,7 @@ import { - DynamicColorIOS, + FlatList, + PlatformColor, Pressable, - ScrollView, StyleSheet, Text, View, @@ -11,76 +11,93 @@ import { exampleList } from '../data'; export function ExampleMenuScreen({ navigation }: { navigation: any }) { return ( - - {exampleList.map((item) => ( + item.key} + style={styles.menuList} + contentContainerStyle={styles.menuContent} + renderItem={({ item }) => ( navigation.push('Example', { key: item.key })} - style={styles.menuItem} + style={({ pressed }) => [ + styles.menuItem, + pressed && styles.menuItemPressed, + ]} > - - - + {item.title} {item.subtitle} - + + + - ))} - + )} + /> ); } const styles = StyleSheet.create({ - menuScrollView: { + menuList: { flex: 1, - backgroundColor: DynamicColorIOS({ light: '#f2f2f7', dark: '#000000' }), + backgroundColor: PlatformColor('systemBackground'), + }, + menuContent: { + paddingBottom: 24, }, menuItem: { - minHeight: 72, + minHeight: 78, paddingHorizontal: 16, - paddingVertical: 12, + paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: DynamicColorIOS({ light: '#d1d1d6', dark: '#2c2c2e' }), - backgroundColor: DynamicColorIOS({ light: '#ffffff', dark: '#1c1c1e' }), + borderBottomColor: PlatformColor('separator'), + backgroundColor: PlatformColor('systemBackground'), flexDirection: 'row', alignItems: 'center', }, + menuItemPressed: { + opacity: 0.72, + }, menuIconWrap: { - width: 30, - height: 30, - marginRight: 14, + width: 40, + height: 40, + marginRight: 16, alignItems: 'center', justifyContent: 'center', borderWidth: 1, - borderRadius: 8, - backgroundColor: DynamicColorIOS({ light: '#f8f8f8', dark: '#2a2a2d' }), + borderRadius: 12, }, menuTextColumn: { flex: 1, + paddingRight: 12, + paddingLeft: 16, }, menuTitle: { fontSize: 17, + lineHeight: 22, fontWeight: '600', - color: DynamicColorIOS({ light: '#111111', dark: '#f5f5f5' }), + color: PlatformColor('label'), }, menuSubtitle: { - marginTop: 2, - fontSize: 12, - color: DynamicColorIOS({ light: '#6d6d72', dark: '#8e8e93' }), + marginTop: 1, + fontSize: 13, + lineHeight: 18, + fontWeight: '400', + color: PlatformColor('secondaryLabel'), }, - menuChevron: { - marginLeft: 12, - fontSize: 24, - lineHeight: 24, - color: DynamicColorIOS({ light: '#c7c7cc', dark: '#636366' }), + menuChevronWrap: { + width: 24, + alignItems: 'flex-end', }, }); diff --git a/example/src/data.ts b/example/src/data.ts index effcfc6..24b30c2 100644 --- a/example/src/data.ts +++ b/example/src/data.ts @@ -1,67 +1,115 @@ -import type { ExampleKey } from './types'; +import type { ExampleKey, ExampleNavigationConfig } from './types'; export const exampleList: Array<{ key: ExampleKey; title: string; subtitle: string; symbol: string; - tint: string; + navigation?: ExampleNavigationConfig; }> = [ { key: 'appStore', title: 'App Store Listing', subtitle: 'Segmented control top bar', symbol: 'bag', - tint: '#2563eb', + navigation: { + title: 'App Store Listing', + headerTransparent: true, + headerLargeTitleEnabled: false, + headerShadowVisible: false, + headerBackButtonDisplayMode: 'minimal', + }, }, { key: 'pullRequests', title: 'Pull Requests', subtitle: 'Filter chips with large title', symbol: 'arrow.triangle.pull', - tint: '#16a34a', + navigation: { + title: 'Pull Requests', + headerTransparent: true, + headerLargeTitleEnabled: true, + headerShadowVisible: false, + headerBackButtonDisplayMode: 'minimal', + }, }, { key: 'prDetail', title: 'PR Detail', subtitle: 'Review banner + action buttons', symbol: 'text.page.badge.magnifyingglass', - tint: '#7c3aed', + navigation: { + title: 'PR Detail', + headerTransparent: true, + headerLargeTitleEnabled: true, + headerShadowVisible: false, + headerBackButtonDisplayMode: 'minimal', + }, }, { key: 'transitionShowcase', title: 'Transition Showcase', subtitle: 'Top and bottom bars over color blocks', symbol: 'paintpalette', - tint: '#db2777', + navigation: { + title: 'Transition Showcase', + headerTransparent: true, + headerLargeTitleEnabled: true, + headerShadowVisible: false, + headerBackButtonDisplayMode: 'minimal', + }, }, { key: 'toolbar', title: 'Toolbar', subtitle: 'Bottom edge bar emphasis', symbol: 'hammer', - tint: '#d97706', + navigation: { + title: 'Toolbar', + headerTransparent: true, + headerLargeTitleEnabled: true, + headerShadowVisible: false, + headerBackButtonDisplayMode: 'minimal', + }, }, { key: 'searchBar', title: 'Search Bar', subtitle: 'Search-like screen with segmented top bar', symbol: 'magnifyingglass', - tint: '#0891b2', + navigation: { + title: 'Search Bar', + headerTransparent: true, + headerLargeTitleEnabled: true, + headerShadowVisible: false, + headerBackButtonDisplayMode: 'minimal', + }, }, { key: 'tabAccessory', title: 'Tab Accessory', subtitle: 'Large bottom accessory-style bar', symbol: 'music.note.list', - tint: '#ea580c', + navigation: { + title: 'Tab Accessory', + headerTransparent: true, + headerLargeTitleEnabled: true, + headerShadowVisible: false, + headerBackButtonDisplayMode: 'minimal', + }, }, { key: 'calendar', title: 'Calendar', subtitle: 'Week selector with stronger top bar', symbol: 'calendar', - tint: '#dc2626', + navigation: { + title: 'Calendar', + headerTransparent: true, + headerLargeTitleEnabled: true, + headerShadowVisible: false, + headerBackButtonDisplayMode: 'minimal', + }, }, ]; @@ -69,6 +117,11 @@ export const titleByKey: Record = Object.fromEntries( exampleList.map((item) => [item.key, item.title]) ) as Record; +export const navigationByKey: Record = + Object.fromEntries( + exampleList.map((item) => [item.key, item.navigation ?? {}]) + ) as Record; + export const appColors = [ '#4fd1c5', '#68d391', diff --git a/example/src/types.ts b/example/src/types.ts index 5c40b54..a994036 100644 --- a/example/src/types.ts +++ b/example/src/types.ts @@ -8,6 +8,14 @@ export type ExampleKey = | 'tabAccessory' | 'calendar'; +export type ExampleNavigationConfig = { + title?: string; + headerTransparent?: boolean; + headerLargeTitleEnabled?: boolean; + headerShadowVisible?: boolean; + headerBackButtonDisplayMode?: 'default' | 'generic' | 'minimal'; +}; + export type RootStackParamList = { Home: undefined; Example: { key: ExampleKey };