跪拜 Guibai
← All articles
iOS · UIKit · Swift

iOS RTL Language Support: Resources, Traits, and the Read-Only App Direction

By songgeb ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Apps shipping Arabic or Hebrew cannot treat RTL as a single global flip: UIKit's direction model has separate read-only and override paths, and in-app switching leaves UIApplication.userInterfaceLayoutDirection untouched. Teams building an in-app language switcher need to know exactly which parts of the system route are reproducible and which are not.

Summary

Supporting Arabic, Hebrew, and other RTL languages on iOS touches three separate layers: which localized resources load, what layout direction the environment reports, and what each control says about its own content semantics. Apple's recommended path has been available since iOS 13: the per-app language setting in system Settings, where the system relaunches the app in the chosen language and state restoration returns the user to where they were. WWDC19 and WWDC24 both push apps toward that route. When a product demands language switching inside the app without a restart, the engineering fallback is to switch resources, override the layout direction trait at a common ancestor, and leave semanticContentAttribute alone. That arrangement respects the difference between a trait environment and app-level direction.

UIKit separates environment direction from content semantics. UITraitCollection.layoutDirection, read-only since iOS 10, describes the trait environment for a view or controller; semanticContentAttribute, writable since iOS 9, lets a control declare that it should follow the environment, keep media playback orientation, or keep absolute spatial meaning. The effective direction a view actually lays out with is UIView.effectiveUserInterfaceLayoutDirection. Both inputs matter: an alignment control in an RTL environment can still resolve to LTR when it uses .spatial. The catch is that none of these overrides writes back to UIApplication.userInterfaceLayoutDirection, the read-only app-level direction property from iOS 5. In-app switching therefore never fully replicates a system language change.

One known trap: setting UIView.appearance().semanticContentAttribute to force RTL can look correct but abuses the property's semantic meaning and has triggered incorrect safe areas on the iPhone Duo simulator. The sane division of labor is switch resources, override direction traits, preserve semantics, then refresh and verify effective direction on new scenes, modal interfaces, and attached windows.

Takeaways
— iOS 13 added per-app language selection; Apple's recommended flow is to send users to system Settings, let the system relaunch the app in the chosen language, and use state restoration to return them to their place.
— For instant in-app switching, the working division of labor is: switch localized resources, override layoutDirection at a common ancestor, and preserve semanticContentAttribute.
— UITraitCollection.layoutDirection is a read-only trait property introduced in iOS 10 and describes the direction of the current trait environment.
— semanticContentAttribute is a read-write UIView property introduced in iOS 9; its values are .unspecified, .playback, .spatial, .forceLeftToRight, and .forceRightToLeft.
— UIView.effectiveUserInterfaceLayoutDirection is the read-only result of combining environment direction with content semantics; custom layout and drawing should consult it instead of assuming the whole subtree shares one direction.
— On iOS 10–16, override layoutDirection with setOverrideTraitCollection(_:forChild:); on iOS 17+, use traitOverrides.layoutDirection on a scene, view, controller, or presentation controller.
— UIApplication.userInterfaceLayoutDirection is read-only and app-level; loading Arabic resources, overriding direction traits, or setting content semantics does not update it through public API.
— The system chooses an app's runtime language from user language preference plus supported localizations, falling back to the development language; the device's preferred language and region format do not determine the app's actual direction.
— Forcing RTL with UIView.appearance().semanticContentAttribute has produced incorrect safe areas on the iPhone Duo simulator, so the property should not be used as a global language switch.
Conclusions

The reason UIKit has both semanticContentAttribute and layoutDirection is that one environment must host controls with opposite directional needs; a global RTL flip that ignores per-control semantics will misplace things like text alignment buttons.

Apple's per-app language route is the only complete way to change direction because UIApplication.userInterfaceLayoutDirection is read-only; an in-app switcher is an engineering workaround that leaves system components and safe areas outside its control.

The iPhone Duo simulator failure is evidence that abusing semanticContentAttribute can corrupt layout beyond visual mirroring, so respecting its semantic contract is practical, not pedantic.

Concepts & terms
RTL (right-to-left)
A layout direction used by languages such as Arabic and Hebrew. UI elements flow from the right edge to the left edge, and controls often need mirroring to match reading order.
Trait
A single characteristic of the iOS interface environment, such as size class, display scale, or layout direction. UITraitCollection bundles these traits, and views and view controllers inherit them from their environment.
semanticContentAttribute
A UIView property that declares how a control's content should behave directionally: follow the environment, keep playback or spatial orientation, or force LTR/RTL.
effectiveUserInterfaceLayoutDirection
The read-only direction a view actually uses, computed by combining the environment's layoutDirection trait with the view's semanticContentAttribute. Custom layout and drawing should consult this value.
traitOverrides
An iOS 17 API for overriding traits such as layoutDirection on a UIWindowScene, UIView, UIViewController, or UIPresentationController, replacing the older setOverrideTraitCollection(_:forChild:) for most cases.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