鍍金池/ 教程/ iOS/ 響應(yīng)式視圖
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

響應(yīng)式視圖

在任何應(yīng)用當(dāng)中將界面做好都不是一件容易的事情。在一個(gè)小小的四邊形中呈現(xiàn)內(nèi)容以及互動(dòng)的結(jié)合看似容易,其實(shí)就算是在很小的應(yīng)用當(dāng)中也很容易寫出混亂不堪的視圖代碼。在有很多工程師合作的復(fù)雜項(xiàng)目當(dāng)中,比如 Facebook 的新鮮事頁面,這些視圖的開發(fā)和維護(hù)是有相當(dāng)難度的。

最近我一直在開發(fā)一個(gè)叫做 Components 的庫來簡(jiǎn)化 iOS View 的開發(fā)。它強(qiáng)調(diào)單項(xiàng)數(shù)據(jù)流動(dòng):從不可變的模型到不可變的”組件”,這些組件描述了視圖應(yīng)該如何被設(shè)置。這個(gè)庫從現(xiàn)在網(wǎng)絡(luò)開發(fā)中很流行的 React Javascript 庫 中汲取了很多靈感。React 通過一個(gè)叫做 “虛擬 DOM” 的概念來抽象化對(duì) DOM 處理。同樣地,Components 會(huì)抽象化對(duì) UIView 層次的處理。

在這篇文章中我會(huì)著重說明使用 Components 在 iOS 上來呈現(xiàn)視圖的一些好處,并且分享一些我學(xué)習(xí)到的經(jīng)驗(yàn)。相信在大家自己的應(yīng)用中也能夠用得上。

零數(shù)學(xué)布局

假設(shè)我們有四個(gè)子視圖而且我們想將它們垂直的分布,水平方向上使用全寬。經(jīng)典的辦法是去實(shí)現(xiàn) -layoutSubviews-sizeThatFits: 這兩個(gè)函數(shù),這樣做需要 52 行代碼. 因?yàn)槠渲杏泻芏鄶?shù)學(xué)運(yùn)算,第一眼看上去不是很容易看出來是在豎直地?cái)[放視圖。在這兩個(gè)函數(shù)中有點(diǎn)重復(fù)的地方,所以在未來的修改中保持同步并不容易。

如果我們使用蘋果的自動(dòng)布局 API,我們可以獲得小小的改進(jìn):34行代碼.。同時(shí)數(shù)學(xué)運(yùn)算以及重復(fù)的代碼問題亦可以解決!但是我們卻換來了另外一些問題:自動(dòng)布局設(shè)置起來很困難,[^1] 調(diào)試起來也很費(fèi)力,[^2] 并且復(fù)雜的層次會(huì)讓運(yùn)行時(shí)性能打一些折扣。[^3]

^1 Interface Builder 簡(jiǎn)化了自動(dòng)布局,但是因?yàn)?XIBs 文件難以融合,你很難在大的團(tuán)隊(duì)里面使用它們。

^2 有很多關(guān)于如何調(diào)試自動(dòng)布局的文章博客

^3 我們用自動(dòng)布局制作了一個(gè)很簡(jiǎn)單的新鮮事頁面,做到 60 幀每秒是非常的困難。

Components 從 CSS Flexbox specification 的布局系統(tǒng)中中吸取了靈感。我不會(huì)介紹太多的細(xì)節(jié),如想進(jìn)一步學(xué)習(xí)請(qǐng)參照 Mozilla 的高質(zhì)量教程 。因?yàn)?Flexbox 大幅簡(jiǎn)化了布局,相對(duì)應(yīng)的 Components 僅僅需要18行代碼。你也不需要使用任何數(shù)學(xué)運(yùn)算以及基于字符串的視覺格式語言。

用下面的代碼就可以依靠 Components 來做到同樣的垂直擺放視圖,對(duì)于不熟悉的人們來說,句型看上去可能會(huì)很奇怪 -- 稍后再來解釋:

@implementation FBStoryComponent

+ (instancetype)newWithStory:(FBStory *)story
{
  return [super newWithComponent:
          [FBStackLayoutComponent
           newWithView:{}
           size:{}
           style:{.alignItems = FBStackLayoutAlignItemsStretch}
           children:{
             {[FBHeaderComponent newWithStory:story]},
             {[FBMessageComponent newWithStory:story]},
             {[FBAttachmentComponent newWithStory:story]},
             {[FBLikeBarComponent newWithStory:story]},
           }]];
}

@end

那些個(gè)波形括號(hào)!

