1
头图
Image via: https://picography.co/ocean-splash-wave/
Author of this article: JDMin

opening

The Arabic script is spoken by approximately 660 million people in more than 22 countries today, making it the third most written language in the world after Latin and Chinese. With the gradual deepening of the overseas expansion of the business, the adaptation of the App to Arabic has been put on the agenda. The most obvious difference from Chinese and English, which we have more contact with, is that Arabic is written and used from right to left. Although iOS itself has a lot of processing for this RTL (Right-To-Left) language, but when we develop, we need to pay attention to using the correct specification to avoid mistakes. At the same time, each business and app has its own special design and other characteristics, which will also bring many new problems to be solved. The following introduces the problems and solutions encountered in the recent project in adapting the RTL language.

Before introducing specific problem scenarios, we first introduce some main features related to RTL language and engineering adaptation:

  • text. The most obvious difference from LTR (Left-To-Right) languages such as Chinese and English is that RTL languages are written and read from right to left.
  • icon. Icons should be handled flexibly for each specific icon. Considering that the copywriting and usage habits of the RTL language are from right to left, many icons with clear directions need to be changed downward (for example, such as the commonly used arrow icons). As for other general icons, they remain unchanged in the UI.
  • number. Our daily contact is mostly Arabic numerals, or Western Arabic numerals. In contrast, Eastern Arabic numerals. Different Arab countries use different Arabic numerals. For example, Morocco and Algeria commonly use Western Arabic numerals, while Iran, Afghanistan, Pakistan and other countries use Eastern Arabic numerals. In countries such as Egypt and Saudi Arabia, both forms of Arabic numerals are used. Before development, it is necessary to confirm which Arabic numerals are used in the areas we need to provide services, and handle and display them correctly.

Project status and characteristics

In fact, the iOS system has done a lot of processing on the RTL language, and provided many APIs for the upper layer to do business adaptation. However, before discussing these specific issues, we need to understand the current status and characteristics of the current project, and choose the most appropriate solution accordingly. In summary, the current characteristics of the project:

  1. Large in size. The project has developed to this day, and the amount of code has been relatively large. For relatively large changes, it is necessary to consider the cost of the transformation, and whether there will be any hidden dangers for future business expansion.
  2. The project has a lot of layout code, especially the layout code of the relatively early business, which is handled by manual layout using frame layout instead of AutoLayout.
  3. The App supports the user to set the language in the app. When the app is launched for the first time, the user's system language is selected as the default language, and the user can switch languages within the app.

As for the specific impact of layout method and in-app language setting on RTL adaptation, we will introduce in detail later.

problems encountered

Switch language within the app

When we set the language to an RTL language such as Arabic in the system settings, the system will automatically change the layout of the App to the RTL layout. The first problem will be encountered here. We can set the language in the App. When the layout of the application setting language and the system language is inconsistent (for example, the application is set to Arabic, and the system is set to English), we want to use the in-app language. prevail. At this time, the default processing of the system can no longer be used. After iOS9, iOS has opened a new ---ad28054a2c09cb6e863a634452297890--- for UIView property ,

 @property (nonatomic) UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0));

Through semanticContentAttribute it is possible to customize whether a view is flipped under RTL and RTL layout by the developer. We need to set the View in the App semanticContentAttribute according to the in-app language to avoid using the system's default judgment. It is obviously too troublesome to make changes to each View here.
Our approach is to deal with the global by setting UIView.appearance().semanticContentAttribute when the language is set.

 if isRTLLanguage {
    UIView.appearance().semanticContentAttribute = .forceRightToLeft
} else {
    UIView.appearance().semanticContentAttribute = .forceLeftToRight
}

For Views with special adaptation scenarios (not flipped in RTL mode), you can set the relevant UI element instances at the top level of the business by semanticContentAttribute .

layout

There are generally two types of layouts that we commonly use today. One is to use AutoLayout, and the other is manual layout of frame layout. We introduce them one by one.

For AutoLayout, including the commonly used three-party packaging libraries Masonry, SnapKit, etc., have a relatively good compatibility with RTL. In RTL and LTR, the actual directions corresponding to Left and Right are the same, and the layout will not change. Therefore, when we set constraints, we need to use the general sense of Leading and Trailing to replace the Left and Right that were commonly used in the past. Leading is the front constraint, corresponding to Left in LTR and Right in RTL. Trailing is a tail constraint, corresponding to right in LTR and Left in RTL. Use Leading and Trailing to set constraints, View will automatically adjust the layout according to its own semanticContentAttribute specifically LTR or RTL.

