Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev app crashes when switching color scheme while Text element with DynamicColorIOS is used #48493

Open
zetavg opened this issue Jan 4, 2025 · 7 comments
Labels
Component: Switch Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Newer Patch Available Platform: iOS iOS applications. Type: New Architecture Issues and PRs related to new architecture (Fabric/Turbo Modules)

Comments

@zetavg
Copy link

zetavg commented Jan 4, 2025

Description

The React Native app in development mode will crash when switching the color scheme (through system or the app) while there's a Text element using DynamicColorIOS. (see App.tsx in repro)

The error message shown in Xcode is:

Assertion failure: self.size() > 3 || std::none_of( self.begin(), self.end(), [&](auto const& k) { return self.key_eq()(key, k); })
Message: 
File: .../ReproducerApp/ios/Pods/Headers/Public/RCT-Folly/folly/container/detail/F14SetFallback.h
Line: 239
Function: findImpl

(It's a FOLLY_SAFE_DCHECK assertion written here)

Stack trace shows the assertion failure occurs while calling _cache.get here in RCTTextLayoutManager.mm:

- (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString
{
  auto sharedNSAttributedString = _cache.get(attributedString, [](AttributedString attributedString) {
    return wrapManagedObject(RCTNSAttributedStringFromAttributedString(attributedString));
  });

  return unwrapManagedObject(sharedNSAttributedString);
}

Fun fact is that due to the assertion statement (self.size() > 3 || ...), if we add more Text elements on the screen, the app won't crash.

This also doesn't affect release builds since that assertion seems to be dev-only.

Steps to reproduce

  1. Run the app through Xcode.
  2. Switch color scheme.
  3. Notice the crash.

React Native Version

0.76.0, 0.76.4, 0.76.5

Affected Platforms

Runtime - iOS

Areas

(not sure) Fabric - The New Renderer

Output of npx react-native info

System:
  OS: macOS 14.5
  CPU: (8) arm64 Apple M1
  Memory: 203.86 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 20.15.0
    path: ~/.asdf/installs/nodejs/20.15.0/bin/node
  Yarn:
    version: 1.22.22
    path: ~/.asdf/installs/nodejs/20.15.0/bin/yarn
  npm:
    version: 10.7.0
    path: ~/.asdf/plugins/nodejs/shims/npm
  Watchman:
    version: 2024.06.24.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.15.2
    path: /Users/z/.asdf/shims/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 24.2
      - iOS 18.2
      - macOS 15.2
      - tvOS 18.2
      - visionOS 2.2
      - watchOS 11.2
  Android SDK: Not Found
IDEs:
  Android Studio: 2024.2 AI-242.23339.11.2421.12550806
  Xcode:
    version: 16.2/16C5032a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.1
    path: /opt/homebrew/opt/openjdk/bin/javac
  Ruby:
    version: 3.0.2
    path: /Users/z/.asdf/shims/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 15.0.1
    wanted: 15.0.1
  react:
    installed: 18.3.1
    wanted: 18.3.1
  react-native:
    installed: 0.76.5
    wanted: 0.76.5
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true

Stacktrace or Logs

Assertion failure: self.size() > 3 || std::none_of( self.begin(), self.end(), [&](auto const& k) { return self.key_eq()(key, k); })
Message: 
File: .../ReproducerApp/ios/Pods/Headers/Public/RCT-Folly/folly/container/detail/F14SetFallback.h
Line: 239
Function: findImpl

Reproducer

https://github.com/zetavg/rn-crash-DynamicColorIOS-color-scheme-switch

Screenshots and Videos

@zetavg zetavg added Needs: Triage 🔍 Type: New Architecture Issues and PRs related to new architecture (Fabric/Turbo Modules) labels Jan 4, 2025
@react-native-bot
Copy link
Collaborator

Tip

Newer version available: You are on a supported minor version, but it looks like there's a newer patch available - 0.76.5. Please upgrade to the highest patch for your minor or latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If it does not repro, please let us know so we can close out this issue. This helps us ensure we are looking at issues that still exist in the most recent releases.

@react-native-bot
Copy link
Collaborator

Tip

Newer version available: You are on a supported minor version, but it looks like there's a newer patch available - undefined. Please upgrade to the highest patch for your minor or latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If it does not repro, please let us know so we can close out this issue. This helps us ensure we are looking at issues that still exist in the most recent releases.

@zetavg zetavg changed the title Dev app crashes when switching color scheme when Text element with DynamicColorIOS is used Dev app crashes when switching color scheme while Text element with DynamicColorIOS is used Jan 4, 2025
@zetavg
Copy link
Author

zetavg commented Jan 4, 2025

Not only DynamicColorIOS but also using PlatformColor will cause the crash.

More investgation:

@zhongwuzw
Copy link
Contributor

Hi , I have a PR to fix another similar crash #48496. Actually, I don't repro using your reproducer, can you please test wether my fix also works for your issue?

@zetavg
Copy link
Author

zetavg commented Jan 5, 2025

@zhongwuzw Thanks! I applied your changes manually to my reproducer Pods, but unfortunately, it still happens.

Screen.Shot.2025-01-05.10.01.28.PM.Xcode.mp4

If you have the time, can you help to confirm that when you run the reproducer, did it reach that FOLLY_SAFE_DCHECK there, and what's the value of self.size()?

When I was preparing the reproducer, I found reproducing challenging since many things will prevent it from happening, such as if the JS bundle has been reloaded or hot updated, it may not occur. If possible, I would like to improve the reproducer or instructions and make it easier to reproduce.

@zhongwuzw
Copy link
Contributor

zhongwuzw commented Jan 6, 2025

@zetavg Hi, I reproduced the crash successfully, but if I applied my changes, seems it not crash any more[ps, crash again if removed my changes]. Can you generate a patch after you applied my changes that I can investigate it ? :)

@zetavg
Copy link
Author

zetavg commented Jan 6, 2025

@zhongwuzw Many thanks, I applied your changes again, and it works now!

And I can confirm the same, if I change color.getUIColorHash() back to color.getColor() in std::hash<facebook::react::Color> inside HostPlatformColor.h, it crashes again.

IDK why it didn't work for the first time, maybe I missed something.

My files that made it work

(Look for the // PATCH: comments)

HostPlatformColor.h

/*
 * 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/graphics/ColorComponents.h>
#include <react/utils/hash_combine.h>
#include <cmath>

namespace facebook::react {

struct DynamicColor {
  int32_t lightColor = 0;
  int32_t darkColor = 0;
  int32_t highContrastLightColor = 0;
  int32_t highContrastDarkColor = 0;
};

struct Color {
  Color(int32_t color);
  Color(const DynamicColor& dynamicColor);
  Color(const ColorComponents& components);
  Color(std::shared_ptr<void> uiColor);
  int32_t getColor() const;
  // PATCH: START
  int32_t getUIColorHash() const;
  // PATCH: END

  std::shared_ptr<void> getUIColor() const {
    return uiColor_;
  }

  float getChannel(int channelId) const;

  ColorComponents getColorComponents() const {
    float ratio = 255;
    int32_t primitiveColor = getColor();
    return ColorComponents{
        (float)((primitiveColor >> 16) & 0xff) / ratio,
        (float)((primitiveColor >> 8) & 0xff) / ratio,
        (float)((primitiveColor >> 0) & 0xff) / ratio,
        (float)((primitiveColor >> 24) & 0xff) / ratio};
  }
  bool operator==(const Color& other) const;
  bool operator!=(const Color& other) const;
  operator int32_t() const {
    return getColor();
  }

 private:
  std::shared_ptr<void> uiColor_;
  // PATCH: START
  mutable std::size_t uiColorHashValue_;
  // PATCH: END
};

namespace HostPlatformColor {

#if defined(__clang__)
#define NO_DESTROY [[clang::no_destroy]]
#else
#define NO_DESTROY
#endif

NO_DESTROY static const facebook::react::Color UndefinedColor = Color(nullptr);
} // namespace HostPlatformColor

inline Color
hostPlatformColorFromRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
  float ratio = 255;
  const auto colorComponents = ColorComponents{
      .red = r / ratio,
      .green = g / ratio,
      .blue = b / ratio,
      .alpha = a / ratio,
  };
  return Color(colorComponents);
}

inline Color hostPlatformColorFromComponents(ColorComponents components) {
  return Color(components);
}

inline ColorComponents colorComponentsFromHostPlatformColor(Color color) {
  return color.getColorComponents();
}

inline float alphaFromHostPlatformColor(Color color) {
  return color.getChannel(3) * 255;
}

inline float redFromHostPlatformColor(Color color) {
  return color.getChannel(0) * 255;
}

inline float greenFromHostPlatformColor(Color color) {
  return color.getChannel(1) * 255;
}

inline float blueFromHostPlatformColor(Color color) {
  return color.getChannel(2) * 255;
}

} // namespace facebook::react

template <>
struct std::hash<facebook::react::Color> {
  size_t operator()(const facebook::react::Color& color) const {
    auto seed = size_t{0};
    // PATCH: START
    // facebook::react::hash_combine(seed, color.getColor());
    facebook::react::hash_combine(seed, color.getUIColorHash());
    // PATCH: END
    return seed;
  }
};

HostPlatformColor.mm

/*
 * 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 "HostPlatformColor.h"

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <react/utils/ManagedObjectWrapper.h>
#import <string>

using namespace facebook::react;

NS_ASSUME_NONNULL_BEGIN

namespace facebook::react {

namespace {
UIColor *_Nullable UIColorFromInt32(int32_t intColor)
{
  CGFloat a = CGFloat((intColor >> 24) & 0xFF) / 255.0;
  CGFloat r = CGFloat((intColor >> 16) & 0xFF) / 255.0;
  CGFloat g = CGFloat((intColor >> 8) & 0xFF) / 255.0;
  CGFloat b = CGFloat(intColor & 0xFF) / 255.0;
  return [UIColor colorWithRed:r green:g blue:b alpha:a];
}

UIColor *_Nullable UIColorFromDynamicColor(const facebook::react::DynamicColor &dynamicColor)
{
  int32_t light = dynamicColor.lightColor;
  int32_t dark = dynamicColor.darkColor;
  int32_t highContrastLight = dynamicColor.highContrastLightColor;
  int32_t highContrastDark = dynamicColor.highContrastDarkColor;

  UIColor *lightColor = UIColorFromInt32(light);
  UIColor *darkColor = UIColorFromInt32(dark);
  UIColor *highContrastLightColor = UIColorFromInt32(highContrastLight);
  UIColor *highContrastDarkColor = UIColorFromInt32(highContrastDark);

  if (lightColor != nil && darkColor != nil) {
    UIColor *color = [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(UITraitCollection *_Nonnull collection) {
      if (collection.userInterfaceStyle == UIUserInterfaceStyleDark) {
        if (collection.accessibilityContrast == UIAccessibilityContrastHigh && highContrastDarkColor != nil) {
          return highContrastDarkColor;
        } else {
          return darkColor;
        }
      } else {
        if (collection.accessibilityContrast == UIAccessibilityContrastHigh && highContrastLightColor != nil) {
          return highContrastLightColor;
        } else {
          return lightColor;
        }
      }
    }];
    return color;
  } else {
    return nil;
  }

  return nil;
}

int32_t ColorFromUIColor(UIColor *color)
{
  float ratio = 255;
  CGFloat rgba[4];
  [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
  return ((int32_t)round((float)rgba[3] * ratio) & 0xff) << 24 | ((int)round((float)rgba[0] * ratio) & 0xff) << 16 |
      ((int)round((float)rgba[1] * ratio) & 0xff) << 8 | ((int)round((float)rgba[2] * ratio) & 0xff);
}

// PATCH: START
// int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)
int32_t ColorFromUIColorForSpecificTraitCollection(
    const std::shared_ptr<void> &uiColor,
    UITraitCollection *traitCollection)
// PATCH: END
{
  UIColor *color = (UIColor *)unwrapManagedObject(uiColor);
  if (color) {
    // PATCH: START
    // UITraitCollection *currentTraitCollection = [UITraitCollection currentTraitCollection];
    // color = [color resolvedColorWithTraitCollection:currentTraitCollection];
    color = [color resolvedColorWithTraitCollection:traitCollection];
    // PATCH: END
    return ColorFromUIColor(color);
  }

  return 0;
}

// PATCH: START
int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)
{
  return ColorFromUIColorForSpecificTraitCollection(uiColor, [UITraitCollection currentTraitCollection]);
}
// PATCH: END

UIColor *_Nullable UIColorFromComponentsColor(const facebook::react::ColorComponents &components)
{
  if (components.colorSpace == ColorSpace::DisplayP3) {
    return [UIColor colorWithDisplayP3Red:components.red
                                    green:components.green
                                     blue:components.blue
                                    alpha:components.alpha];
  }
  return [UIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha];
}
} // anonymous namespace

Color::Color(int32_t color)
{
  uiColor_ = wrapManagedObject(UIColorFromInt32(color));
}

Color::Color(const DynamicColor &dynamicColor)
{
  uiColor_ = wrapManagedObject(UIColorFromDynamicColor(dynamicColor));
}

Color::Color(const ColorComponents &components)
{
  uiColor_ = wrapManagedObject(UIColorFromComponentsColor(components));
}

Color::Color(std::shared_ptr<void> uiColor)
{
  uiColor_ = std::move(uiColor);
}

bool Color::operator==(const Color &other) const
{
  return (!uiColor_ && !other.uiColor_) ||
      (uiColor_ && other.uiColor_ &&
       [unwrapManagedObject(getUIColor()) isEqual:unwrapManagedObject(other.getUIColor())]);
}

bool Color::operator!=(const Color &other) const
{
  return !(*this == other);
}

int32_t Color::getColor() const
{
  return ColorFromUIColor(uiColor_);
}

float Color::getChannel(int channelId) const
{
  CGFloat rgba[4];
  UIColor *color = (__bridge UIColor *)getUIColor().get();
  [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
  return static_cast<float>(rgba[channelId]);
}

// PATCH: START
int32_t Color::getUIColorHash() const
{
  if (!uiColorHashValue_) {
    return uiColorHashValue_;
  }

  static UITraitCollection *darkModeTraitCollection =
      [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
  auto darkColor = ColorFromUIColorForSpecificTraitCollection(uiColor_, darkModeTraitCollection);

  static UITraitCollection *lightModeTraitCollection =
      [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight];
  auto lightColor = ColorFromUIColorForSpecificTraitCollection(uiColor_, lightModeTraitCollection);

  static UITraitCollection *darkModeAccessibilityContrastTraitCollection =
      [UITraitCollection traitCollectionWithTraitsFromCollections:@[
        darkModeTraitCollection,
        [UITraitCollection traitCollectionWithAccessibilityContrast:UIAccessibilityContrastHigh]
      ]];
  auto darkAccessibilityContrastColor =
      ColorFromUIColorForSpecificTraitCollection(uiColor_, darkModeAccessibilityContrastTraitCollection);

  static UITraitCollection *lightModeAccessibilityContrastTraitCollection =
      [UITraitCollection traitCollectionWithTraitsFromCollections:@[
        lightModeTraitCollection,
        [UITraitCollection traitCollectionWithAccessibilityContrast:UIAccessibilityContrastHigh]
      ]];
  auto lightAccessibilityContrastColor =
      ColorFromUIColorForSpecificTraitCollection(uiColor_, lightModeAccessibilityContrastTraitCollection);
  uiColorHashValue_ = facebook::react::hash_combine(
      darkColor, lightColor, darkAccessibilityContrastColor, lightAccessibilityContrastColor);
  return uiColorHashValue_;
}
// PATCH: END

} // namespace facebook::react

NS_ASSUME_NONNULL_END

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Component: Switch Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Newer Patch Available Platform: iOS iOS applications. Type: New Architecture Issues and PRs related to new architecture (Fabric/Turbo Modules)
Projects
None yet
Development

No branches or pull requests

4 participants