沒錯(cuò),我們用的是 Objective-C++。聚合實(shí)例化給我們一個(gè)簡(jiǎn)明并且類安全的方法來指明樣式結(jié)構(gòu)。以下是另外幾個(gè)有效的 style: 值:

style:{} // default values
style:{.justifyContent = FBStackLayoutJustifyContentCenter}
style:{
  .direction = FBStackLayoutDirectionHorizontal,
  .spacing = 10,
}

使用像 std:vectorstd:unordered_map 這樣的標(biāo)準(zhǔn)庫中的容器比我們?cè)?Objective-C 中使用相對(duì)應(yīng)容器有更強(qiáng)的類型安全性。我們同時(shí)能夠用棧來調(diào)用臨時(shí)視圖數(shù)據(jù)結(jié)構(gòu),提升性能。

Components 在句型風(fēng)格上還有另外一些有些奇怪的地方 (為了簡(jiǎn)介而使用 +newWith... 代替 -initWith...,以及非常規(guī)的縮進(jìn)等),這要在更多的上下文中才解釋得通 --- 這個(gè)話題單獨(dú)可以再寫一篇文章?,F(xiàn)在我們回到主題。

聲明式而不是命令式

就算是全新的句型,也不難看懂我們擺放視圖的 Components 版本。用一個(gè)重要的原因是:它是聲明式的而不是命令式的。

大多數(shù)的 iOS 視圖代碼讀起來感覺像是一系列的指令:

  • 建立一個(gè)新的 header 視圖。
  • 將其存進(jìn) _headerView 實(shí)例變量。
  • 加入視圖中
  • 加入限制將頭視圖的左右兩邊和父視圖對(duì)齊。
  • ...對(duì)其他視圖做相似的操作
  • 加入更多擺放視圖用的限制

而 Components 的代碼是聲明式的:

  • 一個(gè) story 視圖是通過將四個(gè)組件垂直擺放并且左右拉升來做到的。

將這兩者的區(qū)別想象成給工人們列出所有材料和指示的清單,和僅僅給他們一張藍(lán)圖的區(qū)別。延伸一下這個(gè)比喻,一個(gè)建筑師不應(yīng)該在工地上四處奔走來告訴建筑工人如何去干他們的活 -- 這樣的話會(huì)太過于混亂。宣言性的技巧著重于什么需要被完成,而不是如何去完成它;結(jié)果是,你得以將精力集中在要解決的問題上而不是實(shí)現(xiàn)細(xì)節(jié)上。

使用 Components 的時(shí)候,不用去操心本地變量和屬性。你不需要在創(chuàng)建視圖的地方,添加限制的地方和使用模型來配置視圖的地方來回跳躍。所有的事情就在你面前好好的放著。

我的建議是:永遠(yuǎn)傾向于聲明式風(fēng)格而不是命令式風(fēng)格,這樣一來代碼更易于讀懂,也更易于維護(hù)。

混合優(yōu)于繼承

小小測(cè)驗(yàn):以下代碼是干什么的?

- (void)loadView {
  self.view = [self newFeedView];
}

- (UIView *)newFeedView {
  return [[FBFeedView alloc] init];
}

如果使用了繼承,那它可以是在做任何事情??赡?-newFeedView 在子類中被重寫了,返回了一個(gè)完全不同的視圖。又或許 -loadView 被重寫去調(diào)用了一個(gè)不同的函數(shù)。在大規(guī)模的代碼庫中大量使用子類會(huì)使得閱讀代碼和理解它們實(shí)際做了什么變得困難。[^4] 繼承產(chǎn)生的問題在我們使用 Components 改寫新鮮事頁面之前經(jīng)常發(fā)生,比如 FBHorizontalScrollerView 有很多子類重寫了不同的方法,這使得超類難以閱讀和重構(gòu)。

^4 objc.io 在以前介紹過這個(gè)主題這篇維基百科文章 也做了很好地介紹。

Components 永遠(yuǎn)都是被混合的,從來不會(huì)被繼承。將它們想象成小的基礎(chǔ)模塊,你可以將它們拼裝在一起組成非常棒的東西。

但是對(duì)混合的大量使用會(huì)造成非常深的層次,而深的 UIView 層次會(huì)將滑動(dòng)變得非常緩慢。有一點(diǎn)需要特別指明的是,其實(shí)是存在那種完全不需要為其創(chuàng)建視圖的 component 的。[^5] 在實(shí)踐中,大多數(shù)的組件是不需要視圖的。 就拿 FBStackLayoutComponent 來作例子;它將它的子視圖碼放在一起,但是它并不需要在層級(jí)中的一個(gè)視圖去執(zhí)行這項(xiàng)任務(wù)。