Since a large amount of layout code in our business is manually laid out using frame layout, it is unrealistic to switch all to AutoLayout. Especially for scenarios with complex UI elements and layout logic, rewriting the layout is difficult and time-consuming. Therefore, it is necessary to consider an RTL adaptation method that provides a smaller modification cost for this layout method. When we set left(view.origin.x) = a in LTR and map to RTL coordinate system, it is actually setting right(view.left + view.width) = view.superview.width - a . When we set right = a in LTR and map it to the RTL coordinate system, it is actually setting view.superview.width - a + self.width , therefore, we can unify the two coordinate systems, refer to the definition in AutoLayout, expand View's leading and trailing properties.

 @implementation UIView (RTL)

- (CGFloat)leading {
    NSAssert(self.superview != nil, @"使用leading必须当前view添加到superView!");
    if ([self isRTL]) {
        return self.superview.width - self.right;
    }
    return self.left;
}

- (void)setLeading:(CGFloat)leading {
    NSAssert(self.superview != nil, @"使用leading必须当前view添加到superView!");
    if ([self isRTL]) {
        self.right = self.superview.width - leading;
    } else {
        self.left = leading;
    }
}

- (CGFloat)trailing {
    NSAssert(self.superview != nil, @"使用trailing必须当前view添加到superView!");
    if ([self isRTL]) {
        return self.leading + self.width;
    }
    return self.right;
}

- (void)setTrailing:(CGFloat)trailing {
    NSAssert(self.superview != nil, @"使用trailing必须当前view添加到superView!");
    if ([self isRTL]) {
        self.right = self.superview.width - trailing + self.width;
    } else {
        self.left = trailing - self.width;
    }
}

@end

Before setting leading and trailing, it is required that the View has been added to the superview and the size has been set. This can be satisfied in most scenarios (taking our project as an example, we have not encountered scenarios that cannot meet the conditions for the time being).
After these related methods are added, when the RTL is adapted, the left setting of the original (LTR scene) is changed to use the leading setting. The original right setting is changed to use the trailing setting. The concept usage of AutoLayout is basically the same, and the adaptation cost is greatly reduced.

Image

As mentioned above, not all pictures need to be flipped in RTL mode, only some pictures (generally, pictures with clear directional meaning and nature) need to be flipped.
For images that need to be flipped, there are several ways to handle them.
After iOS9, UIImage added related methods,

 - (UIImage *)imageFlippedForRightToLeftLayoutDirection API_AVAILABLE(ios(9.0));
@property (nonatomic, readonly) BOOL flipsForRightToLeftLayoutDirection API_AVAILABLE(ios(9.0));

For images that need to be flipped differently under LTR and RTL, it can be set by imageView.image = targetImage.imageFlippedForRightToLeftLayoutDirection() . Or in Image Set, set the Direction of related image resources,
设置direction
It should be noted that these two methods are applied to UIImageView , and will be invalid for other containers. At the same time, pay attention to the use of UIImageView semanticContentAttribute for flip judgment, semanticContentAttribute the setting is wrong, the final display image will also be wrong.
For the above reasons, a custom flip method can be provided on UIImage ,

 @implementation UIImage (RTL)
- (UIImage *_Nonnull)checkOverturn {
    if (isRTL) {
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale);
        CGContextRef bitmap = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(bitmap, self.size.width / 2, self.size.height / 2);
        CGContextScaleCTM(bitmap, -1.0, -1.0);
        CGContextTranslateCTM(bitmap, -self.size.width / 2, -self.size.height / 2);
        CGContextDrawImage(bitmap, CGRectMake(0, 0, self.size.width, self.size.height), self.CGImage);
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        return image;
    }
    return self;
}
@end

At the same time, it provides a flip method for the View container,

 @implementation UIView (RTL)
- (void)checkOverturn {
    // 避免重复翻转
    if (self.overturned) {
        return;
    }
    // 基于transform翻转
    self.transform = CGAffineTransformScale(self.transform, -1, 1);
}
@end

The top-level business can be handled by selecting an appropriate method according to the actual scenario.

text

