From 3121a0e62b75953e012801eb92ec0fc6e5448eef Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Tue, 19 Aug 2025 06:53:44 -0700 Subject: [PATCH] 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 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 `` 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 --- packages/react-native/Package.swift | 3 +- packages/react-native/React/Base/RCTUtils.h | 1 + packages/react-native/React/Base/RCTUtils.mm | 10 +++++ .../Switch/RCTSwitchComponentView.mm | 2 +- .../React/React-RCTFabric.podspec | 1 + .../React-FabricComponents.podspec | 8 ++++ .../switch/AppleSwitchComponentDescriptor.h | 30 +++++++++++++ .../components/switch/AppleSwitchShadowNode.h | 42 +++++++++++++++++++ .../components/switch/IOSSwitchShadowNode.mm | 26 ++++++++++++ .../switch/MacOSSwitchShadowNode.mm | 32 ++++++++++++++ .../platform/ios/ReactCommon/RCTInstance.mm | 1 + 11 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/AppleSwitchComponentDescriptor.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/AppleSwitchShadowNode.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/IOSSwitchShadowNode.mm create mode 100644 packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/MacOSSwitchShadowNode.mm diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index c387085ea23..2c449f8fc3d 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -421,6 +421,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", @@ -433,7 +434,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 diff --git a/packages/react-native/React/Base/RCTUtils.h b/packages/react-native/React/Base/RCTUtils.h index 1f3c29b0062..9df03fb29d8 100644 --- a/packages/react-native/React/Base/RCTUtils.h +++ b/packages/react-native/React/Base/RCTUtils.h @@ -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); diff --git a/packages/react-native/React/Base/RCTUtils.mm b/packages/react-native/React/Base/RCTUtils.mm index dffe4dc800e..d2bf9ded0d1 100644 --- a/packages/react-native/React/Base/RCTUtils.mm +++ b/packages/react-native/React/Base/RCTUtils.mm @@ -426,6 +426,16 @@ CGSize RCTViewportSize(void) return window ? window.bounds.size : RCTScreenSize(); } +CGSize RCTSwitchSize(void) +{ + static CGSize rctSwitchSize; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + rctSwitchSize = [UISwitch new].intrinsicContentSize; + }); + return rctSwitchSize; +} + CGFloat RCTRoundPixelValue(CGFloat value) { CGFloat scale = RCTScreenScale(); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm index 552dd5def91..ba836bf5fd4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Switch/RCTSwitchComponentView.mm @@ -9,10 +9,10 @@ #import -#import #import #import #import +#import #import "RCTFabricComponentsPlugins.h" diff --git a/packages/react-native/React/React-RCTFabric.podspec b/packages/react-native/React/React-RCTFabric.podspec index 974f046fef1..5a453686011 100644 --- a/packages/react-native/React/React-RCTFabric.podspec +++ b/packages/react-native/React/React-RCTFabric.podspec @@ -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"]) diff --git a/packages/react-native/ReactCommon/React-FabricComponents.podspec b/packages/react-native/ReactCommon/React-FabricComponents.podspec index 3448123ee3f..690b4219f5c 100644 --- a/packages/react-native/ReactCommon/React-FabricComponents.podspec +++ b/packages/react-native/ReactCommon/React-FabricComponents.podspec @@ -127,6 +127,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" diff --git a/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/AppleSwitchComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/AppleSwitchComponentDescriptor.h new file mode 100644 index 00000000000..de4cef29aaa --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/AppleSwitchComponentDescriptor.h @@ -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 + +namespace facebook::react { + +/* + * Descriptor for component. + */ +class SwitchComponentDescriptor final + : public ConcreteComponentDescriptor { + public: + SwitchComponentDescriptor(const ComponentDescriptorParameters& parameters) + : ConcreteComponentDescriptor(parameters) {} + + void adopt(ShadowNode& shadowNode) const override { + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/AppleSwitchShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/AppleSwitchShadowNode.h new file mode 100644 index 00000000000..2cffdf139f5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/AppleSwitchShadowNode.h @@ -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 +#include +#include + +namespace facebook::react { + +extern const char AppleSwitchComponentName[]; + +/* + * `ShadowNode` for 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 diff --git a/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/IOSSwitchShadowNode.mm b/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/IOSSwitchShadowNode.mm new file mode 100644 index 00000000000..b38b0b0509c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/IOSSwitchShadowNode.mm @@ -0,0 +1,26 @@ +/* + * 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 +#import +#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(); + return {.width = uiSwitchSize.width, .height = uiSwitchSize.height}; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/MacOSSwitchShadowNode.mm b/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/MacOSSwitchShadowNode.mm new file mode 100644 index 00000000000..cd62f626ba6 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/switch/iosswitch/react/renderer/components/switch/MacOSSwitchShadowNode.mm @@ -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 +#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 diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm index 029c4cf7964..7d621ac6bf8 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm @@ -366,6 +366,7 @@ void RCTInstanceSetRuntimeDiagnosticFlags(NSString *flags) RCTScreenSize(); RCTScreenScale(); + RCTSwitchSize(); std::lock_guard lock(*mutex); *isReady = true;