Skip to content

Commit 02e699b

Browse files
t0maborokkafar
andcommitted
feat(iOS, SplitView) Setup basic logic for custom ShadowNodes (#172)
## Description This PR is adding a basic setup for `RNSSplitViewScreenShadowNode` which is responsible for aligning (x, y) offsets in Shadow tree relatively to native layout metrics. This PR is also adding another controller `RNSSplitViewNavigationController`. According to the doc: https://developer.apple.com/documentation/uikit/uisplitviewcontroller#Child-view-controllers - we can wrap our components in custom NavigationController. In our case `RNSSplitViewNavigationController` is responsible for attaching `NSKeyValueObservation` for `frame`. It's necessary to track updates this way, as `layoutSubviews` won't be called when we're showing or hiding columns, because their width remains constant, no layout update is needed and only origin for our component (and its children relatively) is updated by some (x, y) vector. ## Changes - Updated SplitViewBaseApp - Added necessary logic for new ShadowNode in common directory - Updated SplitView native implementation to read and pass layout to ShadowNode ## Test code and steps to reproduce At the moment, TestSplitView is covering all cases, for testing purposes. Until we have some props supported, I'd recommend overriding SplitView props directly on the native side in `RNSSplitViewHostController` ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes Closes: #180 --------- Co-authored-by: Kacper Kafara <[email protected]>
1 parent 82ab88b commit 02e699b

14 files changed

+337
-10
lines changed

apps/src/tests/TestSplitView/SplitViewBaseApp.tsx

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
11
import React from 'react';
22
import { StyleSheet, Text, View } from 'react-native';
33
import { SplitViewHost, SplitViewScreen } from 'react-native-screens';
4+
import PressableWithFeedback from '../../shared/PressableWithFeedback';
45
import { Colors } from '../../shared/styling/Colors';
56

7+
const TestButton = ({ setButtonState }) => {
8+
return (
9+
<PressableWithFeedback
10+
onPress={() => setButtonState('Pressed')}
11+
onPressIn={() => setButtonState('Pressed In')}
12+
onPressOut={() => setButtonState('Pressed Out')}
13+
style={styles.button}>
14+
<Text style={styles.text}>Touch me</Text>
15+
</PressableWithFeedback>
16+
)
17+
}
18+
619
const SplitViewBaseApp = () => {
20+
const [buttonState, setButtonState] = React.useState('Initial');
21+
const [buttonState2, setButtonState2] = React.useState('Initial');
22+
const [buttonState3, setButtonState3] = React.useState('Initial');
23+
724
return (
825
<SplitViewHost>
926
<SplitViewScreen>
10-
<View style={[styles.container, { backgroundColor: Colors.RedDark100 }]} />
27+
<View style={[styles.container, { backgroundColor: Colors.RedDark100 }]}>
28+
<TestButton setButtonState={setButtonState} />
29+
{buttonState && (<Text style={styles.text}>Button State: {buttonState}</Text>)}
30+
</View>
1131
</SplitViewScreen>
1232
<SplitViewScreen>
13-
<View style={[styles.container, { backgroundColor: Colors.YellowDark100 }]} />
33+
<View style={[styles.container, { backgroundColor: Colors.YellowDark100 }]}>
34+
<TestButton setButtonState={setButtonState2} />
35+
{buttonState2 && (<Text style={styles.text}>Button State: {buttonState2}</Text>)}
36+
</View>
1437
</SplitViewScreen>
1538
<SplitViewScreen>
1639
<View style={[styles.container, { backgroundColor: Colors.White }]}>
17-
<Text style={styles.text}>
18-
Basic demo for splitView application
19-
</Text>
40+
<TestButton setButtonState={setButtonState3} />
41+
{buttonState3 && (<Text style={styles.text}>Button State: {buttonState3}</Text>)}
2042
</View>
2143
</SplitViewScreen>
2244
</SplitViewHost>
@@ -36,6 +58,13 @@ const styles = StyleSheet.create({
3658
},
3759
text: {
3860
fontSize: 24
61+
},
62+
button: {
63+
width: 120,
64+
height: 40,
65+
justifyContent: 'center',
66+
alignItems: 'center',
67+
backgroundColor: Colors.BlueDark100
3968
}
4069
})
4170

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#pragma once
2+
3+
#ifdef ANDROID
4+
#include <fbjni/fbjni.h>
5+
#endif // ANDROID
6+
#include <react/renderer/core/ConcreteComponentDescriptor.h>
7+
#include "RNSSplitViewScreenShadowNode.h"
8+
9+
namespace facebook::react {
10+
11+
class RNSSplitViewScreenComponentDescriptor final
12+
: public ConcreteComponentDescriptor<RNSSplitViewScreenShadowNode> {
13+
public:
14+
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
15+
16+
void adopt(ShadowNode &shadowNode) const override {
17+
react_native_assert(
18+
dynamic_cast<RNSSplitViewScreenShadowNode *>(&shadowNode));
19+
auto &splitViewScreenShadowNode =
20+
static_cast<RNSSplitViewScreenShadowNode &>(shadowNode);
21+
22+
react_native_assert(
23+
dynamic_cast<YogaLayoutableShadowNode *>(&splitViewScreenShadowNode));
24+
auto &layoutableShadowNode =
25+
static_cast<YogaLayoutableShadowNode &>(splitViewScreenShadowNode);
26+
27+
auto state = std::static_pointer_cast<
28+
const RNSSplitViewScreenShadowNode::ConcreteState>(
29+
shadowNode.getState());
30+
auto stateData = state->getData();
31+
32+
if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
33+
layoutableShadowNode.setSize(stateData.frameSize);
34+
}
35+
36+
ConcreteComponentDescriptor::adopt(shadowNode);
37+
}
38+
};
39+
40+
} // namespace facebook::react
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#include "RNSSplitViewScreenShadowNode.h"
2+
3+
namespace facebook::react {
4+
5+
extern const char RNSSplitViewScreenComponentName[] = "RNSSplitViewScreen";
6+
7+
Point RNSSplitViewScreenShadowNode::getContentOriginOffset(
8+
bool /*includeTransform*/) const {
9+
auto stateData = getStateData();
10+
return stateData.contentOffset;
11+
}
12+
13+
} // namespace facebook::react
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#pragma once
2+
3+
#include <jsi/jsi.h>
4+
#include <react/renderer/components/rnscreens/EventEmitters.h>
5+
#include <react/renderer/components/rnscreens/Props.h>
6+
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
7+
#include <react/renderer/core/LayoutContext.h>
8+
#include "RNSSplitViewScreenState.h"
9+
10+
namespace facebook::react {
11+
12+
JSI_EXPORT extern const char RNSSplitViewScreenComponentName[];
13+
14+
using ConcreteViewShadowNodeSuperType = ConcreteViewShadowNode<
15+
RNSSplitViewScreenComponentName,
16+
RNSSplitViewScreenProps,
17+
RNSSplitViewScreenEventEmitter,
18+
RNSSplitViewScreenState>;
19+
20+
class JSI_EXPORT RNSSplitViewScreenShadowNode final
21+
: public ConcreteViewShadowNodeSuperType {
22+
public:
23+
using ConcreteViewShadowNode::ConcreteViewShadowNode;
24+
using StateData = ConcreteViewShadowNode::ConcreteStateData;
25+
26+
#pragma mark - ShadowNode overrides
27+
28+
Point getContentOriginOffset(bool includeTransform) const override;
29+
};
30+
31+
} // namespace facebook::react
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#pragma once
2+
3+
#if defined(ANDROID)
4+
#include <folly/dynamic.h>
5+
#include <react/renderer/mapbuffer/MapBuffer.h>
6+
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
7+
#endif // ANDROID
8+
9+
namespace facebook::react {
10+
11+
class JSI_EXPORT RNSSplitViewScreenState final {
12+
public:
13+
using Shared = std::shared_ptr<const RNSSplitViewScreenState>;
14+
15+
RNSSplitViewScreenState(){};
16+
RNSSplitViewScreenState(Size frameSize_, Point contentOffset_)
17+
: frameSize(frameSize_), contentOffset(contentOffset_){};
18+
19+
const Size frameSize{};
20+
const Point contentOffset{};
21+
22+
#if defined(ANDROID)
23+
RNSSplitViewScreenState(
24+
const RNSSplitViewScreenState &previousState,
25+
folly::dynamic data) {}
26+
folly::dynamic getDynamic() const {
27+
return {};
28+
}
29+
#endif
30+
};
31+
32+
} // namespace facebook::react

ios/bridging/RNSReactBaseView.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_END
1414

1515
NS_ASSUME_NONNULL_BEGIN
1616

17-
@interface RNSReactBaseView
17+
@interface RNSReactBaseView : UIView
1818
@end
1919

2020
NS_ASSUME_NONNULL_END

ios/gamma/split-view/RNSSplitViewHostController.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,15 @@ public class RNSSplitViewHostController: UISplitViewController, ReactMountingTra
4141

4242
let currentSubviews =
4343
splitViewHostComponentView.reactSubviews() as! [RNSSplitViewScreenComponentView]
44-
let currentViewControllers = currentSubviews.map { $0.controller }
44+
let currentViewControllers = currentSubviews.map {
45+
RNSSplitViewNavigationController(rootViewController: $0.controller)
46+
}
4547

4648
viewControllers = currentViewControllers
49+
50+
for controller in currentViewControllers {
51+
controller.viewFrameOriginChangeObserver = self
52+
}
4753

4854
needsChildViewControllersUpdate = false
4955
}
@@ -60,3 +66,31 @@ public class RNSSplitViewHostController: UISplitViewController, ReactMountingTra
6066
updateChildViewControllersIfNeeded()
6167
}
6268
}
69+
70+
extension RNSSplitViewHostController {
71+
var splitViewScreenControllers: [RNSSplitViewScreenController] {
72+
return viewControllers.lazy.map { viewController in
73+
assert(
74+
viewController is RNSSplitViewNavigationController,
75+
"[RNScreens] Expected RNSSplitViewNavigationController but got \(type(of: viewController))")
76+
77+
let splitViewNavigationController = viewController as! RNSSplitViewNavigationController
78+
let splitViewNavigationControllerTopViewController = splitViewNavigationController
79+
.topViewController
80+
assert(
81+
splitViewNavigationControllerTopViewController is RNSSplitViewScreenController,
82+
"[RNScreens] Expected RNSSplitViewScreenController but got \(type(of: splitViewNavigationControllerTopViewController))"
83+
)
84+
85+
return splitViewNavigationControllerTopViewController as! RNSSplitViewScreenController
86+
}
87+
}
88+
}
89+
90+
extension RNSSplitViewHostController: RNSSplitViewNavigationControllerViewFrameObserver {
91+
func splitViewNavCtrlViewDidChangeFrameOrigin(_ splitViewNavCtrl: RNSSplitViewNavigationController) {
92+
for controller in self.splitViewScreenControllers {
93+
controller.columnPositioningDidChangeIn(splitViewController: self)
94+
}
95+
}
96+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
import UIKit
3+
4+
protocol RNSSplitViewNavigationControllerViewFrameObserver: AnyObject {
5+
func splitViewNavCtrlViewDidChangeFrameOrigin(_ splitViewNavCtrl: RNSSplitViewNavigationController)
6+
}
7+
8+
@objc
9+
public class RNSSplitViewNavigationController: UINavigationController {
10+
private var viewFrameObservation: NSKeyValueObservation?
11+
weak var viewFrameOriginChangeObserver: RNSSplitViewNavigationControllerViewFrameObserver?
12+
13+
override public func viewDidLoad() {
14+
super.viewDidLoad()
15+
16+
viewFrameObservation?.invalidate()
17+
viewFrameObservation = self.view.observe(\.frame, options: [.old, .new]) {
18+
[weak self] (view, change) in
19+
guard let oldFrame = change.oldValue, let newFrame = change.newValue else { return }
20+
21+
if oldFrame.origin != newFrame.origin {
22+
self?.onViewOriginChange()
23+
}
24+
}
25+
}
26+
27+
private func onViewOriginChange() {
28+
viewFrameOriginChangeObserver?.splitViewNavCtrlViewDidChangeFrameOrigin(self)
29+
}
30+
}

ios/gamma/split-view/RNSSplitViewScreenComponentView.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#import "RNSReactBaseView.h"
2+
#import "RNSSplitViewScreenShadowStateProxy.h"
23

34
NS_ASSUME_NONNULL_BEGIN
45

@@ -10,4 +11,12 @@ NS_ASSUME_NONNULL_BEGIN
1011

1112
@end
1213

14+
#pragma mark - ShadowTreeState
15+
16+
@interface RNSSplitViewScreenComponentView ()
17+
18+
- (nonnull RNSSplitViewScreenShadowStateProxy *)shadowStateProxy;
19+
20+
@end
21+
1322
NS_ASSUME_NONNULL_END

ios/gamma/split-view/RNSSplitViewScreenComponentView.mm

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
#import "RNSSplitViewScreenComponentView.h"
22
#import <React/RCTAssert.h>
3-
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
3+
#import <rnscreens/RNSSplitViewScreenComponentDescriptor.h>
44

55
#import "Swift-Bridging.h"
66

77
namespace react = facebook::react;
88

99
@implementation RNSSplitViewScreenComponentView {
1010
RNSSplitViewScreenController *_Nullable _controller;
11+
RNSSplitViewScreenShadowStateProxy *_Nonnull _shadowStateProxy;
1112
}
1213

1314
- (RNSSplitViewScreenController *)controller
@@ -31,6 +32,8 @@ - (instancetype)initWithFrame:(CGRect)frame
3132
- (void)initState
3233
{
3334
[self setupController];
35+
36+
_shadowStateProxy = [RNSSplitViewScreenShadowStateProxy new];
3437
}
3538

3639
- (void)setupController
@@ -39,6 +42,14 @@ - (void)setupController
3942
_controller.view = self;
4043
}
4144

45+
#pragma mark - ShadowTreeState
46+
47+
- (nonnull RNSSplitViewScreenShadowStateProxy *)shadowStateProxy
48+
{
49+
RCTAssert(_shadowStateProxy != nil, @"[RNScreens] Attempt to access uninitialized _shadowStateProxy");
50+
return _shadowStateProxy;
51+
}
52+
4253
#pragma mark - RCTViewComponentViewProtocol
4354

4455
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
@@ -53,6 +64,13 @@ + (BOOL)shouldBeRecycled
5364
return NO;
5465
}
5566

67+
- (void)updateState:(react::State::Shared const &)state oldState:(react::State::Shared const &)oldState
68+
{
69+
[super updateState:state oldState:oldState];
70+
71+
[_shadowStateProxy updateState:state oldState:oldState];
72+
}
73+
5674
@end
5775

5876
Class<RCTComponentViewProtocol> RNSSplitViewScreenCls(void)

0 commit comments

Comments
 (0)