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,290 @@
import ExpoModulesCore
import UIKit
class RouterToolbarItemView: RouterViewWithLogger {
var identifier: String = ""
// Properties requiring full rebuild
@ReactiveProp(needsFullRebuild: true) var type: ItemType?
@ReactiveProp(needsFullRebuild: true) var customView: UIView?
// Properties allowing in-place updates
@ReactiveProp var title: String?
@ReactiveProp var systemImageName: String?
@ReactiveProp var xcassetName: String?
var customImage: SharedRef<UIImage>? {
didSet {
performUpdate()
}
}
@ReactiveProp var customTintColor: UIColor?
@ReactiveProp var imageRenderingMode: ImageRenderingMode?
@ReactiveProp var hidesSharedBackground: Bool = false
@ReactiveProp var sharesBackground: Bool = true
@ReactiveProp var barButtonItemStyle: UIBarButtonItem.Style?
@ReactiveProp var width: Double?
@ReactiveProp var selected: Bool = false
@ReactiveProp var possibleTitles: Set<String>?
@ReactiveProp var badgeConfiguration: BadgeConfiguration?
@ReactiveProp var titleStyle: TitleStyle?
@ReactiveProp var routerAccessibilityLabel: String?
@ReactiveProp var routerAccessibilityHint: String?
@ReactiveProp var disabled: Bool = false
// Using "routerHidden" to avoid conflict with UIView's "isHidden"
// This property is not applied in this component, but read by the host
@ReactiveProp var routerHidden: Bool = false
var host: RouterToolbarHostView?
private var currentBarButtonItem: UIBarButtonItem?
let onSelected = EventDispatcher()
func performRebuild() {
// There is no need to rebuild if not mounted
guard self.host != nil else { return }
rebuildBarButtonItem()
self.host?.updateToolbarItems()
}
func performUpdate() {
// There is no need to update if not mounted
guard self.host != nil else { return }
updateBarButtonItem()
// Even though we update in place, we need to notify the host
// so the toolbar array reference is updated and UIKit refreshes
self.host?.updateToolbarItems()
}
@objc func handleAction() {
onSelected()
}
var barButtonItem: UIBarButtonItem {
if let item = currentBarButtonItem {
return item
}
// If no item exists yet, create one
rebuildBarButtonItem()
return currentBarButtonItem ?? UIBarButtonItem()
}
private func updateBarButtonItem() {
guard let item = currentBarButtonItem else {
// If no current item exists, create one
rebuildBarButtonItem()
return
}
// Update content properties (title, image, etc.) for normal buttons
applyContentProperties(to: item)
// Update all common properties
applyCommonProperties(to: item)
}
private func rebuildBarButtonItem() {
var item = UIBarButtonItem()
if let customView {
item = UIBarButtonItem(customView: customView)
} else if type == .fluidSpacer {
item = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
} else if type == .fixedSpacer {
item = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
} else if type == .searchBar {
guard #available(iOS 26.0, *), let controller = self.host?.findViewController() else {
// Check for iOS 26, should already be guarded by the JS side, so this warning will only fire if controller is nil
logger?.warn(
"[expo-router] navigationItem.searchBarPlacementBarButtonItem not available. This is most likely a bug in expo-router."
)
currentBarButtonItem = nil
return
}
guard let navController = controller.navigationController else {
currentBarButtonItem = nil
return
}
guard navController.isNavigationBarHidden == false else {
logger?.warn(
"[expo-router] Toolbar.SearchBarPreferredSlot should only be used when stack header is shown."
)
currentBarButtonItem = nil
return
}
item = controller.navigationItem.searchBarPlacementBarButtonItem
} else {
// Normal button - apply content properties during initial creation
applyContentProperties(to: item)
}
// Set target and action for interactive buttons
item.target = self
item.action = #selector(handleAction)
applyCommonProperties(to: item)
currentBarButtonItem = item
}
private func applyContentProperties(to item: UIBarButtonItem) {
// Only apply content properties for normal buttons
if type == .normal || type == nil {
if let title {
item.title = title
} else {
item.title = ""
}
item.possibleTitles = possibleTitles
if let customImage {
// Use the UIImage from the SharedRef
let renderingMode: UIImage.RenderingMode = imageRenderingMode == .template ? .alwaysTemplate : .alwaysOriginal
item.image = customImage.ref.withRenderingMode(renderingMode)
} else if let xcassetName {
let renderingMode: UIImage.RenderingMode = imageRenderingMode == .template ? .alwaysTemplate : .alwaysOriginal
item.image = UIImage(named: xcassetName)?.withRenderingMode(renderingMode)
} else if let systemImageName {
// Fallback to SF Symbol
item.image = UIImage(systemName: systemImageName)
} else {
item.image = nil
}
item.tintColor = customTintColor
if let titleStyle {
RouterFontUtils.setTitleStyle(fromConfig: titleStyle, for: item)
} else {
RouterFontUtils.clearTitleStyle(for: item)
}
}
}
private func applyCommonProperties(to item: UIBarButtonItem) {
if #available(iOS 26.0, *) {
item.hidesSharedBackground = hidesSharedBackground
item.sharesBackground = sharesBackground
}
item.style = barButtonItemStyle ?? .plain
item.width = width.map { CGFloat($0) } ?? 0
item.isSelected = selected
item.accessibilityLabel = routerAccessibilityLabel
item.accessibilityHint = routerAccessibilityHint
item.isEnabled = !disabled
if #available(iOS 26.0, *) {
if let badgeConfig = badgeConfiguration {
var badge = UIBarButtonItem.Badge.indicator()
if let value = badgeConfig.value {
badge = .string(value)
}
if let backgroundColor = badgeConfig.backgroundColor {
badge.backgroundColor = backgroundColor
}
if let foregroundColor = badgeConfig.color {
badge.foregroundColor = foregroundColor
}
if badgeConfig.fontFamily != nil || badgeConfig.fontSize != nil
|| badgeConfig.fontWeight != nil {
let font = RouterFontUtils.convertTitleStyleToFont(
TitleStyle(
fontFamily: badgeConfig.fontFamily,
fontSize: badgeConfig.fontSize,
fontWeight: badgeConfig.fontWeight
))
badge.font = font
}
item.badge = badge
} else {
item.badge = nil
}
}
}
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
}
override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
guard customView == nil else {
logger?.warn(
"[expo-router] RouterToolbarItemView can only have one child view. This is most likely a bug in expo-router."
)
return
}
customView = childComponentView
}
override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
if customView == childComponentView {
childComponentView.removeFromSuperview()
customView = nil
}
}
}
enum ItemType: String, Enumerable {
case normal
case fixedSpacer
case fluidSpacer
case searchBar
}
struct BadgeConfiguration: Equatable {
var value: String?
var backgroundColor: UIColor?
var color: UIColor?
var fontFamily: String?
var fontSize: Double?
var fontWeight: String?
}
struct TitleStyle: Equatable {
var fontFamily: String?
var fontSize: Double?
var fontWeight: String?
var color: UIColor?
}
@propertyWrapper
struct ReactiveProp<Value: Equatable> {
private var value: Value
let needsFullRebuild: Bool
init(wrappedValue: Value, needsFullRebuild: Bool = false) {
self.value = wrappedValue
self.needsFullRebuild = needsFullRebuild
}
static subscript<EnclosingSelf: RouterToolbarItemView>(
_enclosingInstance instance: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, ReactiveProp<Value>>
) -> Value {
get {
instance[keyPath: storageKeyPath].value
}
set {
let oldValue = instance[keyPath: storageKeyPath].value
if oldValue != newValue {
instance[keyPath: storageKeyPath].value = newValue
if instance[keyPath: storageKeyPath].needsFullRebuild {
instance.performRebuild()
} else {
instance.performUpdate()
}
}
}
}
@available(*, unavailable, message: "Use the enclosing instance subscript.")
var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
}
extension ReactiveProp where Value: ExpressibleByNilLiteral {
init(needsFullRebuild: Bool = false) {
self.value = nil
self.needsFullRebuild = needsFullRebuild
}
}