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

[iOS] Fabric: Fixes crash of dynamic color when light/dark mode changed #48496

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ struct Color {
Color(const ColorComponents& components);
Color(std::shared_ptr<void> uiColor);
int32_t getColor() const;
int32_t getUIColorHash() const;

std::shared_ptr<void> getUIColor() const {
return uiColor_;
}
Expand All @@ -49,6 +51,7 @@ struct Color {

private:
std::shared_ptr<void> uiColor_;
mutable std::size_t uiColorHashValue_;
};

namespace HostPlatformColor {
Expand Down Expand Up @@ -104,7 +107,7 @@ template <>
struct std::hash<facebook::react::Color> {
size_t operator()(const facebook::react::Color& color) const {
auto seed = size_t{0};
facebook::react::hash_combine(seed, color.getColor());
facebook::react::hash_combine(seed, color.getUIColorHash());
return seed;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javache No value in calling hash_combine again with seed 0.

Suggested change
auto seed = size_t{0};
facebook::react::hash_combine(seed, color.getColor());
facebook::react::hash_combine(seed, color.getUIColorHash());
return seed;
return color.getUIColorHash();

}
};
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,24 @@ int32_t ColorFromUIColor(UIColor *color)
((int)round((float)rgba[1] * ratio) & 0xff) << 8 | ((int)round((float)rgba[2] * ratio) & 0xff);
}

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

return 0;
}

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

UIColor *_Nullable UIColorFromComponentsColor(const facebook::react::ColorComponents &components)
{
if (components.colorSpace == ColorSpace::DisplayP3) {
Expand Down Expand Up @@ -142,6 +148,40 @@ int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)
return static_cast<float>(rgba[channelId]);
}

int32_t Color::getUIColorHash() const
{
if (!uiColorHashValue_) {
Copy link
Member

@javache javache Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this work?

UIColor *color = (UIColor *)unwrapManagedObject(uiColor_);
return [color hashValue];

Copy link
Contributor

@cipolleschi cipolleschi Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point here is: the UIColor does not change when the UITraitCollection changes.
It changes only when we resolve the uicolor against the trait collection.

The uiColor is the actual invariant here, we don't need to compute the hash resolving the uicolor against all the trait collections.

Copy link
Contributor Author

@zhongwuzw zhongwuzw Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javache @cipolleschi I don't know details about how UIColor wether implements hash correctly. But seems the hash value is different even two dynamic colors have the same color in all trait collections.

For example: Two methods like below, [self dynamicColor].hash is not equal to [self dynamicColor1].hash. Actually they are the same color. So it may have potential issues if we use the hash value to check equality?

+ (UIColor *)dynamicColor {
  return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
    if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
      return [UIColor whiteColor];
    } else {
      return [UIColor blackColor];
    }
  }];
}

+ (UIColor *)dynamicColor1 {
  return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
    if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
      return [UIColor whiteColor]; 
    } else {
      return [UIColor blackColor]; 
    }
  }];
}

PS. The crash disappeared if we return color.hash.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So UIColor hashValue is probably just falling back to NSObject, which means the value won't change over the objects lifetime, but also can't be reliably used to compare objects that are equal :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is that hashing is supposed to be cheap, and we use it implicitly in a number of different places. This makes the common case of a non-platform color significantly more expensive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hash is computed only once, when needed the first time, and then cached in the uiColorHashValue_. Isn't that enough? The tradeoff is that we recompute the same hash if we have two instances of the same color, but that should be ok, no?

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_;
}

} // namespace facebook::react

NS_ASSUME_NONNULL_END
Loading