Skip to content

Commit

Permalink
Merge pull request #4661 from wix/test/support-react-native-new-arch
Browse files Browse the repository at this point in the history
feat(iOS): support react native new architecture.
  • Loading branch information
asafkorem authored Jan 6, 2025
2 parents 2b4df68 + 0b9b92f commit ac67da0
Show file tree
Hide file tree
Showing 79 changed files with 1,627 additions and 1,838 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
- label: ":ios::react: RN .76 + iOS: Demo app"
- label: ":new::ios::react: RN .76 + iOS: Demo app"
command:
- "nvm install"
- "./scripts/demo-projects.ios.sh"
env:
REACT_NATIVE_VERSION: 0.76.3
RCT_NEW_ARCH_ENABLED: 1
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/artifacts*.tar.gz"
1 change: 1 addition & 0 deletions .buildkite/jobs/pipeline.ios_rn_76.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- "./scripts/ci.ios.sh"
env:
REACT_NATIVE_VERSION: 0.76.3
RCT_NEW_ARCH_ENABLED: 0
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/**/allure-report-*.html"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
- label: ":ios::detox: RN .75 + iOS: Tests app"
- label: ":new::ios::detox: RN .76 + New Arch + iOS: Tests app"
command:
- "nvm install"
- "./scripts/ci.ios.sh"
env:
REACT_NATIVE_VERSION: 0.75.4
REACT_NATIVE_VERSION: 0.76.3
RCT_NEW_ARCH_ENABLED: 1
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/**/allure-report-*.html"
Expand Down
4 changes: 2 additions & 2 deletions .buildkite/pipeline_common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

echo "steps:"

cat .buildkite/jobs/pipeline.ios_rn_76_new_arch.yml
cat .buildkite/jobs/pipeline.ios_rn_76.yml
cat .buildkite/jobs/pipeline.ios_rn_75.yml
cat .buildkite/jobs/pipeline.ios_rn_73.yml
cat .buildkite/jobs/pipeline.ios_demo_app_rn_76.yml
cat .buildkite/jobs/pipeline.ios_demo_app_rn_76_new_arch.yml
cat .buildkite/jobs/pipeline.android_rn_76.yml
cat .buildkite/jobs/pipeline.android_rn_75.yml
cat .buildkite/jobs/pipeline.android_rn_73.yml
Expand Down
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ fabric.properties
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
ios/build/
ios/DerivedData/
*/*/ios/build/
*/*/ios/DerivedData/

## Various settings
*.pbxuser
Expand All @@ -93,12 +93,12 @@ ios/DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
ios/xcuserdata/
*/*/ios/xcuserdata/

## Other
*.moved-aside
*.xcuserstate
ios/.xcode.env.local
*/*/ios/.xcode.env.local

## Obj-C/Swift specific
*.hmap
Expand Down
35 changes: 22 additions & 13 deletions detox/ios/Detox/Invocation/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,28 @@ class Element : NSObject {
return element
}

private func extractScrollView() -> UIScrollView {
if let view = self.view as? UIScrollView {
return view
}
else if let view = self.view as? WKWebView {
return view.scrollView
} else if ReactNativeSupport.isReactNativeApp && NSStringFromClass(type(of: view)) == "RCTScrollView" {
return (view.value(forKey: "scrollView") as! UIScrollView)
}

dtx_fatalError("View “\(self.view.dtx_shortDescription)” is not an instance of “UIScrollView”", viewDescription: debugAttributes)
}

private func extractScrollView() -> UIScrollView {
if let view = self.view as? UIScrollView {
return view
}

if let webView = self.view as? WKWebView {
return webView.scrollView
}

if ReactNativeSupport.isReactNativeApp {
let className = NSStringFromClass(type(of: view))
switch className {
case "RCTScrollView", "RCTScrollViewComponentView":
return (view.value(forKey: "scrollView") as! UIScrollView)
default:
break
}
}

dtx_fatalError("View “\(self.view.dtx_shortDescription)” is not an instance of “UIScrollView”", viewDescription: debugAttributes)
}

override var description: String {
return String(format: "MATCHER(%@)%@", predicate.description, index != nil ? " AT INDEX(\(index!))" : "")
}
Expand Down
56 changes: 40 additions & 16 deletions detox/ios/Detox/Invocation/Predicate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,32 @@ class Predicate : CustomStringConvertible, CustomDebugStringConvertible {
if ReactNativeSupport.isReactNativeApp == false {
return ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex)
} else {
//Will crash if RN app and neither class exists
let RCTTextViewClass : AnyClass = NSClassFromString("RCTText") ?? NSClassFromString("RCTTextView")!

let descendantPredicate = DescendantPredicate(predicate: AndCompoundPredicate(predicates: [
try KindOfPredicate(kind: Kind.type, modifiers: [], className: NSStringFromClass(RCTTextViewClass)),
ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex)
], modifiers: []), modifiers: [Modifier.not])
descendantPredicate.hidden = true

return AndCompoundPredicate(predicates: [
ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex),
descendantPredicate
], modifiers: [])
let possibleRNClasses: [AnyClass] = [
NSClassFromString("RCTParagraphComponentView"),
NSClassFromString("RCTText"),
NSClassFromString("RCTTextView")
].compactMap { $0 }

guard !possibleRNClasses.isEmpty else {
fatalError("No React Native text component classes found")
}

let typePredicates = possibleRNClasses.map { rnClass in
try! KindOfPredicate(kind: Kind.type, modifiers: [], className: NSStringFromClass(rnClass))
}

