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,190 @@
import ExpoModulesCore
import RNScreens
import UIKit
struct TabChangeCommand {
weak var tabBarController: UITabBarController?
let tabIndex: Int
}
internal class LinkPreviewNativeNavigation {
private weak var preloadedScreenView: RNSScreenView?
private weak var preloadedStackView: RNSScreenStackView?
private var tabChangeCommands: [TabChangeCommand] = []
private let logger: ExpoModulesCore.Logger?
init(logger: ExpoModulesCore.Logger?) {
self.logger = logger
}
func pushPreloadedView() {
self.performTabChanges()
guard let preloadedScreenView,
let preloadedStackView
else {
// Check if there were any tab change commands to perform
// If there were, the preview transition could be to a different tab only
if self.tabChangeCommands.isEmpty {
logger?.warn(
"[expo-router] No preloaded screen view to push. Link.Preview transition is only supported inside a native stack or native tabs navigators."
)
}
return
}
// Instead of pushing the preloaded screen view, we set its activity state
// React native screens will then handle the rest.
preloadedScreenView.activityState = Int32(RNSActivityState.onTop.rawValue)
preloadedStackView.markChildUpdated()
self.pushModalInnerScreenIfNeeded(screenView: preloadedScreenView)
}
func updatePreloadedView(screenId: String?, tabPath: TabPathPayload?, responder: UIView) {
self.tabChangeCommands = []
let oldTabKeys = tabPath?.path.map { $0.oldTabKey } ?? []
let stackOrTabView = findStackViewWithScreenIdOrTabBarController(
screenId: screenId, tabKeys: oldTabKeys, responder: responder)
guard let stackOrTabView else {
return
}
if RNScreensTabCompat.isTabScreen(stackOrTabView) {
let newTabKeys = tabPath?.path.map { $0.newTabKey } ?? []
// The order is important here. findStackViewWithScreenIdInSubViews must be called
// even if screenId is nil to compute the tabChangeCommands.
if let stackView = findStackViewWithScreenIdInSubViews(
screenId: screenId, tabKeys: newTabKeys, rootView: stackOrTabView), let screenId {
setPreloadedView(stackView: stackView, screenId: screenId)
}
} else if let stackView = stackOrTabView as? RNSScreenStackView, let screenId {
setPreloadedView(stackView: stackView, screenId: screenId)
}
}
private func performTabChanges() {
self.tabChangeCommands.forEach { command in
command.tabBarController?.selectedIndex = command.tabIndex
}
}
// If screen is a modal with header, it will have an inner stack screen
// https://github.com/software-mansion/react-native-screens/blob/8b82e081e8fdfa6e0864821134bda9e87a745b00/src/components/ScreenStackItem.tsx#L146-L160
// In this case we need to set the activity state of the inner screen as well.
private func pushModalInnerScreenIfNeeded(screenView: RNSScreenView) {
// If the screen is modal with header then it will have exactly one child - RNSNavigationController.
if screenView.isModal() && screenView.controller.children.count == 1 {
// To get the inner screen stack we need to go through RNSNavigationController.
// The structure is as follows:
// RNSScreenView (preloadedScreenView)
// RNSNavigationController (outer stack)
// RNSScreenStackView (innerScreenStack)
if let rnsNavController = screenView.controller.children.first
as? RNSNavigationController,
// The delegate of RNSNavigationController is RNSScreenStackView.
let innerScreenStack = rnsNavController.delegate as? RNSScreenStackView,
// The first and only child of the inner screen stack should be
// RNSScreenView (<ScreenStackItem>).
let screenContentView = innerScreenStack.reactSubviews().first as? RNSScreenView {
// Same as above, we let React Native Screens handle the transition.
// We need to set the activity of inner screen as well, because its
// react value is the same as the preloaded screen - 0.
// https://github.com/software-mansion/react-native-screens/blob/8b82e081e8fdfa6e0864821134bda9e87a745b00/src/components/ScreenStackItem.tsx#L151
screenContentView.activityState = Int32(RNSActivityState.onTop.rawValue)
innerScreenStack.markChildUpdated()
}
}
}
private func setPreloadedView(
stackView: RNSScreenStackView, screenId: String
) {
let screenViews = stackView.reactSubviews()
if let screenView = screenViews?.first(where: {
($0 as? RNSScreenView)?.screenId == screenId
}) as? RNSScreenView {
preloadedScreenView = screenView
preloadedStackView = stackView
}
}
// Allowing for null screenId to support preloading tab navigators
// Even if the desired screenId is not found, we still need to compute the tabChangeCommands
private func findStackViewWithScreenIdInSubViews(
screenId: String?, tabKeys: [String], rootView: UIView
) -> RNSScreenStackView? {
if let rootView = rootView as? RNSScreenStackView,
let screenId {
if rootView.screenIds.contains(screenId) {
return rootView
}
} else if let tabBarController = getTabBarControllerFromTabView(view: rootView) {
if let (tabIndex, tabView) = getIndexAndViewOfFirstTabWithKey(
tabBarController: tabBarController, tabKeys: tabKeys) {
self.tabChangeCommands.append(
TabChangeCommand(tabBarController: tabBarController, tabIndex: tabIndex))
for subview in tabView.subviews {
if let result = findStackViewWithScreenIdInSubViews(
screenId: screenId, tabKeys: tabKeys, rootView: subview) {
return result
}
}
}
} else {
for subview in rootView.subviews {
let result = findStackViewWithScreenIdInSubViews(
screenId: screenId, tabKeys: tabKeys, rootView: subview)
if result != nil {
return result
}
}
}
return nil
}
private func getIndexAndViewOfFirstTabWithKey(
tabBarController: UITabBarController, tabKeys: [String]
) -> (tabIndex: Int, tabView: UIView)? {
let views = tabBarController.viewControllers?.compactMap { $0.view } ?? []
let enumeratedViews = views.enumerated()
if let result =
enumeratedViews
.first(where: { _, view in
guard let tabKey = RNScreensTabCompat.tabKey(from: view) else {
return false
}
return tabKeys.contains(tabKey)
}) {
return (result.offset, result.element)
}
return nil
}
private func getTabBarControllerFromTabView(view: UIView) -> UITabBarController? {
if let tabBarController = RNScreensTabCompat.tabBarController(fromTabScreen: view) {
return tabBarController
}
return RNScreensTabCompat.tabBarController(fromTabHost: view)
}
private func findStackViewWithScreenIdOrTabBarController(
screenId: String?, tabKeys: [String], responder: UIView
) -> UIView? {
var currentResponder: UIResponder? = responder
while let nextResponder = currentResponder?.next {
if let view = nextResponder as? RNSScreenStackView,
let screenId {
if view.screenIds.contains(screenId) {
return view
}
} else if let nextView = nextResponder as? UIView,
let tabKey = RNScreensTabCompat.tabKey(from: nextView),
tabKeys.contains(tabKey) {
return nextView
}
currentResponder = nextResponder
}
return nil
}
}