^5 相同地,在 React 中,也并非每一個(gè)組件都會(huì)創(chuàng)造一個(gè)相應(yīng)的 DOM 元素。

盡管新鮮事頁面的組件層次有好幾十層,但是得到的視圖層其實(shí)才有三層。我們獲取了所有混合帶來的好處卻沒有付出什么代價(jià)。

如果說我從龐大的代碼庫中學(xué)到一樣?xùn)|西的話,就是不要使用繼承!轉(zhuǎn)而使用混合或者其他的模式。

自動(dòng)回收

使用 UITableView 時(shí)的重要一步是 cell 的回收:少量的 UITableViewCell 實(shí)例會(huì)被反復(fù)地利用。這是實(shí)現(xiàn)驚人的滑動(dòng)速度得以實(shí)現(xiàn)的重要原因。

但是,要想在多工程師分享的代碼庫中妥當(dāng)?shù)鼗厥諒?fù)雜的 cells 并不容易。在開始使用 Components 之前,我們?cè)砑右粋€(gè)功能來逐漸淡出一個(gè)故事的一部分界面,但是我們忘記了在回收時(shí)重設(shè) alpha 的值,這樣一來其他的故事也被隨機(jī)的淡化了!另一個(gè)例子,忘記妥善地重設(shè) hidden 屬性導(dǎo)致隨機(jī)地丟失或者覆蓋某些內(nèi)容。

如果使用 Components,你永遠(yuǎn)不需要擔(dān)心回收。庫會(huì)來很好地管理它。不同于寫祈使性的代碼來正確地設(shè)置可能在任何狀態(tài)中所回收的視圖,你只需要指明一個(gè)視圖狀態(tài)即可。庫會(huì)計(jì)算出完成這項(xiàng)任務(wù)所需的最少步驟。

一次優(yōu)化,處處受益

因?yàn)樗袑?duì)視圖的處理全都由 Components 的代碼完成,我們得以通過優(yōu)化一個(gè)算法來提升各個(gè)地方的速度。相較于修改 400 個(gè) UIView 子類并心中默念:“這可是一個(gè)龐大的項(xiàng)目”來說,優(yōu)化一個(gè)地方并且處處受益要來的有意義的多。

比如說,我們加入了一個(gè)優(yōu)化來確保在重新設(shè)置視圖的時(shí)候,除非值確實(shí)被改變了,否則不去使用屬性的 setter (比如 -setText)。盡管大多數(shù)的 setter 在值沒有變化的情況下還是非常有效率的,但我們還是在性能上得到了提升。另外一個(gè)優(yōu)化確保了只有在必要的情況下才重新排序視圖 (通過使用 -exchangeSubviewAtIndex:withSubviewAtIndex:),因?yàn)檫@項(xiàng)操作相對(duì)來說成本很高。

最好的部分是,這些優(yōu)化并不需要任何人去改變寫代碼的方式。開發(fā)者們能夠?qū)W⑼瓿扇蝿?wù)而不是了解高成本的操作并學(xué)會(huì)去避免他們 - 這是一個(gè)對(duì)整個(gè)團(tuán)隊(duì)來說非常大的幫助。

動(dòng)畫的挑戰(zhàn)

沒有一個(gè)框架能夠解決所有的問題,響應(yīng)式 (reactive) 的界面框架中一個(gè)有挑戰(zhàn)性的問題是實(shí)現(xiàn)動(dòng)畫相較于使用傳統(tǒng)視圖框架要更困難一些。

響應(yīng)式的界面開發(fā)鼓勵(lì)將狀態(tài)之間的切換明確化。舉個(gè)例子,有一個(gè)界面會(huì)刪減一部分文本內(nèi)容,但是允許用戶按一個(gè)按鈕來展開并且查看全部的文本。這個(gè)可以輕易通過兩個(gè)狀態(tài)來做到:{Collapsed, Expanded}。

但是如果你想把展開文本做成動(dòng)畫,或者讓用戶自己通過拖拽去精確地控制顯示多少文本內(nèi)容,那么就不可能只是用兩個(gè)狀態(tài)了。有數(shù)以百計(jì)的狀態(tài)對(duì)應(yīng)著動(dòng)畫中某個(gè)時(shí)刻有多少文本會(huì)被顯示出來。響應(yīng)式框架要求你在開始的時(shí)候就把狀態(tài)變化安排好,正是因?yàn)檫@一點(diǎn)動(dòng)畫才變得如此困難。

