react-native-screens 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,15 @@
1
1
  <img src="https://user-images.githubusercontent.com/16062886/117443651-c13d9500-af38-11eb-888d-b6a0b580760c.png" width="100%" alt="React Native Screens by Software Mansion" >
2
2
 
3
-
4
3
  This project aims to expose native navigation container components to React Native. It is not designed to be used as a standalone library but rather as a dependency of a [full-featured navigation library](https://github.com/react-navigation/react-navigation).
5
4
 
5
+ ## Supported platforms
6
+
7
+ - [x] iOS
8
+ - [x] Android
9
+ - [x] tvOS
10
+ - [x] Windows
11
+ - [x] Web
12
+
6
13
  ## Installation
7
14
 
8
15
  ### iOS
@@ -76,9 +83,10 @@ You can also disable the usage of native screens per navigator with [`detachInac
76
83
  ### Using `createNativeStackNavigator` with React Navigation
77
84
 
78
85
  To take advantage of the native stack navigator primitive for React Navigation that leverages `UINavigationController` on iOS and `Fragment` on Android, please refer:
86
+
79
87
  - for React Navigation >= v6 to the [Native Stack Navigator part of React Navigation documentation](https://reactnavigation.org/docs/native-stack-navigator)
80
- - for React Navigation v5 to the [README in react-native-screens/native-stack](https://github.com/software-mansion/react-native-screens/tree/master/native-stack)
81
- - for older versions to the [README in react-native-screens/createNativeStackNavigator](https://github.com/software-mansion/react-native-screens/tree/master/createNativeStackNavigator)
88
+ - for React Navigation v5 to the [README in react-native-screens/native-stack](https://github.com/software-mansion/react-native-screens/tree/master/native-stack)
89
+ - for older versions to the [README in react-native-screens/createNativeStackNavigator](https://github.com/software-mansion/react-native-screens/tree/master/createNativeStackNavigator)
82
90
 
83
91
  ## Interop with [react-native-navigation](https://github.com/wix/react-native-navigation)
84
92
 
@@ -93,6 +101,26 @@ This library should work out of the box with all existing react-native libraries
93
101
  If you are building a navigation library you may want to use `react-native-screens` to have control over which parts of the React component tree are attached to the native view hierarchy.
94
102
  To do that, `react-native-screens` provides you with the components documented [here](https://github.com/kmagiera/react-native-screens/tree/master/guides/GUIDE_FOR_LIBRARY_AUTHORS.md).
95
103
 
104
+ ## Common problems
105
+
106
+ ### Problems with header on iOS
107
+
108
+ - [Focused search bar causes new screens to have incorrect header](https://github.com/software-mansion/react-native-screens/issues/996)
109
+ - [Scrollable content gets cut off by the header with a search bar](https://github.com/software-mansion/react-native-screens/issues/1120)
110
+ - [RefreshControl does not work properly with NativeStackNavigator and largeTitle](https://github.com/software-mansion/react-native-screens/issues/395)
111
+
112
+ #### Solution
113
+
114
+ Use `ScrollView` with prop `contentInsetAdjustmentBehavior=“automatic”` as a main container of the screen and set `headerTranslucent: true` in screen options.
115
+
116
+ ### Other problems
117
+
118
+ | Problem | Solution |
119
+ | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
120
+ | [SVG component becomes transparent when goBack](https://github.com/software-mansion/react-native-screens/issues/773) | [related PRs](https://github.com/software-mansion/react-native-screens/issues/773#issuecomment-783469792) |
121
+ | [Memory leak while moving from one screen to another in the same stack](https://github.com/software-mansion/react-native-screens/issues/843) | [explanation](https://github.com/software-mansion/react-native-screens/issues/843#issuecomment-832034119) |
122
+ | [LargeHeader stays small after pop/goBack/swipe gesture on iOS 14+](https://github.com/software-mansion/react-native-screens/issues/649) | [potential fix](https://github.com/software-mansion/react-native-screens/issues/649#issuecomment-712199895) |
123
+
96
124
  ## Contributing
97
125
 
98
126
  There are many ways to contribute to this project. See [CONTRIBUTING](https://github.com/kmagiera/react-native-screens/tree/master/guides/CONTRIBUTING.md) guide for more information. Thank you for your interest in contributing!
@@ -1,4 +1,7 @@
1
1
  buildscript {
2
+ ext.safeExtGet = {prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
2
5
  repositories {
3
6
  google()
4
7
  jcenter()
@@ -6,21 +9,19 @@ buildscript {
6
9
  }
7
10
  dependencies {
8
11
  classpath('com.android.tools.build:gradle:4.2.2')
9
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
12
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.4.10')}"
13
+ classpath "com.diffplug.spotless:spotless-plugin-gradle:5.15.0"
10
14
  }
11
15
  }
12
16
 
13
- plugins {
14
- id "com.diffplug.spotless" version "5.14.1"
17
+ // spotless is only accessible within react-native-screens repo
18
+ if (project == rootProject) {
19
+ apply from: 'spotless.gradle'
15
20
  }
16
21
 
17
22
  apply plugin: 'com.android.library'
18
23
  apply plugin: 'kotlin-android'
19
24
 
20
- def safeExtGet(prop, fallback) {
21
- rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
22
- }
23
-
24
25
  android {
25
26
  compileSdkVersion safeExtGet('compileSdkVersion', 28)
26
27
 
@@ -33,6 +34,10 @@ android {
33
34
  lintOptions {
34
35
  abortOnError false
35
36
  }
37
+ compileOptions {
38
+ sourceCompatibility JavaVersion.VERSION_1_8
39
+ targetCompatibility JavaVersion.VERSION_1_8
40
+ }
36
41
  }
37
42
 
38
43
  repositories {
@@ -55,18 +60,4 @@ dependencies {
55
60
  implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
56
61
  implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
57
62
  implementation 'com.google.android.material:material:1.1.0'
58
- implementation "androidx.core:core-ktx:1.6.0"
59
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.20"
60
- }
61
-
62
- plugins.apply("com.diffplug.spotless")
63
-
64
- spotless {
65
- kotlin {
66
- target 'src/**/*.kt'
67
- ktlint("0.40.0")
68
- trimTrailingWhitespace()
69
- indentWithSpaces()
70
- endWithNewline()
71
- }
72
63
  }
@@ -11,6 +11,7 @@ import androidx.fragment.app.FragmentActivity
11
11
  import androidx.fragment.app.FragmentManager
12
12
  import androidx.fragment.app.FragmentTransaction
13
13
  import com.facebook.react.ReactRootView
14
+ import com.facebook.react.bridge.ReactContext
14
15
  import com.facebook.react.modules.core.ChoreographerCompat
15
16
  import com.facebook.react.modules.core.ReactChoreographer
16
17
  import com.swmansion.rnscreens.Screen.ActivityState
@@ -176,16 +177,12 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
176
177
  return transaction
177
178
  }
178
179
 
179
- private fun attachScreen(screenFragment: ScreenFragment) {
180
- createTransaction().add(id, screenFragment).commitNowAllowingStateLoss()
180
+ private fun attachScreen(transaction: FragmentTransaction, screenFragment: ScreenFragment) {
181
+ transaction.add(id, screenFragment)
181
182
  }
182
183
 
183
- private fun moveToFront(screenFragment: ScreenFragment) {
184
- createTransaction().remove(screenFragment).add(id, screenFragment).commitNowAllowingStateLoss()
185
- }
186
-
187
- private fun detachScreen(screenFragment: ScreenFragment) {
188
- createTransaction().remove(screenFragment).commitNowAllowingStateLoss()
184
+ private fun detachScreen(transaction: FragmentTransaction, screenFragment: ScreenFragment) {
185
+ transaction.remove(screenFragment)
189
186
  }
190
187
 
191
188
  private fun getActivityState(screenFragment: ScreenFragment): ActivityState? {
@@ -276,10 +273,18 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
276
273
  // The exception to this rule is `updateImmediately` which is triggered by actions
277
274
  // not connected to React view hierarchy changes, but rather internal events
278
275
  mNeedUpdate = true
276
+ (context as? ReactContext)?.runOnUiQueueThread {
277
+ // We schedule the update here because LayoutAnimations of `react-native-reanimated`
278
+ // sometimes attach/detach screens after the layout block of `ScreensShadowNode` has
279
+ // already run, and we want to update the container then too. In the other cases,
280
+ // this code will do nothing since it will run after the UIBlock when `mNeedUpdate`
281
+ // will already be false.
282
+ performUpdates()
283
+ }
279
284
  }
280
285
 
281
286
  protected fun performUpdatesNow() {
282
- // we want to update the immediately when the fragment manager is set or native back button
287
+ // we want to update immediately when the fragment manager is set or native back button
283
288
  // dismiss is dispatched or Screen's activityState changes since it is not connected to React
284
289
  // view hierarchy changes and will not trigger `onBeforeLayout` method of `ScreensShadowNode`
285
290
  mNeedUpdate = true
@@ -287,7 +292,7 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
287
292
  }
288
293
 
289
294
  fun performUpdates() {
290
- if (!mNeedUpdate || !mIsAttached || mFragmentManager == null) {
295
+ if (!mNeedUpdate || !mIsAttached || mFragmentManager == null || mFragmentManager?.isDestroyed == true) {
291
296
  return
292
297
  }
293
298
  mNeedUpdate = false
@@ -296,43 +301,53 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
296
301
  }
297
302
 
298
303
  open fun onUpdate() {
299
- // detach screens that are no longer active
300
- val orphaned: MutableSet<Fragment> = HashSet(requireNotNull(mFragmentManager, { "mFragmentManager is null when performing update in ScreenContainer" }).fragments)
301
- for (screenFragment in mScreenFragments) {
302
- if (getActivityState(screenFragment) === ActivityState.INACTIVE &&
303
- screenFragment.isAdded
304
- ) {
305
- detachScreen(screenFragment)
304
+ createTransaction().let {
305
+ // detach screens that are no longer active
306
+ val orphaned: MutableSet<Fragment> = HashSet(requireNotNull(mFragmentManager, { "mFragmentManager is null when performing update in ScreenContainer" }).fragments)
307
+ for (screenFragment in mScreenFragments) {
308
+ if (getActivityState(screenFragment) === ActivityState.INACTIVE &&
309
+ screenFragment.isAdded
310
+ ) {
311
+ detachScreen(it, screenFragment)
312
+ }
313
+ orphaned.remove(screenFragment)
306
314
  }
307
- orphaned.remove(screenFragment)
308
- }
309
- if (orphaned.isNotEmpty()) {
310
- val orphanedAry = orphaned.toTypedArray()
311
- for (fragment in orphanedAry) {
312
- if (fragment is ScreenFragment) {
313
- if (fragment.screen.container == null) {
314
- detachScreen(fragment)
315
+ if (orphaned.isNotEmpty()) {
316
+ val orphanedAry = orphaned.toTypedArray()
317
+ for (fragment in orphanedAry) {
318
+ if (fragment is ScreenFragment) {
319
+ if (fragment.screen.container == null) {
320
+ detachScreen(it, fragment)
321
+ }
315
322
  }
316
323
  }
317
324
  }
318
- }
319
- var transitioning = true
320
- if (topScreen != null) {
325
+
321
326
  // if there is an "onTop" screen it means the transition has ended
322
- transitioning = false
323
- }
327
+ val transitioning = topScreen == null
324
328
 
325
- // attach newly activated screens
326
- var addedBefore = false
327
- for (screenFragment in mScreenFragments) {
328
- val activityState = getActivityState(screenFragment)
329
- if (activityState !== ActivityState.INACTIVE && !screenFragment.isAdded) {
330
- addedBefore = true
331
- attachScreen(screenFragment)
332
- } else if (activityState !== ActivityState.INACTIVE && addedBefore) {
333
- moveToFront(screenFragment)
329
+ // attach newly activated screens
330
+ var addedBefore = false
331
+ val pendingFront: ArrayList<T> = ArrayList()
332
+
333
+ for (screenFragment in mScreenFragments) {
334
+ val activityState = getActivityState(screenFragment)
335
+ if (activityState !== ActivityState.INACTIVE && !screenFragment.isAdded) {
336
+ addedBefore = true
337
+ attachScreen(it, screenFragment)
338
+ } else if (activityState !== ActivityState.INACTIVE && addedBefore) {
339
+ // we detach the screen and then reattach it later to make it appear on front
340
+ detachScreen(it, screenFragment)
341
+ pendingFront.add(screenFragment)
342
+ }
343
+ screenFragment.screen.setTransitioning(transitioning)
344
+ }
345
+
346
+ for (screenFragment in pendingFront) {
347
+ attachScreen(it, screenFragment)
334
348
  }
335
- screenFragment.screen.setTransitioning(transitioning)
349
+
350
+ it.commitNowAllowingStateLoss()
336
351
  }
337
352
  }
338
353
 
@@ -3,8 +3,6 @@ package com.swmansion.rnscreens
3
3
  import android.content.Context
4
4
  import android.graphics.Canvas
5
5
  import android.view.View
6
- import androidx.fragment.app.Fragment
7
- import androidx.fragment.app.FragmentManager
8
6
  import androidx.fragment.app.FragmentTransaction
9
7
  import com.facebook.react.bridge.ReactContext
10
8
  import com.facebook.react.uimanager.UIManagerModule
@@ -20,21 +18,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
20
18
  private val drawingOpPool: MutableList<DrawingOp> = ArrayList()
21
19
  private val drawingOps: MutableList<DrawingOp> = ArrayList()
22
20
  private var mTopScreen: ScreenStackFragment? = null
23
- private val mBackStackListener = FragmentManager.OnBackStackChangedListener {
24
- if (mFragmentManager?.backStackEntryCount == 0) {
25
- // when back stack entry count hits 0 it means the user's navigated back using hw back
26
- // button. As the "fake" transaction we installed on the back stack does nothing we need
27
- // to handle back navigation on our own.
28
- mTopScreen?.let { dismiss(it) }
29
- }
30
- }
31
- private val mLifecycleCallbacks: FragmentManager.FragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
32
- override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
33
- if (mTopScreen === f) {
34
- setupBackHandlerIfNeeded(f)
35
- }
36
- }
37
- }
38
21
  private var mRemovalTransitionStarted = false
39
22
  private var isDetachingCurrentScreen = false
40
23
  private var reverseLastTwoChildren = false
@@ -65,28 +48,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
65
48
  return ScreenStackFragment(screen)
66
49
  }
67
50
 
68
- override fun onDetachedFromWindow() {
69
- mFragmentManager?.let {
70
- it.removeOnBackStackChangedListener(mBackStackListener)
71
- it.unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks)
72
- if (!it.isStateSaved && !it.isDestroyed) {
73
- // State save means that the container where fragment manager was installed has been
74
- // unmounted.
75
- // This could happen as a result of dismissing nested stack. In such a case we don't need to
76
- // reset back stack as it'd result in a crash caused by the fact the fragment manager is no
77
- // longer attached.
78
- it.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
79
- }
80
- }
81
- super.onDetachedFromWindow()
82
- }
83
-
84
- override fun onAttachedToWindow() {
85
- super.onAttachedToWindow()
86
- val fragmentManager = requireNotNull(mFragmentManager, { "mFragmentManager is null when ScreenStack attached to window" })
87
- fragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false)
88
- }
89
-
90
51
  override fun startViewTransition(view: View) {
91
52
  super.startViewTransition(view)
92
53
  mRemovalTransitionStarted = true
@@ -159,10 +120,8 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
159
120
  // if the previous top screen does not exist anymore and the new top was not on the stack
160
121
  // before, probably replace or reset was called, so we play the "close animation".
161
122
  // Otherwise it's open animation
162
- shouldUseOpenAnimation = (
163
- mScreenFragments.contains(mTopScreen) ||
164
- newTop.screen.replaceAnimation !== Screen.ReplaceAnimation.POP
165
- )
123
+ val containsTopScreen = mTopScreen?.let { mScreenFragments.contains(it) } == true
124
+ shouldUseOpenAnimation = containsTopScreen || newTop.screen.replaceAnimation !== Screen.ReplaceAnimation.POP
166
125
  stackAnimation = newTop.screen.stackAnimation
167
126
  } else if (mTopScreen == null && newTop != null) {
168
127
  // mTopScreen was not present before so newTop is the first screen added to a stack
@@ -278,7 +237,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
278
237
  mStack.clear()
279
238
  mStack.addAll(mScreenFragments)
280
239
  it.commitNowAllowingStateLoss()
281
- mTopScreen?.let { screen -> setupBackHandlerIfNeeded(screen) }
282
240
  }
283
241
  }
284
242
 
@@ -288,61 +246,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
288
246
  }
289
247
  }
290
248
 
291
- /**
292
- * The below method sets up fragment manager's back stack in a way that it'd trigger our back
293
- * stack change listener when hw back button is clicked.
294
- *
295
- *
296
- * Because back stack by default rolls back the transaction the stack entry is associated with
297
- * we generate a "fake" transaction that hides and shows the top fragment. As a result when back
298
- * stack entry is rolled back nothing happens and we are free to handle back navigation on our own
299
- * in `mBackStackListener`.
300
- *
301
- *
302
- * We pop that "fake" transaction each time we update stack and we add a new one in case the
303
- * top screen is allowed to be dismissed using hw back button. This way in the listener we can
304
- * tell if back button was pressed based on the count of the items on back stack. We expect 0
305
- * items in case hw back is pressed because we try to keep the number of items at 1 by always
306
- * resetting and adding new items. In case we don't add a new item to back stack we remove
307
- * listener so that it does not get triggered.
308
- *
309
- *
310
- * It is important that we don't install back handler when stack contains a single screen as in
311
- * that case we want the parent navigator or activity handler to take over.
312
- */
313
- private fun setupBackHandlerIfNeeded(topScreen: ScreenStackFragment) {
314
- if (mTopScreen?.isResumed != true) {
315
- // if the top fragment is not in a resumed state, adding back stack transaction would throw.
316
- // In such a case we skip installing back handler and use FragmentLifecycleCallbacks to get
317
- // notified when it gets resumed so that we can install the handler.
318
- return
319
- }
320
- mFragmentManager?.let {
321
- it.removeOnBackStackChangedListener(mBackStackListener)
322
- it.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
323
- var firstScreen: ScreenStackFragment? = null
324
- var i = 0
325
- val size = mStack.size
326
- while (i < size) {
327
- val screen = mStack[i]
328
- if (!mDismissed.contains(screen)) {
329
- firstScreen = screen
330
- break
331
- }
332
- i++
333
- }
334
- if (topScreen !== firstScreen && topScreen.isDismissible) {
335
- it
336
- .beginTransaction()
337
- .show(topScreen)
338
- .addToBackStack(BACK_STACK_TAG)
339
- .setPrimaryNavigationFragment(topScreen)
340
- .commitNowAllowingStateLoss()
341
- it.addOnBackStackChangedListener(mBackStackListener)
342
- }
343
- }
344
- }
345
-
346
249
  // below methods are taken from
347
250
  // https://github.com/airbnb/native-navigation/blob/9cf50bf9b751b40778f473f3b19fcfe2c4d40599/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinatorLayout.java#L43
348
251
  // and are used to swap the order of drawing views when navigating forward with the transitions
@@ -418,7 +321,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
418
321
  }
419
322
 
420
323
  companion object {
421
- private const val BACK_STACK_TAG = "RN_SCREEN_LAST"
422
324
  private fun isSystemAnimation(stackAnimation: StackAnimation): Boolean {
423
325
  return stackAnimation === StackAnimation.DEFAULT || stackAnimation === StackAnimation.FADE || stackAnimation === StackAnimation.NONE
424
326
  }
@@ -145,9 +145,6 @@ class ScreenStackFragment : ScreenFragment {
145
145
  return view
146
146
  }
147
147
 
148
- val isDismissible: Boolean
149
- get() = screen.isGestureEnabled
150
-
151
148
  fun canNavigateBack(): Boolean {
152
149
  val container: ScreenContainer<*>? = screen.container
153
150
  check(container is ScreenStack) { "ScreenStackFragment added into a non-stack container" }
@@ -369,7 +369,7 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
369
369
  mDirection = direction
370
370
  }
371
371
 
372
- private class DebugMenuToolbar(context: Context?) : Toolbar(context) {
372
+ private class DebugMenuToolbar(context: Context) : Toolbar(context) {
373
373
  override fun showOverflowMenu(): Boolean {
374
374
  (context.applicationContext as ReactApplication)
375
375
  .reactNativeHost
package/ios/RNSScreen.h CHANGED
@@ -65,6 +65,7 @@ typedef NS_ENUM(NSInteger, RNSWindowTrait) {
65
65
 
66
66
  - (instancetype)initWithView:(UIView *)view;
67
67
  - (void)notifyFinishTransitioning;
68
+ - (UIViewController *)findChildVCForConfigAndTrait:(RNSWindowTrait)trait includingModals:(BOOL)includingModals;
68
69
 
69
70
  @end
70
71
 
package/ios/RNSScreen.m CHANGED
@@ -564,6 +564,8 @@
564
564
  _shouldNotify = NO;
565
565
  }
566
566
 
567
+ [self hideHeaderIfNecessary];
568
+
567
569
  // as per documentation of these methods
568
570
  _goingForward = [self isBeingPresented] || [self isMovingToParentViewController];
569
571
 
@@ -575,6 +577,35 @@
575
577
  }
576
578
  }
577
579
 
580
+ - (void)hideHeaderIfNecessary
581
+ {
582
+ #if !TARGET_OS_TV
583
+ // On iOS >=13, there is a bug when user transitions from screen with active search bar to screen without header
584
+ // In that case default iOS header will be shown. To fix this we hide header when the screens that appears has header
585
+ // hidden and search bar was active on previous screen. We need to do it asynchronously, because default header is
586
+ // added after viewWillAppear.
587
+ if (@available(iOS 13.0, *)) {
588
+ NSUInteger currentIndex = [self.navigationController.viewControllers indexOfObject:self];
589
+
590
+ if (currentIndex > 0 && [self.view.reactSubviews[0] isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
591
+ UINavigationItem *prevNavigationItem =
592
+ [self.navigationController.viewControllers objectAtIndex:currentIndex - 1].navigationItem;
593
+ RNSScreenStackHeaderConfig *config = ((RNSScreenStackHeaderConfig *)self.view.reactSubviews[0]);
594
+
595
+ BOOL wasSearchBarActive = prevNavigationItem.searchController.active;
596
+ BOOL shouldHideHeader = config.hide;
597
+
598
+ if (wasSearchBarActive && shouldHideHeader) {
599
+ dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0);
600
+ dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
601
+ [self.navigationController setNavigationBarHidden:YES animated:NO];
602
+ });
603
+ }
604
+ }
605
+ }
606
+ #endif
607
+ }
608
+
578
609
  - (void)viewWillDisappear:(BOOL)animated
579
610
  {
580
611
  [super viewWillDisappear:animated];
@@ -13,6 +13,8 @@
13
13
 
14
14
  @interface RNScreensViewController : UIViewController <RNScreensViewControllerDelegate>
15
15
 
16
+ - (UIViewController *)findActiveChildVC;
17
+
16
18
  @end
17
19
 
18
20
  @interface RNSScreenContainerManager : RCTViewManager
@@ -489,6 +489,23 @@
489
489
  [self setModalViewControllers:modalControllers];
490
490
  }
491
491
 
492
+ // By default, the header buttons that are not inside the native hit area
493
+ // cannot be clicked, so we check it by ourselves
494
+ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
495
+ {
496
+ if (CGRectContainsPoint(_controller.navigationBar.frame, point)) {
497
+ // headerConfig should be the first subview of the topmost screen
498
+ UIView *headerConfig = [[_reactSubviews.lastObject reactSubviews] firstObject];
499
+ if ([headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
500
+ UIView *headerHitTestResult = [headerConfig hitTest:point withEvent:event];
501
+ if (headerHitTestResult != nil) {
502
+ return headerHitTestResult;
503
+ }
504
+ }
505
+ }
506
+ return [super hitTest:point withEvent:event];
507
+ }
508
+
492
509
  - (void)layoutSubviews
493
510
  {
494
511
  [super layoutSubviews];
@@ -118,6 +118,27 @@
118
118
  _screenView = nil;
119
119
  }
120
120
 
121
+ // this method is never invoked by the system since this view
122
+ // is not added to native view hierarchy so we can apply our logic
123
+ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
124
+ {
125
+ for (RNSScreenStackHeaderSubview *subview in _reactSubviews) {
126
+ if (subview.type == RNSScreenStackHeaderSubviewTypeLeft || subview.type == RNSScreenStackHeaderSubviewTypeRight) {
127
+ // we wrap the headerLeft/Right component in a UIBarButtonItem
128
+ // so we need to use the only subview of it to retrieve the correct view
129
+ UIView *headerComponent = subview.subviews.firstObject;
130
+ // we convert the point to RNSScreenStackView since it always contains the header inside it
131
+ CGPoint convertedPoint = [_screenView.reactSuperview convertPoint:point toView:headerComponent];
132
+
133
+ UIView *hitTestResult = [headerComponent hitTest:convertedPoint withEvent:event];
134
+ if (hitTestResult != nil) {
135
+ return hitTestResult;
136
+ }
137
+ }
138
+ }
139
+ return nil;
140
+ }
141
+
121
142
  - (void)updateViewControllerIfNeeded
122
143
  {
123
144
  UIViewController *vc = _screenView.controller;
@@ -138,10 +159,23 @@
138
159
  }
139
160
  }
140
161
 
162
+ - (void)layoutNavigationControllerView
163
+ {
164
+ UIViewController *vc = _screenView.controller;
165
+ UINavigationController *navctr = vc.navigationController;
166
+ [navctr.view setNeedsLayout];
167
+ }
168
+
141
169
  - (void)didSetProps:(NSArray<NSString *> *)changedProps
142
170
  {
143
171
  [super didSetProps:changedProps];
144
172
  [self updateViewControllerIfNeeded];
173
+ // We need to layout navigation controller view after translucent prop changes, because otherwise
174
+ // frame of RNSScreen will not be changed and screen content will remain the same size.
175
+ // For more details look at https://github.com/software-mansion/react-native-screens/issues/1158
176
+ if ([changedProps containsObject:@"translucent"]) {
177
+ [self layoutNavigationControllerView];
178
+ }
145
179
  }
146
180
 
147
181
  - (void)didUpdateReactSubviews
@@ -444,8 +478,10 @@
444
478
 
445
479
  [navctr setNavigationBarHidden:shouldHide animated:animated];
446
480
 
447
- if (config.direction == UISemanticContentAttributeForceLeftToRight ||
448
- config.direction == UISemanticContentAttributeForceRightToLeft) {
481
+ if ((config.direction == UISemanticContentAttributeForceLeftToRight ||
482
+ config.direction == UISemanticContentAttributeForceRightToLeft) &&
483
+ // iOS 12 cancels swipe gesture when direction is changed. See #1091
484
+ navctr.view.semanticContentAttribute != config.direction) {
449
485
  navctr.view.semanticContentAttribute = config.direction;
450
486
  navctr.navigationBar.semanticContentAttribute = config.direction;
451
487
  }
@@ -554,6 +590,13 @@
554
590
  break;
555
591
  }
556
592
  case RNSScreenStackHeaderSubviewTypeSearchBar: {
593
+ if (subview.subviews == nil || [subview.subviews count] == 0) {
594
+ RCTLogWarn(
595
+ @"Failed to attach search bar to the header. We recommend using `useLayoutEffect` when managing "
596
+ "searchBar properties dynamically. \n\nSee: github.com/software-mansion/react-native-screens/issues/1188");
597
+ break;
598
+ }
599
+
557
600
  if ([subview.subviews[0] isKindOfClass:[RNSSearchBar class]]) {
558
601
  #if !TARGET_OS_TV
559
602
  if (@available(iOS 11.0, *)) {
@@ -17,4 +17,9 @@
17
17
  + (UIInterfaceOrientationMask)maskFromOrientation:(UIInterfaceOrientation)orientation;
18
18
  #endif
19
19
 
20
+ + (BOOL)shouldAskScreensForTrait:(RNSWindowTrait)trait
21
+ includingModals:(BOOL)includingModals
22
+ inViewController:(UIViewController *)vc;
23
+ + (BOOL)shouldAskScreensForScreenOrientationInViewController:(UIViewController *)vc;
24
+
20
25
  @end
@@ -1,5 +1,7 @@
1
1
  #import "RNSScreenWindowTraits.h"
2
2
  #import "RNSScreen.h"
3
+ #import "RNSScreenContainer.h"
4
+ #import "RNSScreenStack.h"
3
5
 
4
6
  @implementation RNSScreenWindowTraits
5
7
 
@@ -190,4 +192,31 @@
190
192
  }
191
193
  #endif
192
194
 
195
+ // method to be used in Expo for checking if RNScreens have trait set
196
+ + (BOOL)shouldAskScreensForTrait:(RNSWindowTrait)trait
197
+ includingModals:(BOOL)includingModals
198
+ inViewController:(UIViewController *)vc
199
+ {
200
+ UIViewController *lastViewController = [[vc childViewControllers] lastObject];
201
+ if ([lastViewController conformsToProtocol:@protocol(RNScreensViewControllerDelegate)]) {
202
+ UIViewController *vc = nil;
203
+ if ([lastViewController isKindOfClass:[RNScreensViewController class]]) {
204
+ vc = [(RNScreensViewController *)lastViewController findActiveChildVC];
205
+ } else if ([lastViewController isKindOfClass:[RNScreensNavigationController class]]) {
206
+ vc = [(RNScreensNavigationController *)lastViewController topViewController];
207
+ }
208
+ return [vc isKindOfClass:[RNSScreen class]] &&
209
+ [(RNSScreen *)vc findChildVCForConfigAndTrait:trait includingModals:includingModals] != nil;
210
+ }
211
+ return NO;
212
+ }
213
+
214
+ // same method as above, but directly for orientation
215
+ + (BOOL)shouldAskScreensForScreenOrientationInViewController:(UIViewController *)vc
216
+ {
217
+ return [RNSScreenWindowTraits shouldAskScreensForTrait:RNSWindowTraitOrientation
218
+ includingModals:YES
219
+ inViewController:vc];
220
+ }
221
+
193
222
  @end