let descendantPredicate = DescendantPredicate(predicate: AndCompoundPredicate(predicates: [
OrCompoundPredicate(predicates: typePredicates, modifiers: []),
ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex)
], modifiers: []), modifiers: [Modifier.not])
descendantPredicate.hidden = true

return AndCompoundPredicate(predicates: [
ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex),
descendantPredicate
], modifiers: [])
}

case Kind.text:
let text = dictionaryRepresentation[Keys.value] as! String
var orPredicates = [
Expand All @@ -88,9 +100,21 @@ class Predicate : CustomStringConvertible, CustomDebugStringConvertible {
]

if ReactNativeSupport.isReactNativeApp == true {
//Will crash if RN app and neither class exists
let RCTTextViewClass : AnyClass = NSClassFromString("RCTText") ?? NSClassFromString("RCTTextView")!
orPredicates.append(try KindOfPredicate(kind: Kind.type, modifiers: [], className: NSStringFromClass(RCTTextViewClass)))
let possibleRNClasses: [AnyClass] = [
NSClassFromString("RCTParagraphComponentView"),
NSClassFromString("RCTText"),
NSClassFromString("RCTTextView")
].compactMap { $0 }

guard !possibleRNClasses.isEmpty else {
fatalError("No React Native text component classes found")
}

possibleRNClasses.forEach { rnClass in
let predicate = try! KindOfPredicate(kind: Kind.type, modifiers: [], className: NSStringFromClass(rnClass))

orPredicates.append(predicate)
}
}

let orCompoundPredicate = OrCompoundPredicate(predicates: orPredicates, modifiers: [])
Expand Down
60 changes: 38 additions & 22 deletions detox/ios/Detox/Utilities/NSObject+DontCrash.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,48 @@ @implementation NSObject (DontCrash)

- (id)_dtx_text
{
if([self respondsToSelector:@selector(text)])
{
return [(UITextView*)self text];
}

static Class RCTTextView;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTTextView = NSClassFromString(@"RCTTextView");
});
if(RCTTextView != nil && [self isKindOfClass:RCTTextView])
{
return [(NSTextStorage*)[self valueForKey:@"textStorage"] string];
}

return nil;
if([self respondsToSelector:@selector(text)])
{
return [(UITextView*)self text];
}

static Class RCTParagraphComponentViewClass;
static Class RCTTextClass;
static Class RCTTextViewClass;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTParagraphComponentViewClass = NSClassFromString(@"RCTParagraphComponentView");
RCTTextClass = NSClassFromString(@"RCTText");
RCTTextViewClass = NSClassFromString(@"RCTTextView");
});

if(RCTParagraphComponentViewClass != nil && [self isKindOfClass:RCTParagraphComponentViewClass])
{
NSAttributedString *attributedText = [self valueForKey:@"attributedText"];
return [attributedText string];
}

if(RCTTextClass != nil && [self isKindOfClass:RCTTextClass])
{
return [(NSTextStorage*)[self valueForKey:@"textStorage"] string];
}

if(RCTTextViewClass != nil && [self isKindOfClass:RCTTextViewClass])
{
return [(NSTextStorage*)[self valueForKey:@"textStorage"] string];
}

return nil;
}

- (id)_dtx_placeholder
{
if([self respondsToSelector:@selector(placeholder)])
{
return [(UITextField*)self placeholder];
}
return nil;
if([self respondsToSelector:@selector(placeholder)])
{
return [(UITextField*)self placeholder];
}

return nil;
}

@end
50 changes: 30 additions & 20 deletions detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,36 @@ + (BOOL)isReactNativeApp

+ (void)reloadApp
{
if([DTXReactNativeSupport hasReactNative] == NO)
{
return;
}

id<RN_RCTBridge> bridge = [NSClassFromString(@"RCTBridge") valueForKey:@"currentBridge"];

SEL reqRelSel = NSSelectorFromString(@"requestReload");
if([bridge respondsToSelector:reqRelSel])
{
//Call RN public API to request reload.
[bridge requestReload];
}
else
{
//Legacy call to reload RN.
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification
object:nil
userInfo:nil];
}
// Early return if React Native is not present
if (![DTXReactNativeSupport hasReactNative]) {
return;
}

// Try legacy reload approach (without new arch)
id<RN_RCTBridge> bridge = [NSClassFromString(@"RCTBridge") valueForKey:@"currentBridge"];
if ([bridge respondsToSelector:@selector(requestReload)]) {
[bridge requestReload];
return;
}

// Try New Architecture reload approach
NSObject<UIApplicationDelegate> *appDelegate = UIApplication.sharedApplication.delegate;
NSObject *rootViewFactory = [appDelegate valueForKey:@"rootViewFactory"];
NSObject *reactHost = [rootViewFactory valueForKey:@"reactHost"];

SEL reloadCommand = NSSelectorFromString(@"didReceiveReloadCommand");
if (reactHost && [reactHost respondsToSelector:reloadCommand]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[reactHost performSelector:reloadCommand];
#pragma clang diagnostic pop
return;
}

// Fallback to legacy^2 reload approach
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification
object:nil
userInfo:nil];
}

+ (void)waitForReactNativeLoadWithCompletionHandler:(void (^)(void))handler
Expand Down
2 changes: 1 addition & 1 deletion detox/ios/DetoxSync
Loading

0 comments on commit ac67da0

Please sign in to comment.