[LOCAL] Fix Switch layout for iOS 26 (#53389)

* Fix Switch layout with iOS26 (#53247)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53247

Apple changed the sizes of the UISwitchComponent and now, if you build an iOs app using the <Switch> component, the layout of the app will be broken because of wrong layout measurements.
This has been reported also by [https://github.com/facebook/react-native/issues/52823](https://github.com/facebook/react-native/issues/52823).

The `<Switch>` component was using hardcoded values for its size.
This change fixes the problem by:
- Using codegen for interface only
- Implementing a custom Sadow Node to ask the platform for the Switch measurements
- Updating the JS layout to wrap the size around the native component.

[iOS][Fixed] - Fix Switch layout to work with iOS26

Pull Request resolved: https://github.com/facebook/react-native/pull/53067

Test Plan:
Tested locally with RNTester.

| iOS Version | Before | After |
| --- | --- | --- |
| < iOS 26 | https://github.com/user-attachments/assets/91d73ea3-30ba-4a5c-948e-ea5c63aa7c6d | https://github.com/user-attachments/assets/76061bc8-0f14-412a-a8fb-d1c3951772e6 |
| >= iOS 26 | https://github.com/user-attachments/assets/1abc477f-bc0a-4762-938e-98814fb2a054 | https://github.com/user-attachments/assets/77e562e1-b803-46ac-9cf6-102f062a1cd4 |

Rollback Plan:

Reviewed By: sammy-SC

Differential Revision: D79653120

Pulled By: cipolleschi

fbshipit-source-id: d99b353b7b7b5496b148779de4abe3e57dd38156

* feat: update js layout

* fix crash for feature flag
This commit is contained in:
Riccardo Cipolleschi
2025-08-27 12:27:24 +01:00
committed by GitHub
parent 6da5e66522
commit dffbfe6fd9
13 changed files with 160 additions and 3 deletions
@@ -264,7 +264,7 @@ const Switch: component(
disabled,
onTintColor: trackColorForTrue,
style: StyleSheet.compose(
{height: 31, width: 51},
{alignSelf: 'flex-start' as const},
StyleSheet.compose(
style,
ios_backgroundColor == null
+2 -1
View File
@@ -424,6 +424,7 @@ let reactFabricComponents = RNTarget(
"components/view/platform/android",
"components/view/platform/windows",
"components/view/platform/macos",
"components/switch/iosswitch/react/renderer/components/switch/MacOSSwitchShadowNode.mm",
"components/textinput/platform/android",
"components/text/platform/android",
"components/textinput/platform/macos",
@@ -436,7 +437,7 @@ let reactFabricComponents = RNTarget(
"conponents/rncore", // this was the old folder where RN Core Components were generated. If you ran codegen in the past, you might have some files in it that might make the build fail.
],
dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging],
sources: ["components/inputaccessory", "components/modal", "components/safeareaview", "components/text", "components/text/platform/cxx", "components/textinput", "components/textinput/platform/ios/", "components/unimplementedview", "components/virtualview", "textlayoutmanager", "textlayoutmanager/platform/ios"]
sources: ["components/inputaccessory", "components/modal", "components/safeareaview", "components/text", "components/text/platform/cxx", "components/textinput", "components/textinput/platform/ios/", "components/unimplementedview", "components/virtualview", "textlayoutmanager", "textlayoutmanager/platform/ios", "components/switch/iosswitch"]
)
/// React-FabricImage.podspec
@@ -54,6 +54,7 @@ RCT_EXTERN CGFloat RCTScreenScale(void);
RCT_EXTERN CGFloat RCTFontSizeMultiplier(void);
RCT_EXTERN CGSize RCTScreenSize(void);
RCT_EXTERN CGSize RCTViewportSize(void);
RCT_EXTERN CGSize RCTSwitchSize(void);
// Round float coordinates to nearest whole screen pixel (not point)
RCT_EXTERN CGFloat RCTRoundPixelValue(CGFloat value);
@@ -426,6 +426,18 @@ CGSize RCTViewportSize(void)
return window ? window.bounds.size : RCTScreenSize();
}
CGSize RCTSwitchSize(void)
{
static CGSize rctSwitchSize;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTUnsafeExecuteOnMainQueueSync(^{
rctSwitchSize = [UISwitch new].intrinsicContentSize;
});
});
return rctSwitchSize;
}
CGFloat RCTRoundPixelValue(CGFloat value)
{
CGFloat scale = RCTScreenScale();
@@ -9,10 +9,10 @@
#import <React/RCTConversions.h>
#import <react/renderer/components/FBReactNativeSpec/ComponentDescriptors.h>
#import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/Props.h>
#import <react/renderer/components/FBReactNativeSpec/RCTComponentViewHelpers.h>
#import <react/renderer/components/switch/AppleSwitchComponentDescriptor.h>
#import "RCTFabricComponentsPlugins.h"
@@ -75,6 +75,7 @@ Pod::Spec.new do |s|
"react/renderer/components/scrollview/platform/cxx",
"react/renderer/components/text/platform/cxx",
"react/renderer/components/textinput/platform/ios",
"react/renderer/components/switch/iosswitch",
]);
add_dependency(s, "React-graphics", :additional_framework_paths => ["react/renderer/graphics/platform/ios"])
@@ -124,6 +124,14 @@ Pod::Spec.new do |s|
sss.header_dir = "react/renderer/components/iostextinput"
end
ss.subspec "switch" do |sss|
sss.source_files = podspec_sources(
["react/renderer/components/switch/iosswitch/**/*.{m,mm,cpp,h}"],
["react/renderer/components/switch/iosswitch/**/*.h"])
sss.exclude_files = "react/renderer/components/switch/iosswitch/**/MacOS*.{m,mm,cpp,h}"
sss.header_dir = "react/renderer/components/switch/"
end
ss.subspec "textinput" do |sss|
sss.source_files = podspec_sources("react/renderer/components/textinput/*.{m,mm,cpp,h}", "react/renderer/components/textinput/**/*.h")
sss.header_dir = "react/renderer/components/textinput"
@@ -0,0 +1,30 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include "AppleSwitchShadowNode.h"
#include <react/renderer/core/ConcreteComponentDescriptor.h>
namespace facebook::react {
/*
* Descriptor for <Switch> component.
*/
class SwitchComponentDescriptor final
: public ConcreteComponentDescriptor<SwitchShadowNode> {
public:
SwitchComponentDescriptor(const ComponentDescriptorParameters& parameters)
: ConcreteComponentDescriptor(parameters) {}
void adopt(ShadowNode& shadowNode) const override {
ConcreteComponentDescriptor::adopt(shadowNode);
}
};
} // namespace facebook::react
@@ -0,0 +1,42 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#include <react/renderer/components/FBReactNativeSpec/Props.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
namespace facebook::react {
extern const char AppleSwitchComponentName[];
/*
* `ShadowNode` for <IOSSwitch> component.
*/
class SwitchShadowNode final : public ConcreteViewShadowNode<
AppleSwitchComponentName,
SwitchProps,
SwitchEventEmitter> {
public:
using ConcreteViewShadowNode::ConcreteViewShadowNode;
static ShadowNodeTraits BaseTraits() {
auto traits = ConcreteViewShadowNode::BaseTraits();
traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
return traits;
}
#pragma mark - LayoutableShadowNode
Size measureContent(
const LayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const override;
};
} // namespace facebook::react
@@ -0,0 +1,28 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTUtils.h>
#import <UIKit/UIKit.h>
#include "AppleSwitchShadowNode.h"
namespace facebook::react {
extern const char AppleSwitchComponentName[] = "Switch";
#pragma mark - LayoutableShadowNode
Size SwitchShadowNode::measureContent(
const LayoutContext & /*layoutContext*/,
const LayoutConstraints & /*layoutConstraints*/) const
{
CGSize uiSwitchSize = RCTSwitchSize();
// Apple has some error when returning the width of the component and it doesn't
// account for the borders.
return {.width = uiSwitchSize.width + 2, .height = uiSwitchSize.height};
}
} // namespace facebook::react
@@ -0,0 +1,32 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <AppKit/AppKit.h>
#include "AppleSwitchShadowNode.h"
namespace facebook::react {
extern const char AppleSwitchComponentName[] = "Switch";
#pragma mark - LayoutableShadowNode
Size SwitchShadowNode::measureContent(
const LayoutContext & /*layoutContext*/,
const LayoutConstraints & /*layoutConstraints*/) const
{
static CGSize nsSwitchSize = CGSizeZero;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
nsSwitchSize = [NSSwitch new].intrinsicContentSize;
});
});
return {.width = nsSwitchSize.width, .height = nsSwitchSize.height};
}
} // namespace facebook::react
@@ -366,6 +366,7 @@ void RCTInstanceSetRuntimeDiagnosticFlags(NSString *flags)
RCTScreenSize();
RCTScreenScale();
RCTSwitchSize();
std::lock_guard<std::mutex> lock(*mutex);
*isReady = true;
@@ -58,4 +58,5 @@ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
export default (codegenNativeComponent<SwitchNativeProps>('Switch', {
paperComponentName: 'RCTSwitch',
excludedPlatforms: ['android'],
interfaceOnly: true,
}): ComponentType);