banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

React Native Screens を使用してネイティブナビゲーションの内部原理を構築する

上回说到,我们已经初步了解 React Native Screens 的 ScreenStackItem 用法。这节我们探索一下这个组件的原生实现,然后写一个简单的 Navigation 类。

ScreenStackItem はどのように実装されているか#

ソースコードの位置#

React Native Screens のソースコード に入ると、このコンポーネントの位置が見つかります。

image

ここで Screen コンポーネントが使用されているのがわかります。さらに探します。

const Screen = React.forwardRef<View, ScreenProps>((props, ref) => {
  const ScreenWrapper = React.useContext(ScreenContext) || InnerScreen

  return <ScreenWrapper {...props} ref={ref} />
})

Screen コンポーネントは InnerScreen コンポーネントを使用しています。これは NativeView のさらなるラッピングです。

image

AnimatedScreen が使用されており、異なるシーンに対して異なる Native コンポーネントが使用されています。しかし最終的にはすべてが Screen 基本クラスです。

image

上記の AnimatedNativeScreenAnimatedNativeModalScreen は実際のネイティブコンポーネントです。それぞれ RNSScreenRNSModalScreen コンポーネントに対応しています。

iOS の例を挙げると、そのネイティブ実装を見つけます。RSScreen.h ヘッダーファイルを通じて、このコンポーネントはネイティブで ViewController であることがわかります。だからこそ、これらのライフサイクルイベントが存在します。例えば viewDidLoadviewWillAppearviewDidAppear などです。


@interface RNSScreen : UIViewController <RNSViewControllerDelegate>

- (instancetype)initWithView:(UIView *)view;
- (UIViewController *)findChildVCForConfigAndTrait:(RNSWindowTrait)trait includingModals:(BOOL)includingModals;
- (BOOL)hasNestedStack;
- (void)calculateAndNotifyHeaderHeightChangeIsModal:(BOOL)isModal;
- (void)notifyFinishTransitioning;
- (RNSScreenView *)screenView;
#ifdef RCT_NEW_ARCH_ENABLED
- (void)setViewToSnapshot;
- (CGFloat)calculateHeaderHeightIsModal:(BOOL)isModal;
#endif

@end

RNSModalScreenRNSScreen を継承しています。

@interface RNSModalScreen : RNSScreenView
@end

[!NOTE]

なぜ二つのコンポーネントを別々に定義する必要があるのか?

iOS では、モーダルと通常のビューには違いがあり、モーダルは Root Navigation Controller から離れて新しい Navigation Controller に入る必要があります。それは孤立したビューです。

したがって、ViewController だけでは不十分です。VC を管理する Navigation Controller が必要です。以前 ScreenStackItem を使用していたときに、ScreenStack でラップする必要があったのを覚えていますか?

それを見つけました。iOS では RNSScreenStack に対応し、これは Navigation Controller です。

@interface RNSNavigationController : UINavigationController <RNSViewControllerDelegate, UIGestureRecognizerDelegate>

@end

@interface RNSScreenStackView :
#ifdef RCT_NEW_ARCH_ENABLED
    RCTViewComponentView <RNSScreenContainerDelegate>
#else
    UIView <RNSScreenContainerDelegate, RCTInvalidating>
#endif

- (void)markChildUpdated;
- (void)didUpdateChildren;
- (void)startScreenTransition;
- (void)updateScreenTransition:(double)progress;
- (void)finishScreenTransition:(BOOL)canceled;

@property (nonatomic) BOOL customAnimation;
@property (nonatomic) BOOL disableSwipeBack;

#ifdef RCT_NEW_ARCH_ENABLED
#else
@property (nonatomic, copy) RCTDirectEventBlock onFinishTransitioning;
#endif // RCT_NEW_ARCH_ENABLED

@end

@interface RNSScreenStackManager : RCTViewManager <RCTInvalidating>

@end

ページ切り替えの実装原理#

現在、Navigation Controller と View Controller が揃ったので、React Native Screens はページ間の切り替えをどのように管理し、アニメーションを行うのでしょうか。

iOS では、Navigation Controller 上で命令的に pushViewController メソッドを呼び出すことでページ間の切り替えが実現できます。しかし、前回の記事のデモでは、ネイティブメソッドを呼び出すことなく、React 側のコンポーネントの状態が更新されただけでした。

覚えていますか?振り返ってみましょう。

const Demo = () => {
  const [otherRoutes, setOtherRoutes] = useState<
    {
      screenId: string
      route: ReactNode
    }[]
  >([])
  const cnt = useRef(0)
  const pushNewRoute = useEventCallback(() => {
    const screenId = `new-route-${cnt.current}`
    cnt.current++
    setOtherRoutes((prev) => [
      ...prev,
      {
        screenId,
        route: (
          <ScreenStackItem
            style={StyleSheet.absoluteFill}
            key={prev.length}
            screenId={screenId}
            onDismissed={() => {
              setOtherRoutes((prev) => prev.filter((route) => route.screenId !== screenId))
            }}
          >
            <View className="flex-1 items-center justify-center bg-white">
              <Text>新しいルート</Text>
            </View>
          </ScreenStackItem>
        ),
      },
    ])
  })
  return (
    <ScreenStack style={StyleSheet.absoluteFill}>
      <ScreenStackItem screenId="root" style={StyleSheet.absoluteFill}>
        <View className="flex-1 items-center justify-center bg-white">
          <Text>ルートルート</Text>
          <Button title="新しいルートをプッシュ" onPress={pushNewRoute} />
        </View>
      </ScreenStackItem>
      {otherRoutes.map((route) => route.route)}
    </ScreenStack>
  )
}

