iOS屏幕適配實踐淺談
前端開發的屏幕適配其實算是基本功,每個碼農在長期實踐中都有自己的總結。
在 iOS 平臺上,蘋果爸爸對適配的支持個人感覺很不人性化,提供了 AutoLayout、sizeClass 等技術,感覺沒有前端類似 flexBox 這樣的技術來得靈活。像是點歪了技能樹,過于重視使用 xib 配置 UI,但很多碼農還是習慣純代碼編程。Cocoa 沒有 css 這樣的純布局文件,導致很多時候我們將布局、UI 和邏輯寫在一起,十分混亂、冗長。
下面簡單介紹下在實踐中適配屏幕的方向思路,拋磚引玉。
從設計到代碼:溝通與標準
App 的 UI 界面是由設計人員(產品,UI)繪制的,然后由開發實現,雙方要有良好的溝通,并且把設計內容標準化、文檔化。
對設計方來說,適配的規則總是在設計師心中的,是按比例的縮放,還是固定的間距,是公用一套規則,還是在大屏下有特殊的布局,都需要有明確方式傳達給耿直的碼農們。
一般常見的布局方式有:
- 固定間距:在不同尺寸下,間距總是固定。
- 流式布局:文字,圖片等在不同屏幕下流式排布,比如大屏下一行顯示四張圖片,小屏一行三張,圖片尺寸固定。
- 比例放大:間距,文字大小,圖片大小等比例放大。
- 保持比值:兩個UI元素或者圖片的長寬等屬性保持一定的比值。
- 對齊:元素間按某個方向對齊。
設計師需要將這些布局規則標注清楚,有利溝通,也方便日后追溯。
對于一些通用 UI 組件,要進行標準化,設計上有利于 App 風格統一,實現上也方便開發進行封裝。
UI 的搭建:xib VS 純代碼
蘋果一直用 xib 來標榜他們家 App 開發簡單易上手:將各種你需要的東西往屏幕上一拖一放,一個 UI 界面就搞定了,這很 cool 不是嘛!
Xib 的優點顯而易見:
- 易上手、可視化,所見即所得
- 減少代碼量
- 快,適合小 App 快速開發
但是在我們的實際項目中,是不推薦使用 xib 的。
首先,xib 本身過于笨拙,只能搭建一些簡單的 UI,動態性很差,難以滿足 App 復雜的 UI 交互需求。
其次,做過性能優化的同學都知道,xib(or StoryBoard)的性能是很差的,相對于用純代碼 alloc 的組件來說,xib 加載慢,而且會占用 App 包的體積。不僅僅是 App 的性能,使用老 mac 打開較大的 xib 文件,有時候會卡的你懷疑人生,嚴重影響開發效率(心情)。
除此以外,對于團隊協作來說,xib 也不是一個好選項:閱讀困難,無法在 git 上查看歷史改動,容易造成沖突,造成沖突后難以解決,元素通過 outlets 與代碼的鏈接難以維護,容易在改動中造成錯漏等等。
另外,對于我這種中途轉到前端的工程師來說,對一切在 IDE 界面上配置的東西都有種迷之不信任,感覺不如一行行黑底白字的代碼來的靠譜。
當然我們不是完全禁用了 xib,用代碼碼 UI 的缺點也很明顯:繁瑣,代碼量大。因此對一些元素較多,又比較固定的 UI 組件,我們可以用 xib 來減少代碼量:
針對UI代碼繁瑣,重復編碼多的情況,我們可以通過適當封裝(UI 工廠類),組織結構(MVC,分離 UI 代碼)等手段,清晰邏輯。
- // label 工廠方法
- + (UILabel *)labelWithFont:(UIFont *)font
- color:(UIColor *)
- text:(NSString *)text
- attributeText:(NSAttributeString *)attributeText
- alignment:(NSTextAlignment)alignment;
布局:返璞歸真
從 iOS7 開始蘋果在 Cocoa 平臺引入 AutoLayout 進行 UI 的基本布局。但是 AutoLayout 非常反人類,不僅代碼繁瑣而且使用不靈活限制很多。
比如我想要把三個元素等間距地展示在屏幕上,用 AutoLayout 寫完基本蛋都碎了,更別說動態地在兩套布局間切換這種高級需求。
后來蘋果推出 sizeClass,試圖解決多套布局的問題,但是仍然沒有觸及到碼農的痛點,而且依賴 xib 使它泛用性不好。
一段典型的 AutoLayout 代碼如下所示:
- _topViewTopPositionConstraint = [NSLayoutConstraint
- constraintWithItem:_topInfoView
- attribute:NSLayoutAttributeTop
- relatedBy:NSLayoutRelationEqual
- toItem:self.view
- attribute:NSLayoutAttributeTop
- multiplier:1.0
- constant:self.navigationController.navigationBar.frame.size.height + self.navigationController.navigationBar.frame.origin.y];
- [self.view addConstraint:topViewLeftPositionConstraint];
- (這里省略上述類似結構*4)
上面省略了很多代碼,實際上一頁都放不下。它干了什么呢,只是將一個元素緊貼屏幕上邊緣放置。項目中我們會使用三方 AutoLayout 的封裝:PureLayout ,簡化代碼,也有其它實用功能。
AutoLayout 比較適合:
- 基本的對齊(上下左右對齊,居中對齊等)
- 固定的布局,固定的間距,動態性不高的頁面
- 簡單且數量較少的 UI 元素
不擅長:
- 比例布局
- 動態性較強的頁面局部
- 不同屏幕大小比例的適配
- 復雜的 UI
另外有一點,AutoLayout 對性能是有損耗的,所以對性能有要求的場景,比如列表中的 cell,我們會用代碼計算 frame,提高滑動幀率。
所以在實際工程中,需要來選擇布局方式。
下面是 App 中首頁新聞 Feeds 的布局代碼片段:
- - (void)layoutSubviews {
- [super layoutSubviews];
- CGFloat cellWidth = CGRectGetWidth(self.bounds);
- CGFloat currentY = 0.f;
- // 0.content
- CGFloat cellHeight = CGRectGetHeight(self.bounds);
- CGFloat contentHeigth = cellHeight - kCellPaddingHeight;
- _mainContentView.frame = CGRectMake(0, 0, cellWidth, contentHeigth);
- // 1. topic
- CGFloat topicLabelWidth = [_topicLabel.text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:_topicLabel.font} context:nil].size.width;
- CGFloat topicLabelHeight = [@"測高度" boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:_topicLabel.font} context:nil].size.height;
- CGFloat topicLogoLeftPadding = 3.f;
- CGFloat topicLogoWidth = 10.f;
- CGFloat topicLeftPadding = 13.f;
- _topicView.frame = CGRectMake(topicLeftPadding, currentY + kTopicUpPadding, topicLogoWidth + topicLogoLeftPadding + topicLabelWidth, topicLabelHeight);
- _topicLogo.frame = CGRectMake(topicLabelWidth + topicLogoLeftPadding, CGRectGetHeight(_topicView.frame) / 2.0 - topicLogoWidth / 2.0, topicLogoWidth, topicLogoWidth);
- _topicLabel.frame = CGRectMake(0, 0, topicLabelWidth, topicLabelHeight);
- (省略大量代碼……)
- // 10._sourceLabel
- CGSize sourceSize = [_sourceLabel.text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:_sourceLabel.font} context:nil].size;
- _sourceLabel.frame = CGRectMake(kEdgeHorizontalPadding, currentY + kLeadingUpPading, sourceSize.width, sourceSize.height);
- }
可以看到,為了確定每個元素的位置,我們需要進行大量的計算,代碼可讀性也不好,繁瑣難讀。如果引入動態性,比如不同屏幕字體大小改變,元素大小按比例擴大等,則計算量又要上一個數量級。
動態布局:清晰獨立
UI 界面是動態的,在不同狀態,不同尺寸或者手機的橫豎屏情況下,我們往往需要在多套布局方案中切換,或者對布局進行微調。如果使用 xib 布局的話,可以使用 SizeClass + AutoLayout 的方案;如果是代碼實現的頁面,則沒有官方提供的工具,只能用邏輯去判斷。
一般來說,我們寫復雜的 UI 頁面,需要遵循兩個原則:
- UI 布局代碼要清晰:這是最重要的,要一眼就知道在調整那一塊,怎么調整,如果不能,適當拆分,優化命名。
- 布局代碼要和業務邏輯獨立:在一些常用設計模式下,我們會將 UI 和數據模型解耦,在 UI 內部,同樣要將交互,配置這些邏輯和布局解耦,獨立出類似前端 css 這樣的純布局文件。
將布局代碼提煉出來,在不同尺寸下調用不同的實現:
- if (IS_IPHONE_6){
- self.layout = [MyLayout iPhone6Layout];
- }else if (IS_IPHONE_6_PLUS){
- self.layout = [MyLayout iPhone6PlusLayout];
- }
- // 實現小屏幕布局
- + (MyLayout *)iPhone6Layout {...}
- // 實現大屏幕布局
- + (MyLayout *)iPhone6PlusLayout {...}
字體適配:字體集
在開發中我們經常會遇到需要動態設置字體的情況:
- 不同屏幕尺寸,或者橫豎屏,需要展示不同的字體大小。
- 為用戶提供了文章調節字體選項。
- App 的不同語言版本,需要顯示的字體不一樣。
較為簡單的做法是用宏或者枚舉定義字體參數,針對不同尺寸的屏幕,我們拿到不同的值:
- #ifdef IPHONE6
- #define kChatFontSize 16.f
- #else IPHONE6Plus
- #define kChatFontSize 18.f
- #endif
在對一些舊代碼做字體適配擴展的時候,直接修改源碼改動太多,容易混亂,可以采用 runTime 方法 hack Label 等控件的展示,替換原有的 setFont 方法:
- + (void)load{
- Method newMethod = class_getClassMethod([self class], @selector(mySystemFontOfSize:));
- Method method = class_getClassMethod([self class], @selector(systemFontOfSize:));
- method_exchangeImplementations(newMethod, method);
- }
- + (UIFont *)mySystemFontOfSize:(CGFloat)fontSize{
- UIFont *newFont=nil;
- if (IS_IPHONE_6){
- newFont = [UIFont adjustFont:fontSize * IPHONE6_INCREMENT];
- }else if (IS_IPHONE_6_PLUS){
- newFont = [UIFont adjustFont:fontSize * IPHONE6PLUS_INCREMENT];
- }else{
- newFont = [UIFont adjustFont:fontSize];
- }
- return newFont;
- }
以上套路缺點顯而易見:不夠靈活,將邏輯分散,不便于維護,擴展性也不好。
一種比較好的實踐是引入字體集(Font Collection)的概念,什么是字體集呢,我們在用 Keynote 或者 Office 的時候,軟件會提供一些段落樣式,定義了段落、標題、說明等文字的字體,我們可以在不同的段落樣式中切換,來直接改變整個文章的字體風格。
聽上去和我們的需求是不是很像呢,我們在代碼中也是做類似的事情,將不同場景下的字體定義到一個 Font Collection 中:
- @protocol XRFontCollectionProtocol <NSObject>
- - (UIFont *)bodyFont; // 文章
- - (UIFont *)chatFont; // 聊天
- - (UIFont *)titleFont; // 標題
- - (UIFont *)noteFont; // 說明
- ......
- @end
不同的場景,靈活選擇不同的字體集:
- + (id<XRFontCollectionProtocol>)currentFontCollection {
- #ifdef IS_IPhone6
- return [self collectionForIPhone6];
- #elif IS_IPhone6p
- return [self collectionForIPhone6Plus];
- #endif
- return nil;
- }
- // set font
- titleLabel.font = [[XRFontManager currentFontCollection] titleFont];
適配新的屏幕或者場景,我們只需要簡單地增加一套字體集就好了,可以很方便的管理 App 中的字體樣式,做動態切換也很簡單。
總結來說,用代碼在一個尺寸實現設計稿是比較簡單的,但是要在各種尺寸下忠實反應設計的想法需要合理的代碼設計以及一定的代碼量。
UI 的還原其實也是大前端開發非常重要的部分,作為程序員,往往重視代碼的穩定,業務的正常使用而忽略軟件界面這個同樣重要的用戶體驗因素。設身處地地想,如果設計看到自己精心調配的比例、字體、色號在不同尺寸手機上顯示得歪七倒八,一定會氣的要死吧。
作者是杏仁移動開發工程師,前嵌入式工程師,關注大前端技術新潮流。