There are three important issues to be dealt with in the text here, one is the alignment of the text. One is the handling of AttributeString. One is the order of the characters in the text (characters from left to right or right to left). We introduce them one by one.

alignment

Let's discuss alignment first. In NSText , NSTextAlignment is defined as,

 /* Values for NSTextAlignment */
typedef NS_ENUM(NSInteger, NSTextAlignment) {
    NSTextAlignmentLeft      = 0,    // Visually left aligned
#if TARGET_ABI_USES_IOS_VALUES
    NSTextAlignmentCenter    = 1,    // Visually centered
    NSTextAlignmentRight     = 2,    // Visually right aligned
#else /* !TARGET_ABI_USES_IOS_VALUES */
    NSTextAlignmentRight     = 1,    // Visually right aligned
    NSTextAlignmentCenter    = 2,    // Visually centered
#endif
    NSTextAlignmentJustified = 3,    // Fully-justified. The last line in a paragraph is natural-aligned.
    NSTextAlignmentNatural   = 4     // Indicates the default alignment for script
}

Let's take the most commonly used Text container UILabel as an example. UILabel ec072ed7e0f47db9cc104bd6864d32e7--- ,如果没有设置textAlignment ,在iOS9 之前会NSTextAlignmentLeft ,在iOS9 之后默认是NSTextAlignmentNaturalNSTextAlignmentNatural will automatically help us adjust the appropriate alignment according to whether the system language is RTL. For scenarios that require in-app language setting, the system's default processing cannot be used because the in-app language may be inconsistent with the system language. You need to manually set the UILabel of textAlignment according to whether the current application is set to be RTL language. For convenience, the rtlAlignment method of ---6aaa17e8f1b5571985a278cb908ef4a0 UILabel can be extended. The business layer can be set as required rtlAlignment .

 typedef NS_ENUM(NSUInteger, NMLLabelRTLAlignment) {
    NMLLabelRTLAlignmentUndefine,
    NMLLabelRTLAlignmentLeft,
    NMLLabelRTLAlignmentRight,
    NMLLabelRTLAlignmentCenter,
};

@implementation UILabel (RTL)

- (void)setRtlAlignment:(RTLAlignment)rtlAlignment {
    [self bk_associateValue:@(rtlAlignment) withKey:@selector(rtlAlignment)];
    
    switch (rtlAlignment) {
        case RTLAlignmentLeading:
            self.textAlignment = (isRTL ? NSTextAlignmentRight : NSTextAlignmentLeft);
            break;
        case RTLAlignmentTrailing:
            self.textAlignment = (isRTL ? NSTextAlignmentLeft : NSTextAlignmentRight);
            break;
        case RTLAlignmentCenter:
            self.textAlignment = NSTextAlignmentCenter;
        case RTLAlignmentUndefine:
            break;
        default:
            break;
    }
}

- (RTLAlignment)rtlAlignment {
    NSNumber *identifier = [self bk_associatedValueForKey:@selector(rtlAlignment)];
    if (identifier) {
        return identifier.integerValue;
    }
    return RTLAlignmentUndefine;
}

@end

Handling of AttributeString

Since setting textAlignment cannot take effect on AttributeString, AttributeString needs to be handled separately. The processing method is similar to setting textAlignment, but it is replaced with NSParagraphStyle to process.

 @implementation NSMutableAttributedString (RTL)

- (void)setRtlAlignment:(RTLAlignment)rtlAlignment {
    switch (rtlAlignment) {
        case RTLAlignmentLeading:
            self.yy_alignment = (isRTL ? NSTextAlignmentRight : NSTextAlignmentLeft);
            break;
        case RTLAlignmentTrailing:
            self.yy_alignment = (isRTL ? NSTextAlignmentLeft : NSTextAlignmentRight);
            break;
        case RTLAlignmentCenter:
            self.yy_alignment = NSTextAlignmentCenter;
        case RTLAlignmentUndefine:
            break;
        default:
            break;
    }
}

@end

character order

The system will use the first character of Text as the basis for judging the sorting order. For example, the text "مرحبا Hello", because the first character is an Arabic character, the system will use RTL rules to process. Similarly, if the text is "Hello مرحبا", since the first character is Chinese, the LTR rule will be used. This processing method is no problem when there is only a single language in Text. However, when the RTL language and LTR language are mixed, the situation becomes much more complicated and requires more careful consideration.

