Skip to content

fix(input): derive heading bold from the base font on iOS - #586

Open
wildseansy wants to merge 1 commit into
software-mansion:mainfrom
wildseansy:sh/heading-bold-base-font
Open

fix(input): derive heading bold from the base font on iOS#586
wildseansy wants to merge 1 commit into
software-mansion:mainfrom
wildseansy:sh/heading-bold-base-font

Conversation

@wildseansy

Copy link
Copy Markdown
Contributor

Summary

A heading with a bold fontWeight (the shared default from headingDefaults) builds its font with UIFont systemFontOfSize:weight:, which discards the editor's base font family — an editor configured with a custom font gets system-font headings. Android does not have this problem: InputHeadingSpan folds the heading weight into the current typeface via Typeface.create(tp.typeface, BOLD).

Change

iOS now derives the bold face from the base font via a font-descriptor symbolic trait (the same approach fontForTraits: already uses for inline bold), falling back to the system font only when the family has no bold face. Non-bold explicit weights keep the existing system-font behavior.

Testing

  • Verified in a consuming app on an iOS simulator: with a custom base font, editor headings now render in that family's bold face instead of the system font; behavior matches Android.
  • clang-format clean on the touched file.

🤖 Generated with Claude Code

A heading with a bold fontWeight (the shared default) built its font with
UIFont systemFontOfSize:weight:, discarding the editor's base font family —
custom-font editors got system-font headings. Android already folds heading
weight into the current typeface (InputHeadingSpan), so iOS now derives the
bold face from the base font via a font-descriptor trait, falling back to
the system font only when the family has no bold face. Non-bold explicit
weights keep the previous system-font behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@eszlamczyk eszlamczyk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @wildseansy overall great catch! While this is a good thing to look for and the fix itself is decent I believe we can make this better by making it more robust than this solution. Please refer to the comments.

Also: If you could drop this comment in the middle I'd be grateful, I think this code is well self-explainatory

UIFont *font = weightString.length > 0 ? [UIFont systemFontOfSize:size weight:ENRMFontWeightFromString(weightString)]
: [_baseFont fontWithSize:size];
UIFont *font;
if (weightString.length > 0 && ENRMFontWeightFromString(weightString) >= UIFontWeightBold) {

@eszlamczyk eszlamczyk Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

While this is correct, I've checked that ENRMFontWeightFromString is incomplete if we use it for this comparasion; Because the mapping returns UIFontWeightRegular (0.0) for "800", "900", "heavy", and "black", those weights arrive at line 116 as 0.0, fail the >= 0.4 check, and skip the new bold-derivation branch — even though semantically they are bolder than bold.

If we want to use this function in this way we would require to fix the ENRMFontWeightFromString to mirror RN's own RCTFont table:

Weight strings UIFont.Weight constant
100, ultralight UIFontWeightUltraLight
200, thin UIFontWeightThin
300, light UIFontWeightLight
400, normal, regular UIFontWeightRegular
500, medium UIFontWeightMedium
600, semibold UIFontWeightSemibold
700, bold UIFontWeightBold
800, heavy UIFontWeightHeavy
900, black UIFontWeightBlack
---- (default) UIFontWeightRegular

font = descriptor ? [UIFont fontWithDescriptor:descriptor size:size]
: [UIFont systemFontOfSize:size weight:UIFontWeightBold];
} else if (weightString.length > 0) {
font = [UIFont systemFontOfSize:size weight:ENRMFontWeightFromString(weightString)];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The new bold branch correctly derives the heading font from the base font's family, but the else if below it still routes every sub-bold weight ('600', '500', '300', …) through [UIFont systemFontOfSize:weight:], which always produces the system font. So with a custom editor font (say Arial) and h1: { fontWeight: '600' }:

  • iOS editor → system font semibold (family lost)
  • Android editor → Arial (InputHeadingSpan never leaves the base typeface)
  • iOS preview → Arial semibold (RCTFont resolves the weight within the family)

This is the same divergence this PR fixes for bold, just for the remaining weights — and the family may well have matching faces (Arial Light/Medium/etc.) we could use.

Suggestion: instead of special-casing bold via the symbolic trait, derive every non-empty weight from the base font's descriptor using the weight trait, e.g.

UIFontDescriptor *descriptor = [_baseFont.fontDescriptor fontDescriptorByAddingAttributes:@{
  UIFontDescriptorTraitsAttribute : @{UIFontWeightTrait : @(weight)}
}];

The system picks the closest face in the family to the requested weight, which would collapse the bold branch and this one into a single path (keeping the system-font fallback for families where descriptor resolution fails).

// InputHeadingSpan, which folds heading weight into the current typeface —
// so a custom editor font keeps its family in headings. Fall back to the
// system font only when the family has no bold face.
UIFont *sized = [_baseFont fontWithSize:size];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nitpick - Two small things in this hunk:

  1. sized is a throwaway - symbolic traits don't depend on point size, so the descriptor can come straight from _baseFont, and fontWithDescriptor:size: already applies the size:
UIFontDescriptor *descriptor = [_baseFont.fontDescriptor
   fontDescriptorWithSymbolicTraits:_baseFont.fontDescriptor.symbolicTraits | UIFontDescriptorTraitBold];
  1. ENRMFontWeightFromString(weightString) runs twice (here and in the else if),
    and weightString.length > 0 is checked twice. Computing the weight once above
    the if simplifies both branches:
UIFontWeight weight = ENRMFontWeightFromString(weightString);
if (weightString.length > 0 && weight >= UIFontWeightBold) {
  ...
} else if (weightString.length > 0) {
  font = [UIFont systemFontOfSize:size weight:weight];
}

Non-blocking — and if we go with the UIFontWeightTrait approach from the other
comment, most of this restructures anyway, so feel free to fold it in there instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants