Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ internal class TabsContainer(

internal val selectedTab: TabsScreenFragment
get() =
checkNotNull(getFragmentForScreenKey(navState.selectedKey)) { "[RNScreens] No selected tab present" }
checkNotNull(getFragmentForScreenKey(navState.selectedScreenKey)) { "[RNScreens] No selected tab present" }

internal val invalidationFlags = TabsContainerInvalidationFlags()

Expand Down Expand Up @@ -259,14 +259,14 @@ internal class TabsContainer(
val tabSelectOp = pendingOperation as TabSelectOp

val nextSelectedMenuItemId =
checkNotNull(getMenuItemIdForFragment(requireFragmentForScreenKey(tabSelectOp.navState.selectedKey))) {
"[RNScreens] Failed to find Menu Item for screenKey: ${tabSelectOp.navState.selectedKey}"
checkNotNull(getMenuItemIdForFragment(requireFragmentForScreenKey(tabSelectOp.request.selectedScreenKey))) {
"[RNScreens] Failed to find Menu Item for screenKey: ${tabSelectOp.request.selectedScreenKey}"
}

if (rejectOpsWithStaleNavState && isNavStateStale(tabSelectOp.navState)) {
if (rejectOpsWithStaleNavState && isNavStateStale(tabSelectOp.request)) {
delegate.onNavStateUpdateRejected(
navState,
tabSelectOp.navState,
tabSelectOp.request,
TabsNavStateUpdateRejectionReason.STALE,
)
pendingOperation = null
Expand All @@ -281,7 +281,7 @@ internal class TabsContainer(
} else {
delegate.onNavStateUpdateRejected(
navState,
tabSelectOp.navState,
tabSelectOp.request,
TabsNavStateUpdateRejectionReason.REPEATED,
)
}
Expand Down Expand Up @@ -328,7 +328,7 @@ internal class TabsContainer(
val currentSelectedFragment = selectedTab

if (nextSelectedFragment === currentSelectedFragment) {
progressNavigationState(navState.selectedKey)
progressNavigationState(navState.selectedScreenKey)
return true
}

Expand All @@ -343,8 +343,8 @@ internal class TabsContainer(
return true
}

private fun progressNavigationState(selectedKey: String) {
navState = TabsNavState(selectedKey, navState.provenance + 1)
private fun progressNavigationState(selectedScreenKey: String) {
navState = TabsNavState(selectedScreenKey, navState.provenance + 1)
if (!isInExternalOperationContext) {
lastUINavState = navState
}
Expand Down Expand Up @@ -377,7 +377,15 @@ internal class TabsContainer(
navState,
isRepeated = isRepeated,
hasTriggeredSpecialEffect = hasTriggeredSpecialEffect,
actionOrigin = if (isInExternalOperationContext) TabsActionOrigin.PROGRAMMATIC_JS else TabsActionOrigin.USER,
actionOrigin =
if (isInExternalOperationContext) {
check(pendingOperation != null && pendingOperation is TabSelectOp) {
"[RNScreens] Unexpected pending operation $pendingOperation while in external operation context"
}
(pendingOperation as TabSelectOp).request.actionOrigin
Comment thread
kligarski marked this conversation as resolved.
} else {
TabsActionOrigin.USER
},
)
}

Expand Down Expand Up @@ -413,7 +421,7 @@ internal class TabsContainer(

private fun getSelectedTabsScreenFragmentId(): Int? =
tabsModel
.indexOfFirst { it.requireScreenKey == navState.selectedKey }
.indexOfFirst { it.requireScreenKey == navState.selectedScreenKey }
.takeIf { it != -1 }

private fun getMenuItemForTabsScreen(tabsScreen: TabsScreen): MenuItem? =
Expand Down Expand Up @@ -544,9 +552,9 @@ internal class TabsContainer(
fragmentManager = null
}

private fun isNavStateStale(state: TabsNavState): Boolean {
private fun isNavStateStale(request: TabsNavStateUpdateRequest): Boolean {
if (navState.isEmpty() || lastUINavState.isEmpty()) return false
return state.provenance < lastUINavState.provenance
return request.baseProvenance < lastUINavState.provenance
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ internal interface TabsContainerDelegate {
* Called when the container rejects a navigation state update.
*
* @param currentNavState The currently active navigation state that was kept.
* @param rejectedNavState The navigation state update that was rejected.
* @param rejectedRequest The navigation state update request that was rejected.
* @param reason Why the update was rejected.
*/
fun onNavStateUpdateRejected(
currentNavState: TabsNavState,
rejectedNavState: TabsNavState,
rejectedRequest: TabsNavStateUpdateRequest,
reason: TabsNavStateUpdateRejectionReason,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package com.swmansion.rnscreens.gamma.tabs.container
internal sealed class TabsContainerOp

internal data class TabSelectOp(
val navState: TabsNavState,
val request: TabsNavStateUpdateRequest,
) : TabsContainerOp()
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionR
/**
* Describes navigation state of a tabs container.
*
* @property selectedKey Screen key of the currently selected tab.
* @property selectedScreenKey Screen key of the currently selected tab.
* @property provenance Monotonically increasing number describing the history (generation) of the state.
* State with provenance `N + 1` is derived from state with provenance `N`.
* This allows detecting stale updates.
*/
data class TabsNavState(
val selectedKey: String,
val selectedScreenKey: String,
val provenance: Int,
) {
internal fun isEmpty(): Boolean = this === EMPTY
Expand All @@ -24,6 +24,23 @@ data class TabsNavState(
}
}

/**
* A request to change navigation state.
*
* Carries the target [selectedScreenKey], the [baseProvenance] of the state the request was derived from,
* and the [actionOrigin] (actor) that initiated it. Mirrors the public `TabsHostNavStateRequest` TS type
* plus an [actionOrigin] carried internally.
*
* @property selectedScreenKey Screen key of the requested tab.
* @property baseProvenance Provenance of the state this request was derived from. Used for staleness detection.
* @property actionOrigin Origin (actor) that initiated this request.
*/
data class TabsNavStateUpdateRequest(
val selectedScreenKey: String,
val baseProvenance: Int,
val actionOrigin: TabsActionOrigin,
)

/**
* Reason why a navigation state update was rejected by the container.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.swmansion.rnscreens.gamma.tabs.container.TabsContainer
import com.swmansion.rnscreens.gamma.tabs.container.TabsContainerDelegate
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionReason
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRequest
import com.swmansion.rnscreens.gamma.tabs.screen.TabsScreen
import com.swmansion.rnscreens.utils.RNSLog
import kotlin.properties.Delegates
Expand All @@ -30,7 +31,7 @@ class TabsHost(
TabsContainerDelegate,
UIManagerListener {
private val renderedScreens: ArrayList<TabsScreen> = arrayListOf()
private var jsNavStateRequest: TabsNavState = TabsNavState.EMPTY
private var jsNavStateRequest: TabsNavStateUpdateRequest? = null

private val container: TabsContainer =
TabsContainer(reactContext, this).apply {
Expand Down Expand Up @@ -110,9 +111,9 @@ class TabsHost(
container.removeAllTabsScreens()
}

internal fun updateJSNavStateRequest(navStateRequest: TabsNavState) {
internal fun updateJSNavStateRequest(navStateRequest: TabsNavStateUpdateRequest) {
jsNavStateRequest = navStateRequest
container.setContainerOperation(TabSelectOp(jsNavStateRequest.copy()))
container.setContainerOperation(TabSelectOp(navStateRequest.copy()))
}

private val layoutCallback =
Expand Down Expand Up @@ -163,7 +164,7 @@ class TabsHost(
actionOrigin: TabsActionOrigin,
) {
eventEmitter.emitOnTabSelectedEvent(
navState.selectedKey,
navState.selectedScreenKey,
navState.provenance,
isRepeated,
hasTriggeredSpecialEffect,
Expand All @@ -173,12 +174,12 @@ class TabsHost(

override fun onNavStateUpdateRejected(
currentNavState: TabsNavState,
rejectedNavState: TabsNavState,
rejectedRequest: TabsNavStateUpdateRequest,
reason: TabsNavStateUpdateRejectionReason,
) {
eventEmitter.emitOnTabSelectionRejectedEvent(
currentNavState,
rejectedNavState,
rejectedRequest,
reason,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.swmansion.rnscreens.gamma.common.event.BaseEventEmitter
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionReason
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRequest
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectedEvent
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectionPreventedEvent
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectionRejectedEvent
Expand Down Expand Up @@ -42,15 +43,15 @@ internal class TabsHostEventEmitter(
*/
fun emitOnTabSelectionRejectedEvent(
currentNavState: TabsNavState,
rejectedNavState: TabsNavState,
rejectedRequest: TabsNavStateUpdateRequest,
rejectionReason: TabsNavStateUpdateRejectionReason,
) {
reactEventDispatcher.dispatchEvent(
TabsHostTabSelectionRejectedEvent(
surfaceId,
viewTag,
currentNavState,
rejectedNavState,
rejectedRequest,
rejectionReason,
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import com.facebook.react.viewmanagers.RNSTabsHostAndroidManagerDelegate
import com.facebook.react.viewmanagers.RNSTabsHostAndroidManagerInterface
import com.swmansion.rnscreens.gamma.common.colorscheme.ColorScheme
import com.swmansion.rnscreens.gamma.helpers.makeEventRegistrationInfo
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
import com.swmansion.rnscreens.gamma.tabs.container.TabsActionOrigin
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRequest
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectedEvent
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectionPreventedEvent
import com.swmansion.rnscreens.gamma.tabs.host.event.TabsHostTabSelectionRejectedEvent
Expand Down Expand Up @@ -79,7 +80,13 @@ class TabsHostViewManager :
val navStateRequestMap = requireNotNull(value) { "[RNScreens] navStateRequest must not be nullish" }
val selectedScreenKey = requireNotNull(navStateRequestMap.getString("selectedScreenKey"))
val baseProvenance = requireNotNull(navStateRequestMap.getInt("baseProvenance"))
view.updateJSNavStateRequest(TabsNavState(selectedScreenKey, baseProvenance))
view.updateJSNavStateRequest(
TabsNavStateUpdateRequest(
selectedScreenKey = selectedScreenKey,
baseProvenance = baseProvenance,
actionOrigin = TabsActionOrigin.PROGRAMMATIC_JS,
),
)
}

override fun setRejectStaleNavStateUpdates(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class TabsHostTabSelectionPreventedEvent(

override fun getEventData(): WritableMap? =
Arguments.createMap().apply {
putString(EK_SELECTED_KEY, currentNavState.selectedKey)
putString(EK_SELECTED_KEY, currentNavState.selectedScreenKey)
putInt(EK_PROVENANCE, currentNavState.provenance)
putString(EK_PREVENTED_KEY, preventedScreenKey)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ import com.facebook.react.uimanager.events.Event
import com.swmansion.rnscreens.gamma.common.event.NamingAwareEventType
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavState
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRejectionReason
import com.swmansion.rnscreens.gamma.tabs.container.TabsNavStateUpdateRequest

/**
* React Native event dispatched to JS when a tab selection request is rejected by the container.
*
* Carries the currently active navigation state ([currentNavState]), the rejected update
* ([rejectedNavState]), and the [rejectionReason]. This event is never coalesced — every
* Carries the currently active navigation state ([currentNavState]), the rejected request
* ([rejectedRequest]), and the [rejectionReason]. This event is never coalesced — every
* rejection is delivered individually so the JS side has a complete picture of state transitions.
*/
class TabsHostTabSelectionRejectedEvent(
surfaceId: Int,
viewId: Int,
val currentNavState: TabsNavState,
val rejectedNavState: TabsNavState,
val rejectedRequest: TabsNavStateUpdateRequest,
val rejectionReason: TabsNavStateUpdateRejectionReason,
) : Event<TabsHostTabSelectionRejectedEvent>(surfaceId, viewId),
NamingAwareEventType {
Expand All @@ -31,10 +32,10 @@ class TabsHostTabSelectionRejectedEvent(

override fun getEventData(): WritableMap? =
Arguments.createMap().apply {
putString(EK_SELECTED_KEY, currentNavState.selectedKey)
putString(EK_SELECTED_KEY, currentNavState.selectedScreenKey)
putInt(EK_PROVENANCE, currentNavState.provenance)
putString(EK_REJECTED_KEY, rejectedNavState.selectedKey)
putInt(EK_REJECTED_PROVENANCE, rejectedNavState.provenance)
putString(EK_REJECTED_KEY, rejectedRequest.selectedScreenKey)
putInt(EK_REJECTED_PROVENANCE, rejectedRequest.baseProvenance)
putString(EK_REJECTION_REASON, rejectionReason.toString())
}

Expand Down
56 changes: 30 additions & 26 deletions ios/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,11 @@ - (UIImage *)loadBackButtonImageInViewController:(UIViewController *)vc
// in the image attribute not being updated. We manually set frame to the size of an image
// in order to trigger proper reload that'd update the image attribute.
RCTImageSource *imageSource = [RNSScreenStackHeaderConfig imageSourceFromImageView:imageView];
[imageView reactSetFrame:CGRectMake(imageView.frame.origin.x,
imageView.frame.origin.y,
imageSource.size.width,
imageSource.size.height)];
[imageView reactSetFrame:CGRectMake(
imageView.frame.origin.x,
imageView.frame.origin.y,
imageSource.size.width,
imageSource.size.height)];
Comment thread
kkafar marked this conversation as resolved.
}

UIImage *image = imageView.image;
Expand Down Expand Up @@ -847,12 +848,13 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone
return;
}

RCTAssert(childComponentView.superview == nil,
@"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)",
self,
childComponentView,
@(index),
@([childComponentView.superview tag]));
RCTAssert(
childComponentView.superview == nil,
@"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)",
self,
childComponentView,
@(index),
@([childComponentView.superview tag]));

// [_reactSubviews insertObject:(RNSScreenStackHeaderSubview *)childComponentView atIndex:index];
[self insertReactSubview:(RNSScreenStackHeaderSubview *)childComponentView atIndex:index];
Expand Down Expand Up @@ -1129,21 +1131,23 @@ @implementation RNSScreenStackHeaderConfigManager

@implementation RCTConvert (RNSScreenStackHeader)

RCT_ENUM_CONVERTER(UISemanticContentAttribute,
(@{
@"ltr" : @(UISemanticContentAttributeForceLeftToRight),
@"rtl" : @(UISemanticContentAttributeForceRightToLeft),
}),
UISemanticContentAttributeUnspecified,
integerValue)

RCT_ENUM_CONVERTER(UINavigationItemBackButtonDisplayMode,
(@{
@"default" : @(UINavigationItemBackButtonDisplayModeDefault),
@"generic" : @(UINavigationItemBackButtonDisplayModeGeneric),
@"minimal" : @(UINavigationItemBackButtonDisplayModeMinimal),
}),
UINavigationItemBackButtonDisplayModeDefault,
integerValue)
RCT_ENUM_CONVERTER(
UISemanticContentAttribute,
(@{
@"ltr" : @(UISemanticContentAttributeForceLeftToRight),
@"rtl" : @(UISemanticContentAttributeForceRightToLeft),
}),
UISemanticContentAttributeUnspecified,
integerValue)

RCT_ENUM_CONVERTER(
UINavigationItemBackButtonDisplayMode,
(@{
@"default" : @(UINavigationItemBackButtonDisplayModeDefault),
@"generic" : @(UINavigationItemBackButtonDisplayModeGeneric),
@"minimal" : @(UINavigationItemBackButtonDisplayModeMinimal),
}),
UINavigationItemBackButtonDisplayModeDefault,
integerValue)

@end
Loading
Loading