知阅百微 见微知著

2026年5月17日

|

Do you think these two SwiftUI snippets are equivalent?

Text("Hello, world!")
    .kerning(10.0)
Text("Hello, world!")
    .kerning(4.0)
    .kerning(6.0)

The answer is: it depends.

It depends on the SwiftUI semantics your app is linked against. More specifically, it depends on whether Semantics.TextModifiersOverrideParentValues is enabled.

In SwiftUI, the kerning branch of Text.Modifier looks like this:

case let .kerning(value):
    if Semantics.TextModifiersOverrideParentValues.isEnabled {
        style.kerning = value
    } else {
        style.kerning = (style.kerning ?? .zero) + value
    }

That small branch reveals two different behaviors:

  1. When TextModifiersOverrideParentValues is enabled, the modifier currently being applied overwrites the current kerning value.
  2. When it is not enabled, the modifier currently being applied is added to the current kerning value.

There is one more detail that matters: Text applies its modifiers in reverse order during resolution.

for modifier in modifiers.reversed() {
    modifier.modify(style: &result.style, environment: environment)
}

So under the old semantics:

Text("Hello, world!")
    .kerning(4.0)
    .kerning(6.0)

resolves to a final kerning value of 10.0, which matches .kerning(10.0).

Under the new semantics, however, the modifiers are applied in reverse source order. SwiftUI first applies .kerning(6.0), then applies .kerning(4.0), so the final value is 4.0. It is no longer equivalent to .kerning(10.0).

The key is the semantic feature definition:

extension Semantics {
    package typealias TextModifiersOverrideParentValues = _SemanticFeature_v4
}

package struct _SemanticFeature_v4: SemanticFeature {
    package static let introduced = Semantics.v4
}

extension SemanticFeature {
    package static var requirement: SemanticRequirement { .linkedOnOrAfter }
}

Semantics.v4
corresponds to the SwiftUI 4 era: iOS 16.0, macOS 13.0, watchOS 9.0, and tvOS 16.0 SDK semantics. The linkedOnOrAfter requirement means this is based on the SDK version your app was built and linked with, not simply the OS version of the device running the app.

In other words, if your app is linked with a pre-iOS 16 SDK, TextModifiersOverrideParentValues is disabled and chained kerning modifiers accumulate. If your app is linked with an iOS 16 or newer SDK, the feature is enabled and chained kerning modifiers use override semantics.

This is why the same SwiftUI source code can produce a tiny but real typography difference after an SDK upgrade. Text.kerning looks like a normal modifier, but it also sits on SwiftUI's semantic compatibility surface for preserving old app behavior.

My practical takeaway: if you mean one final kerning value, write it once with a single .kerning(...). Do not rely on chained kerning modifiers accumulating, because that behavior is controlled by SDK-linked semantics.