Taking a common example, such as the @ format syntax often used in chat messages, there are probably these scenarios in LTR and RTL,
at

It can be seen that although the default processing of the system can deal with most cases. However, in some scenarios, the requirements cannot be met, such as the above label[3].
We want to treat "@" and the following username as a whole, for "مرحبا@我, how is the weather today", we expected it to be displayed as "@我, how is the weather today مرحبا", but it ended up being displayed as "me , how is the weather today @مرحبا". Or, for example, we want to show in LTR,
image right
But in the end it will show as,
image wrong

For these scenarios, we need to insert some relevant Unicode to do the correction. The more commonly used related Unicode are as follows.
untitled

Going back to the two examples just now, for "مرحبا@我, how is the weather today", iOS treats @ as part of the Arabic مرحبا, we need to manually add the LEFT-TO-RIGHT sign to @ and declare it as LTR exhibit. For the second example, we need to add \u202A to several Arabic texts to declare as LTR, and use \u202C as the closing tag.
text final

Other notes

In addition to those described above, there are some scattered points that need to be paid attention to.

UICollectionView

UICollectionView It also needs to be flipped in the RTL scenario, the system will not help us do this by default, we need to deal with it by ourselves. After iOS11, UICollectionViewLayout extends a readonly of property .

 @property(nonatomic, readonly) BOOL flipsHorizontallyInOppositeLayoutDirection;

flipsHorizontallyInOppositeLayoutDirection default is false , when set to true , UICollectionView horizontal coordinate will be flipped according to the current RT. Since this is a property of readonly , we need to inherit UICollectionViewLayout and rewrite the getter method of flipsHorizontallyInOppositeLayoutDirection .

UIEdgeInsets

UIEdgeInsets is defined in left and right , in RTL scenario, the system will not help us to do flip processing. Although after iOS11, the system has added NSDirectionalEdgeInsets definition, but the commonly used UI controls (such as UIButton , etc.) do not have extended related properties, you still need to set UIEdgeInsets . Therefore, you can consider adding a definition similar to UIEdgeInsetsMake_RTLFlip for the convenience of the upper layer.

 UIEdgeInsets UIEdgeInsetsMake_RTLFlip(CGFloat top, CGFloat left, CGFloat bottom, CGFloat right)
{
    if (!isRTL)
    {
        UIEdgeInsets insets = {top, left, bottom, right};
        return insets;
    }
    UIEdgeInsets insets = {top, right, bottom, left};
    return insets;
}

UINavigationController

The sliding return gesture of navigationBar will do RTL processing according to the current system language. For our commonly used LTR scene, it is to swipe right to return. In the RTL scenario, it is a left swipe to return. For in-app custom language scenarios, setting UIView.appearance().semanticContentAttribute will not change this gesture, and you need to set UINavigationController.view.semanticContentAttribute .

 - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        self.view.semanticContentAttribute = [UIView appearance].semanticContentAttribute;
    }
    return self;
}

Gesture

For Gestures with directionality (such as UISwipeGestureRecognizer etc.), the system will not change the response direction of the gesture. This can only be logically judged by the upper layer based on whether the current RTL scene is present. Generally speaking, such gestures are not used very frequently, so the business layer adaptation processing cost is not large.

number

As introduced at the beginning, numbers are also an important point to consider. Whether to use Western Arabic numerals or Eastern Arabic numerals differs in numeral rules and display. Because the Western Arabic numeral rules used in this business adaptation are the same as our daily contact, it will not be expanded here. If Eastern Arabic numerals are used, additional processing of the numeral logic is required.

Summarize

At this point, the overall RTL compatibility is basically completed. To sum up, since the current App needs to support the in-app language setting, many problems are complicated. Moreover, due to the many characteristics of the App itself, it is necessary to choose a solution with relatively controllable costs and risks when designing the solution. If you do not need to set the App language independently in the app, or if you just want to develop an App from 0 to 1, you can design a more suitable solution for the current business according to your own business characteristics.

References

This article is published from the NetEase Cloud Music technical team, and any form of reprinting of the article is prohibited without authorization. We recruit various technical positions all year round. If you are ready to change jobs and happen to like cloud music, then join us at grp.music-fe(at)corp.netease.com!

云音乐技术团队
3.6k 声望3.5k 粉丝

网易云音乐技术团队