Skip to content
Open
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
7 changes: 5 additions & 2 deletions .github/workflows/test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ jobs:
if: github.repository == 'react/react-native'
outputs:
any_code_change: ${{ steps.filter_exclusions.outputs.any_code_change == 'true' || github.event_name != 'pull_request' }}
should_test_android: ${{ steps.filter_exclusions.outputs.should_test_android == 'true' || github.event_name != 'pull_request' }}
should_test_ios: ${{ steps.filter_exclusions.outputs.should_test_ios == 'true' || github.event_name != 'pull_request' }}
should_test_android: ${{ steps.filter_exclusions.outputs.should_test_android == 'true' || steps.filter_inclusions.outputs.react_shared == 'true' || github.event_name != 'pull_request' }}
should_test_ios: ${{ steps.filter_exclusions.outputs.should_test_ios == 'true' || steps.filter_inclusions.outputs.react_shared == 'true' || github.event_name != 'pull_request' }}
debugger_shell: ${{ steps.filter_inclusions.outputs.debugger_shell }}
steps:
- name: Checkout
Expand Down Expand Up @@ -85,6 +85,9 @@ jobs:
id: filter_inclusions
with:
filters: |
# Shared Kotlin and its build configuration affect both platforms.
react_shared:
- 'packages/react-native/ReactShared/**'
debugger_shell:
- 'packages/debugger-shell/**'
- 'scripts/debugger-shell/**'
Expand Down
404 changes: 404 additions & 0 deletions .github/workflows/test-kmp.yml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ nexusPublishing {
tasks.register("clean", Delete::class.java) {
description = "Remove all the build files and intermediate build outputs"
dependsOn(gradle.includedBuild("gradle-plugin").task(":clean"))
dependsOn(gradle.includedBuild("react-native-shared").task(":clean"))
subprojects.forEach {
if (
it.project.plugins.hasPlugin("com.android.library") ||
Expand Down Expand Up @@ -103,6 +104,7 @@ tasks.register("clean", Delete::class.java) {
tasks.register("build") {
description = "Build and test all the React Native relevant projects."
dependsOn(gradle.includedBuild("gradle-plugin").task(":build"))
dependsOn(gradle.includedBuild("react-native-shared").task(":jvmTest"))
}

tasks.register("publishAllToMavenTempLocal") {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
import Foundation
import PackageDescription

if ProcessInfo.processInfo.environment["RCT_USE_KMP"] == "1" {
fatalError("RCT_USE_KMP=1 currently supports CocoaPods source builds only. Run RCT_USE_KMP=1 pod install in the iOS app; the SwiftPM prebuild does not include ReactNativeShared.")
}

let BUILD_FROM_SOURCE = false

// Removing the legacy TurboModule and component interop layers is opt-in while those
Expand Down
18 changes: 17 additions & 1 deletion packages/react-native/React-Core.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))
version = package['version']
kmp_enabled = ENV['RCT_USE_KMP'] == '1'
if kmp_enabled && ENV['RCT_USE_PREBUILT_RNCORE'] != '0'
raise 'RCT_USE_KMP=1 requires React Native core source builds. Use use_react_native! or set RCT_USE_PREBUILT_RNCORE=0.'
end

source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
Expand Down Expand Up @@ -54,13 +58,25 @@ Pod::Spec.new do |s|
s.compiler_flags = js_engine_flags()
s.header_dir = "React"
s.weak_framework = "JavaScriptCore"
s.pod_target_xcconfig = {
pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => header_search_paths,
"DEFINES_MODULE" => "YES",
"GCC_PREPROCESSOR_DEFINITIONS" => "RCT_METRO_PORT=${RCT_METRO_PORT}",
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"FRAMEWORK_SEARCH_PATHS" => frameworks_search_paths.join(" ")
}
if kmp_enabled
s.dependency 'React-KMP'
# React-Core is the common dependency of all Apple consumers. With dynamic
# pods it owns the Kotlin runtime once, even before Core calls a shared API.
# Static builds link the archive in the application via the post-install helper.
%w[iphoneos iphonesimulator].each do |sdk|
pod_target_xcconfig["GCC_PREPROCESSOR_DEFINITIONS[sdk=#{sdk}*]"] = '$(inherited) RCT_USE_KMP=1'
pod_target_xcconfig["FRAMEWORK_SEARCH_PATHS[sdk=#{sdk}*]"] = '$(inherited) "$(PODS_CONFIGURATION_BUILD_DIR)/ReactNativeSharedKMP"'
pod_target_xcconfig["OTHER_LDFLAGS[sdk=#{sdk}*]"] = '$(inherited) -ObjC -framework ReactNativeShared'
end
end
s.pod_target_xcconfig = pod_target_xcconfig
s.user_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Headers/Private/React-Core\""}
s.default_subspec = "Default"

Expand Down
56 changes: 49 additions & 7 deletions packages/react-native/React/Base/RCTMultipartStreamReader.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

#import "RCTMultipartStreamReader.h"
#import <QuartzCore/QuartzCore.h>
#import <TargetConditionals.h>

#if RCT_USE_KMP && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
#define RCT_MULTIPART_USE_KMP 1
#import <ReactNativeShared/ReactNativeShared.h>
#else
#define RCT_MULTIPART_USE_KMP 0
#endif

#define CRLF @"\r\n"

Expand All @@ -30,6 +38,12 @@ - (NSDictionary *)parseHeaders:(NSData *)data
{
NSMutableDictionary *headers = [NSMutableDictionary new];
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
#if RCT_MULTIPART_USE_KMP
for (RNSMultipartHeader *header in [RNSMultipartHeaders.shared parseText:text ?: @""]) {
NSString *value = [header.value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[headers setValue:value forKey:header.name];
}
#else
NSArray<NSString *> *lines = [text componentsSeparatedByString:CRLF];
for (NSString *line in lines) {
NSUInteger location = [line rangeOfString:@":"].location;
Expand All @@ -41,6 +55,7 @@ - (NSDictionary *)parseHeaders:(NSData *)data
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[headers setValue:value forKey:key];
}
#endif
return headers;
}

Expand Down Expand Up @@ -84,13 +99,17 @@ - (void)emitProgress:(NSDictionary *)headers
- (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback
progressCallback:(RCTMultipartProgressCallback)progressCallback
{
NSInteger chunkStart = 0;
NSInteger bytesSeen = 0;

NSData *delimiter =
[[NSString stringWithFormat:@"%@--%@%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
NSData *closeDelimiter =
[[NSString stringWithFormat:@"%@--%@--%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
#if RCT_MULTIPART_USE_KMP
RNSMultipartFraming *framing = [[RNSMultipartFraming alloc] initWithDelimiterLength:(int32_t)delimiter.length
closeDelimiterLength:(int32_t)closeDelimiter.length];
#else
NSInteger chunkStart = 0;
NSInteger bytesSeen = 0;
#endif
NSMutableData *content = [[NSMutableData alloc] initWithCapacity:1];
NSDictionary *currentHeaders = nil;
NSUInteger currentHeadersLength = 0;
Expand All @@ -101,9 +120,13 @@ - (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback
[_stream open];
while (true) {
BOOL isCloseDelimiter = NO;
// Search only a subset of chunk that we haven't seen before + few bytes
// to allow for the edge case when the delimiter is cut by read call
#if RCT_MULTIPART_USE_KMP
NSInteger searchStart = [framing searchStartBufferOffset:0];
NSInteger chunkStart = [framing partStartBufferOffset:0];
#else
// Preserve overlap when a delimiter is split between reads.
NSInteger searchStart = MAX(bytesSeen - (NSInteger)closeDelimiter.length, chunkStart);
#endif
NSRange remainingBufferRange = NSMakeRange(searchStart, content.length - searchStart);

// Check for delimiters.
Expand All @@ -113,6 +136,14 @@ - (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback
range = [content rangeOfData:closeDelimiter options:0 range:remainingBufferRange];
}

#if RCT_MULTIPART_USE_KMP
NSInteger index = range.location == NSNotFound ? -1 : (NSInteger)range.location;
RNSMultipartChunk *chunk = [framing nextChunkBufferLength:content.length
bufferOffset:0
delimiterIndex:isCloseDelimiter ? -1 : index
closeDelimiterIndex:isCloseDelimiter ? index : -1];
#endif

if (range.location == NSNotFound) {
if (currentHeaders == nil) {
// Check for the headers delimiter.
Expand All @@ -131,7 +162,9 @@ - (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback
callback:progressCallback];
}

#if !RCT_MULTIPART_USE_KMP
bytesSeen = content.length;
#endif
NSInteger bytesRead = [_stream read:buffer maxLength:bufferLen];
if (bytesRead <= 0 || _stream.streamError) {
return NO;
Expand All @@ -140,12 +173,19 @@ - (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback
continue;
}

#if RCT_MULTIPART_USE_KMP
NSInteger chunkEnd = chunk.end;
BOOL isPart = chunk.isPart;
isCloseDelimiter = chunk.isLast;
#else
NSInteger chunkEnd = range.location;
NSInteger length = chunkEnd - chunkStart;
BOOL isPart = chunkStart > 0;
bytesSeen = chunkEnd;
#endif
NSInteger length = chunkEnd - chunkStart;

// Ignore preamble
if (chunkStart > 0) {
if (isPart) {
NSData *chunk = [content subdataWithRange:NSMakeRange(chunkStart, length)];
[self emitProgress:currentHeaders
contentLength:chunk.length - currentHeadersLength
Expand All @@ -160,7 +200,9 @@ - (BOOL)readAllPartsWithCompletionCallback:(RCTMultipartCallback)callback
return YES;
}

#if !RCT_MULTIPART_USE_KMP
chunkStart = chunkEnd + delimiter.length;
#endif
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,44 @@

#import "RCTEnhancedScrollView.h"
#import <React/RCTUtils.h>
#import <TargetConditionals.h>
#import <react/utils/FloatComparison.h>

#if RCT_USE_KMP && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
#define RCT_SCROLL_SNAP_USE_KMP 1
#import <ReactNativeShared/ReactNativeShared.h>
#else
#define RCT_SCROLL_SNAP_USE_KMP 0
#endif

@interface RCTEnhancedScrollView () <UIScrollViewDelegate>
@end

@implementation RCTEnhancedScrollView {
__weak id<UIScrollViewDelegate> _publicDelegate;
BOOL _isSetContentOffsetDisabled;
#if RCT_SCROLL_SNAP_USE_KMP
RNSScrollSnapOffsets *_sharedSnapOffsets;
#endif
}

#if RCT_SCROLL_SNAP_USE_KMP
@synthesize snapToOffsets = _snapToOffsets;

- (void)setSnapToOffsets:(NSArray<NSNumber *> *)snapToOffsets
{
_snapToOffsets = [snapToOffsets copy];
_sharedSnapOffsets = nil;
if (_snapToOffsets.count > 0) {
// Convert once per property update, retaining the existing floatValue precision.
RNSKotlinDoubleArray *offsets = [RNSKotlinDoubleArray arrayWithSize:(int32_t)_snapToOffsets.count];
for (NSUInteger i = 0; i < _snapToOffsets.count; i++) {
[offsets setIndex:(int32_t)i value:_snapToOffsets[i].floatValue];
}
_sharedSnapOffsets = [[RNSScrollSnapOffsets alloc] initWithOffsets:offsets];
}
}
#endif

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
Expand Down Expand Up @@ -199,6 +228,15 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView

// Calculate the snap offsets adjacent to the initial offset target
CGFloat targetOffset = isHorizontal ? targetContentOffset->x : targetContentOffset->y;
#if RCT_SCROLL_SNAP_USE_KMP
targetOffset = [_sharedSnapOffsets resolveCurrentOffset:offsetAlongAxis
targetOffset:targetOffset
maximumOffset:maximumOffset
velocity:velocityAlongAxis
snapToStart:self.snapToStart
snapToEnd:self.snapToEnd]
.targetOffset;
#else
CGFloat smallerOffset = 0.0;
CGFloat largerOffset = maximumOffset;

Expand Down Expand Up @@ -250,6 +288,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView

// Make sure the new offset isn't out of bounds
targetOffset = MIN(MAX(0, targetOffset), maximumOffset);
#endif

// Set new targetContentOffset
if (isHorizontal) {
Expand Down
52 changes: 52 additions & 0 deletions packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@
#import "RCTGradientUtils.h"
#import <React/RCTAnimationUtils.h>
#import <React/RCTConversions.h>
#import <TargetConditionals.h>
#import <react/utils/FloatComparison.h>
#include <optional>
#import <vector>

#if RCT_USE_KMP && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
#define RCT_GRADIENT_USE_KMP 1
#import <ReactNativeShared/ReactNativeShared.h>
#else
#define RCT_GRADIENT_USE_KMP 0
#endif

using namespace facebook::react;

namespace {
Expand Down Expand Up @@ -156,6 +164,45 @@ CGSize calculateMultipliers(CGSize bounds)
return std::nullopt;
}

#if RCT_GRADIENT_USE_KMP
static std::vector<ProcessedColorStop> resolveSharedColorStops(
const std::vector<ColorStop> &colorStops,
CGFloat gradientLineLength)
{
NSMutableArray<RNSGradientStopInput *> *inputs = [NSMutableArray arrayWithCapacity:colorStops.size()];
for (const auto &stop : colorStops) {
auto position = resolveColorStopPosition(stop.position, gradientLineLength);
RNSDouble *boxedPosition = position.has_value() ? [RNSDouble numberWithDouble:position.value()] : nil;
auto input = [[RNSGradientStopInput alloc] initWithPosition:boxedPosition hasColor:static_cast<bool>(stop.color)];
[inputs addObject:input];
}

auto resolved = [RNSGradientStops.shared resolveStops:inputs epsilon:kDefaultEpsilon useDoublePrecision:YES];
std::vector<ProcessedColorStop> result;
result.reserve(resolved.count);
NSArray<NSNumber *> *inputRange = @[ @0.0, @1.0 ];
for (RNSResolvedGradientStop *stop in resolved) {
const auto &leftColor = colorStops[stop.leftColorIndex].color;
SharedColor color;
if (stop.leftColorIndex == stop.rightColorIndex) {
// Preserve the original native color, including dynamic UIColor behavior.
color = leftColor;
} else if (std::isfinite(stop.weight)) {
const auto &rightColor = colorStops[stop.rightColorIndex].color;
NSArray<UIColor *> *outputRange =
@[ RCTUIColorFromSharedColor(leftColor), RCTUIColorFromSharedColor(rightColor) ];
auto interpolatedColor = RCTInterpolateColorInRange(stop.weight, inputRange, outputRange);
auto alpha = (interpolatedColor >> 24) & 0xFF;
auto red = (interpolatedColor >> 16) & 0xFF;
auto green = (interpolatedColor >> 8) & 0xFF;
auto blue = interpolatedColor & 0xFF;
color = colorFromRGBA(red, green, blue, alpha);
}
result.push_back({.color = color, .position = stop.position});
}
return result;
}
#else
// Spec: https://drafts.csswg.org/css-images-4/#coloring-gradient-line (Refer transition hint section)
// Browsers add 9 intermediate color stops when a transition hint is present
// Algorithm is referred from Blink engine
Expand Down Expand Up @@ -261,12 +308,16 @@ CGSize calculateMultipliers(CGSize bounds)

return colorStops;
}
#endif

@implementation RCTGradientUtils
// https://drafts.csswg.org/css-images-4/#color-stop-fixup
+ (std::vector<ProcessedColorStop>)getFixedColorStops:(const std::vector<ColorStop> &)colorStops
gradientLineLength:(CGFloat)gradientLineLength
{
#if RCT_GRADIENT_USE_KMP
return resolveSharedColorStops(colorStops, gradientLineLength);
#else
if (colorStops.empty()) {
return {};
}
Expand Down Expand Up @@ -334,6 +385,7 @@ @implementation RCTGradientUtils
}
}
return processColorTransitionHints(fixedColorStops);
#endif
}

// CAGradientLayer linear gradient squishes the non-square gradient to square gradient.
Expand Down
Loading