React Children の数に応じて ScreenStackItem コンポーネントを更新することで、ページ間の切り替えを実現しました。

このプロセスでは実際に何が起こっているのでしょうか?

実際にはすべて RNSScreenStack 内で処理されており、更新前後の children 配列を比較して、push するか pop するかを決定します。


- (void)didUpdateReactSubviews
{
  // 子供のレイアウトが設定されるまで待つ必要があります。この時点ではまだレイアウトが設定されていませんが、レイアウト呼び出しはすでに UI スレッドにキューイングされています。UI キューに更新呼び出しをキューイングすることで、更新がレイアウトの後に実行されることが保証されます。
  dispatch_async(dispatch_get_main_queue(), ^{
    [self maybeAddToParentAndUpdateContainer];
  });
}


- (void)maybeAddToParentAndUpdateContainer
{
  BOOL wasScreenMounted = _controller.parentViewController != nil;
  if (!self.window && !wasScreenMounted) {
    // スタックがマウントされるまで親コントローラーに追加するのを待ちます。
    // ウィンドウがアタッチされていないときに追加すると、一部のビュー遷移がブロックされることがあります(つまり、モーダル遷移)し、内部ビューコントローラーの状態が画面上のものと同期しなくなります。
    return;
  }
  [self updateContainer];
  if (!wasScreenMounted) {
    // スタックがまだ親 VC に追加されていない場合、2つのことを行います:
    // 1) updateContainer(上記のもの)を実行します - これは、VC がマウントされる前にプッシュビューコントローラーをインストールしたいからです。親に追加された後にそれを行うと、プッシュ更新操作が UIKit によってブロックされます。
    // 2) 親にナビゲーション VS を追加します - これは VC ライフサイクルイベントが適切にディスパッチされるために必要です。
    // 3) 再度 updateContainer を呼び出します - 今回はモーダルコントローラーを開くためにこれを行います。モーダルは(1)では開かれません。なぜなら、ナビゲーターが親に追加される必要があるからです。このケースは setModalViewControllers で優雅に処理され、いつでも開くことが再試行できます。
    [self reactAddControllerToClosestParent:_controller];
    [self updateContainer];
  }
}


- (void)updateContainer
{
  NSMutableArray<UIViewController *> *pushControllers = [NSMutableArray new];
  NSMutableArray<UIViewController *> *modalControllers = [NSMutableArray new];
  for (RNSScreenView *screen in _reactSubviews) {
    if (!screen.dismissed && screen.controller != nil && screen.activityState != RNSActivityStateInactive) {
      if (pushControllers.count == 0) {
        // リストの最初の画面は「プッシュコントローラー」として配置する必要があります
        [pushControllers addObject:screen.controller];
      } else {
        if (screen.stackPresentation == RNSScreenStackPresentationPush) {
          [pushControllers addObject:screen.controller];
        } else {
          [modalControllers addObject:screen.controller];
        }
      }
    }
  }

  [self setPushViewControllers:pushControllers];
  [self setModalViewControllers:modalControllers];
}

React コンポーネントの children が変更されると didUpdateReactSubviews メソッドが呼び出されます。そして最後に updateContainer メソッドに入ります。

updateContainer メソッドでは、RNSScreenViewstackPresentation 属性に基づいて、push するか pop するかを決定します。そして setPushViewControllers または setModalViewControllers メソッドを呼び出して、ネイティブビューを更新します。

setPushViewControllers メソッド内でネイティブの pushViewController メソッドが呼び出されます。

image

したがって、ネイティブでは、全体の Navigation Controller は無状態です。コントローラーの配列を保存していますが、前後を比較して遷移する必要のあるページを決定するだけです。

これにより、React で ScreenStackItem を管理しない場合、dismiss をトリガーした後にページが戻ったように見えますが、再度クリックして入ると、前回よりも +1 のページがプッシュされることになります。

image

この理由から、onDismissed イベント内で、ネイティブは React Native にどのページが dismiss されたかを伝えることができず、dismiss の数のみを提供することしかできません。

onDismissed?: (e: NativeSyntheticEvent<{ dismissCount: number }>) => void;

次の計画#

さて、この記事はここまでです。すでに少し長くなっています。

次回は、シンプルな Navigation クラスを実装しましょう。

この記事は Mix Space によって xLog に同期更新されました。元のリンクは https://innei.in/posts/tech/building-native-navigation-with-react-native-screens-part-2

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。