first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
import ExpoModulesCore
import RNScreens
import UIKit
class RouterToolbarHostView: RouterViewWithLogger, LinkPreviewMenuUpdatable {
// Cached reference to the view controller to avoid responder chain traversal
private weak var cachedController: RNSScreen?
// Mutable map of toolbar items
var toolbarItemsArray: [String] = []
var toolbarItemsMap: [String: RouterToolbarItemView] = [:]
var menuItemsMap: [String: LinkPreviewNativeActionView] = [:]
// Batching state for toolbar updates
private var hasPendingToolbarUpdate = false
private func addRouterToolbarItemAtIndex(
_ item: RouterToolbarItemView,
index: Int
) {
let identifier = item.identifier
toolbarItemsArray.insert(identifier, at: index)
toolbarItemsMap[identifier] = item
item.host = self
}
private func addMenuToolbarItemAtIndex(
_ item: LinkPreviewNativeActionView,
index: Int
) {
let identifier = item.identifier
toolbarItemsArray.insert(identifier, at: index)
menuItemsMap[identifier] = item
}
private func removeToolbarItemWithId(_ id: String) {
if let index = toolbarItemsArray.firstIndex(of: id) {
toolbarItemsArray.remove(at: index)
toolbarItemsMap.removeValue(forKey: id)
menuItemsMap.removeValue(forKey: id)
}
}
func updateToolbarItems() {
// If update already scheduled, skip - the pending async block will handle it
if hasPendingToolbarUpdate {
return
}
hasPendingToolbarUpdate = true
// Defer actual update to next run loop iteration
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.hasPendingToolbarUpdate = false
self.performToolbarUpdate()
}
}
private func performToolbarUpdate() {
if let controller = self.findViewController() {
if #available(iOS 18.0, *) {
let items = toolbarItemsArray.compactMap { identifier -> UIBarButtonItem? in
if let item = toolbarItemsMap[identifier] {
if item.routerHidden {
return nil
}
return item.barButtonItem
}
// TODO: Extract this logic to separate function
if let menu = menuItemsMap[identifier] {
if menu.routerHidden {
return nil
}
let item = UIBarButtonItem(
title: menu.label,
image: menu.image,
primaryAction: nil,
menu: menu.uiAction as? UIMenu
)
// Otherwise, the menu items will be reversed in the toolbar
item.preferredMenuElementOrder = .fixed
if #available(iOS 26.0, *) {
if let hidesSharedBackground = menu.hidesSharedBackground {
item.hidesSharedBackground = hidesSharedBackground
}
if let sharesBackground = menu.sharesBackground {
item.sharesBackground = sharesBackground
}
}
if let titleStyle = menu.titleStyle {
RouterFontUtils.setTitleStyle(fromConfig: titleStyle, for: item)
}
item.isEnabled = !menu.disabled
if let accessibilityLabel = menu.accessibilityLabelForMenu {
item.accessibilityLabel = accessibilityLabel
} else if let label = menu.label {
item.accessibilityLabel = label
}
if let accessibilityHint = menu.accessibilityHintForMenu {
item.accessibilityHint = accessibilityHint
}
item.tintColor = menu.customTintColor
if let style = menu.barButtonItemStyle {
item.style = style
}
return item
}
logger?.warn(
"[expo-router] Warning: Could not find toolbar item or menu for identifier \(identifier). This is most likely a bug in expo-router."
)
return nil
}
controller.setToolbarItems(items, animated: true)
controller.navigationController?.setToolbarHidden(
false, animated: true)
return
}
}
}
override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
if let toolbarItem = childComponentView as? RouterToolbarItemView {
if toolbarItem.identifier.isEmpty {
logger?.warn(
"[expo-router] RouterToolbarItemView identifier is empty. This is most likely a bug in expo-router."
)
return
}
addRouterToolbarItemAtIndex(toolbarItem, index: index)
} else if let menu = childComponentView as? LinkPreviewNativeActionView {
menu.parentMenuUpdatable = self
addMenuToolbarItemAtIndex(menu, index: index)
} else {
logger?.warn(
"[expo-router] Unknown child component view (\(childComponentView)) mounted to RouterToolbarHost. This is most likely a bug in expo-router."
)
}
updateToolbarItems()
}
override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
if let toolbarItem = childComponentView as? RouterToolbarItemView {
if toolbarItem.identifier.isEmpty {
logger?.warn(
"[expo-router] RouterToolbarItemView identifier is empty. This is most likely a bug in expo-router."
)
return
}
removeToolbarItemWithId(toolbarItem.identifier)
} else if let menu = childComponentView as? LinkPreviewNativeActionView {
if menu.identifier.isEmpty {
logger?.warn(
"[expo-router] Menu identifier is empty. This is most likely a bug in expo-router.")
return
}
removeToolbarItemWithId(menu.identifier)
} else {
logger?.warn(
"[expo-router] Unknown child component view (\(childComponentView)) unmounted from RouterToolbarHost. This is most likely a bug in expo-router."
)
}
updateToolbarItems()
}
override func didMoveToWindow() {
super.didMoveToWindow()
if window == nil {
// View was removed from window - hide toolbar and clear items
// Use cached controller since responder chain may be broken
if let controller = cachedController {
controller.setToolbarItems(nil, animated: true)
}
cachedController = nil // Clear cache when removed from window
} else {
// View was added to window - update toolbar items
updateToolbarItems()
}
}
func updateMenu() {
updateToolbarItems()
}
func findViewController() -> RNSScreen? {
if let cached = cachedController {
return cached
}
var responder: UIResponder? = self
while let r = responder {
if let screen = r as? RNSScreen {
cachedController = screen
return screen
}
responder = r.next
}
return nil
}
}