我們開發(fā)了兩種手段來管理 Components 中的動(dòng)畫:

  • 可以使用一個(gè)叫做 animationsFromPreviousComponent: 的 API 來宣言性地表達(dá)靜態(tài)的動(dòng)畫。比如說,一個(gè)組件可以指明在第一次出現(xiàn)的時(shí)候使用漸入的效果。
  • 動(dòng)態(tài)動(dòng)畫可以通過使用一個(gè) “逃出窗” 來回到傳統(tǒng)的祈使和可變的代碼來完成。你不會(huì)得到宣言性代碼和狀態(tài)管理明確化帶來的好處,但是你可以自由地使用 UIKit 的威力。

我們的設(shè)想是開發(fā)強(qiáng)大的工具去用宣言性的代碼來寫出簡(jiǎn)約的動(dòng)態(tài)動(dòng)畫,我們只是還沒有完成這個(gè)計(jì)劃而已。

React Native

在 Facebook,我們最近宣布了 React Native,一個(gè)運(yùn)用 React Javascript 庫來處理本地應(yīng)用中 UIView 層次的框架,與網(wǎng)頁版不同,這個(gè)庫使管理的是 UIView 而非網(wǎng)頁中的 DOM 元素。要告訴大家的是,Components 庫并不是 React Native,而是一個(gè)單獨(dú)的項(xiàng)目,雖然這可能讓人有些驚訝。

它們的區(qū)別是什么?其實(shí)很簡(jiǎn)單:當(dāng)我們用 Components 重建新鮮事頁面的時(shí)候 React Native 還沒有被發(fā)明出來。在 Facebook, 每一個(gè)人都非??春?React Native 的前景。并且已經(jīng)應(yīng)用在Mobile Ads ManagerGroups兩個(gè)應(yīng)用中使用了。

和所有框架一樣,也存在取舍;比如說,Components 選擇使用 Objective-C++ 是因?yàn)樗念愋桶踩院托阅埽?React Native 對(duì) Javascript 的運(yùn)用讓在開發(fā)環(huán)境下即時(shí)更新成為可能。這些項(xiàng)目經(jīng)常會(huì)分享一些推動(dòng)兩者共同進(jìn)步的創(chuàng)意。

AsyncDisplayKit

那么用來驅(qū)動(dòng) Facebook Paper 應(yīng)用的 UI 框架 AsyncDisplayKit 呢?它增添了在后臺(tái)線程計(jì)算和渲染的能力,讓你無需面對(duì)使用 UIKit 主線程會(huì)遇到的麻煩。

從設(shè)計(jì)哲學(xué)的角度上來說,AsyncDisplayKit 和 UIKit 的關(guān)聯(lián)比和 React 要更強(qiáng)。不像 React,AsyncDisplayKit 沒有強(qiáng)調(diào)使用宣言性句法,混合以及不可變性。

像 AsyncDisplayKit 一樣,Components 在后臺(tái)線程進(jìn)行組件創(chuàng)造和分布 (這個(gè)很容易,因?yàn)槲覀兊哪P蛯?duì)象和組件本身全都是不可變的 - 不可能出現(xiàn)競(jìng)態(tài)條件!)

AsyncDisplayKit 能夠進(jìn)行復(fù)雜的手勢(shì)驅(qū)動(dòng)的動(dòng)畫,這一點(diǎn)正是 Components 的弱項(xiàng)所在。這樣一來做選擇就很容易了:如果你在設(shè)計(jì)一個(gè)復(fù)雜的手勢(shì)驅(qū)動(dòng)的界面,AsyncDisplayKit 應(yīng)該是正確的選擇。如果你的界面看起來和 Facebook 的新鮮事頁面更類似,那么 Components 是恰當(dāng)?shù)倪x擇。

Components 的未來

Components 庫在所有的顯示大量信息的頁面都會(huì)用到 (新鮮事,時(shí)間軸,群,事件,頁面和搜索等等) 并且正在快速地在 Facebook 應(yīng)用的其他部分被應(yīng)用起來。用簡(jiǎn)潔,宣言性,可混合的組件是非常有趣的。

你可能覺得 Components 中的一些東西聽起來很瘋狂。但是用點(diǎn)時(shí)間消化一下,你可能會(huì)自己挑戰(zhàn)之前的一些假設(shè),但是這些東西我們用著非常好而且對(duì)你們可能也有幫助。如果你想學(xué)習(xí)更多,可以看看這個(gè)演講,它深入地討論了 Components 的一些細(xì)節(jié)。為什么用 React? 的博文和它鏈接的資源都是非常好的參考。

我們非常想和社區(qū)分享 Components 背后的代碼,而且我們馬上要著手去做。如果你有想法要分享,隨時(shí)都可以聯(lián)系我 - 尤其是關(guān)于動(dòng)畫的想法!

上一篇:Android Intents下一篇:Views