iOS13 Compositional Layout

前言

UITableView 和 UICollectionView 是我們開發(fā)者最常用的控件了,大量的流式布局需要這兩個控件來實現(xiàn)虑瀑,因此這兩個控件也是 Apple 重點優(yōu)化的對象。在往屆 WWDC 中滴须,我們已經(jīng)受益于 UITableViewDataSourcePrefetching 舌狗、優(yōu)化版 Autolayout 等帶來的性能提升,以及 UITableViewDragDelegate 帶來的原生拖拽功能扔水。今年痛侍,Apple 帶來了全新的 Compositional Layout 。它將徹底顛覆 UICollectionView 的布局體驗魔市,大大拓展 UICollectionView 的可塑性主届。

背景

早期的 App 設(shè)計相對簡單,使用 UICollectionViewFlowLayout 可以應(yīng)付大多數(shù)使用場景待德。而隨著應(yīng)用的發(fā)展君丁,越來越多的頁面趨于復(fù)雜化,UICollectionViewFlowLayout 在面對復(fù)雜布局往往會顯得力不從心将宪,或者非常復(fù)雜绘闷,需要進行大量的計算和判斷橡庞。而自由度更高的 UICollectionViewLayout 則有著更高的接入門檻,稍有不慎還容易出現(xiàn)各種各樣的 bug 印蔗。

image

我們就拿 App Store為例扒最,它包含了大小不一的 Item ,以及可以上下华嘹、左右滑動的交互吧趣。假如你是開發(fā)者,你會如何搭建這個 UI 除呵?你可能會使用多個 UICollectionView 嵌套在一個 UIScrollerView 中再菊,因為 UICollectionView 的滾動軸只能有一個(橫向 / 豎向)。但如果我告訴你颜曾,在新版 iOS 13 中纠拔,這個頁面只使用了一個 UICollectionView ,你會有什么感覺泛豪。你一定很好奇它是怎么做到的稠诲。其中的秘密就是 Compositional Layout 。

介紹

Compositional Layout 是此次隨 iOS 13 一同發(fā)布的全新 UICollectionView 布局诡曙。它的目標(biāo)有三個:

  1. Composable 可組合的
  2. Flexible 靈活的
  3. Fast 快

為了達到上面這三個目標(biāo)臀叙,Compositional Layout 在原有 UICollectionViewLayout Item Section 的基礎(chǔ)上,增加了一層 Group 的概念价卤。多個 Item 組成一個 Group 劝萤,多個 Group 組成一個 Section

說了這么多慎璧,還不如上代碼

// Create a List by Specifying Three Core Components: Item, Group and Section
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                  heightDimension: .absolute(44.0))
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item]) 
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)

可以看到床嫌,為了能夠?qū)?fù)雜的布局描述清楚,我們需要創(chuàng)建多個類來分別描述 Item 胸私、 Group 厌处、 Section 的大小、間距等屬性岁疼。

如何解讀上面這段代碼阔涉?

  1. 首先 Item 的高度為44定高,寬度是父視圖(Group)寬度的 100% 捷绒。
  2. Group 的尺寸描述使用了和 Item 完全相同的的 size 瑰排,即高度為44定高,寬度是父視圖(Section)寬度的 100% 疙驾。
  3. Section 的寬度是 UICollectionView的寬度凶伙,高度默認(rèn)為其 Group 所有元素渲染出來的總高度,即 Group 的高度它碎。
  4. 最終函荣,我們會通過 Frame 或 AutoLayout對 UICollectionView 進行尺寸設(shè)置显押。

通過上面的解析,你能夠在腦中勾畫出這個 UICollectionView 長什么樣子嗎傻挂?好吧乘碑,其實我也不能,但好在我能夠跑一下代碼看下實際但結(jié)果金拒。

image

結(jié)果就是一個類似 UITableView 的布局兽肤。

好吧,我承認(rèn)這有點難绪抛。因為我們看代碼的順序都是從上而下资铡,但假如 Compositional Layout 層級的尺寸依賴于父視圖,我們就不得不結(jié)合父視圖和自身的布局來推倒出最終的布局幢码,這需要一定的空間想象力笤休。

在上面這個例子中,每一個 “UITableViewCell” 就是一個 Item 症副,也是一個 Group 店雅,而整個 “UITableViewCell” 只包含了一個 Section

所以看到這里你一定會好奇贞铣,我們?yōu)槭裁葱枰?Group 這么一個東西闹啦?很抱歉我需要將這個疑問留到最后。

核心布局

我們先來談?wù)勛罨A(chǔ)的核心布局辕坝。
在詳細(xì)介紹 Compositional Layout 中用到的四大類之前窍奋,我們需要先來了解一下,一個新的用于描述尺寸大小的類酱畅。

NSCollectionLayoutDimension

過去费变,我們可以使用 CGSize 來描述一個固定大小的 Item 。后來圣贸,我們擁有了 estimatedItemSize 來描述一個動態(tài)計算大小的 Item ,并且給它一個預(yù)估的值扛稽。但更多的時候吁峻,為了適配不同的屏幕尺寸,我們需要根據(jù)屏幕的寬度手動計算出 Item 的大性谡拧(比如限定一行只顯示3個 Item )用含。

如何用簡潔優(yōu)雅的方式去描述上面三種場景呢?答案是 NSCollectionLayoutDimension

class NSCollectionLayoutDimension {
    class func fractionalWidth(_ fractionalWidth: CGFloat) -> Self 
    class func fractionalHeight(_ fractionalHeight: CGFloat) -> Self 
    class func absolute(_ absoluteDimension: CGFloat) -> Self
    class func estimated(_ estimatedDimension: CGFloat) -> Self
}

NSCollectionLayoutDimension 添加了根據(jù)父視圖的比例來描述尺寸的 fractionalWidth / fractionalHeight 的方法帮匾,并將定值啄骇、自適應(yīng)、比例這三大描述方式統(tǒng)一分裝了起來瘟斜。

我們來看一個例子缸夹。

let size = NSCollectionLayoutDimension(widthDimension: .fractionalWidth(0.25), 
                                       heightDimension: .fractionalWidth(0.25))
}

image

如圖痪寻,使用簡單的描述,我們就可以得到以父視圖(Item 的父視圖為 Group)為基準(zhǔn)的比例尺寸虽惭。它不僅被用于描述 Item 的大小橡类,同樣也用于 Group

了解完這個基礎(chǔ)之后芽唇,讓我們看看 NSCollectionLayoutDimension 是如何在 Compositional Layout 中發(fā)揮作用的顾画。

  1. NSCollectionLayoutSize

    class NSCollectionLayoutSize {
        init(widthDimension: NSCollectionLayoutDimension,
    }
    

    單純用于描述 Item 的大小,使用到了上面介紹的 NSCollectionLayoutDimension匆笤。

  2. NSCollectionLayoutItem

    class NSCollectionLayoutItem {
        convenience init(layoutSize: NSCollectionLayoutSize)
        var contentInsets: NSDirectionalEdgeInsets
    }
    

    用于描述一個 Item 的完整布局信息研侣,包含了上面的尺寸 NSCollectionLayoutSize ,以及邊距 NSDirectionalEdgeInsets炮捧。

  3. NSCollectionLayoutGroup

    class NSCollectionLayoutGroup: NSCollectionLayoutItem { 
        class func horizontal(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self 
        class func vertical(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self 
        class func custom(layoutSize: NSCollectionLayoutSize, itemProvider: NSCollectionLayoutGroupCustomItemProvider) -> Self
    }
    

    用于描述 Group 布局庶诡。它也提供了垂直 / 水平兩種方向。同時你也可以實現(xiàn) NSCollectionLayoutGroupCustomItemProvider 自定義 Group 的布局方式寓盗。

    它同樣接收一個 NSCollectionLayoutDimension 灌砖,用于確定 Group 的大小。需要注意的是傀蚌,當(dāng) Item 使用了 fractionalWidth / fractionalHeight 時基显, Group 的大小會影響 Item 的大小。

    此外善炫,它還有一個 subitems 參數(shù)撩幽,類型為 NSCollectionLayoutItem 數(shù)組,用于傳遞 Item 箩艺。

  4. NSCollectionLayoutSection

    class NSCollectionLayoutSection {
        convenience init(layoutGroup: NSCollectionLayoutGroup) 
        var contentInsets: NSDirectionalEdgeInsets
    }
    

    用于描述 Section 布局信息窜醉。同樣可以通過修改 contentInsets 來改變 Section 的邊距。

以上就是用于描述 Compositional Layout 用到的四個類艺谆。通過對布局的精確描述榨惰,我們就能夠得到可塑性非常強的 UICollectionView布局,而無需重寫復(fù)雜的 UICollectionViewLayout 静汤。不過琅催,Compositional Layout 的可玩性還不止于此,如果想要進一步的自定義虫给,需要使用到一些額外的高級布局技巧藤抡。

高級布局

NSCollectionLayoutAnchor

對于 Item 而言,我們可能會有類似 iOS 桌面小圓點的需求抹估。通過 NSCollectionLayoutAnchor 缠黍,我們可以很容易的給 Item 添加自定義小控件。

// NSCollectionLayoutAnchor
let badgeAnchor = NSCollectionLayoutAnchor(edges: [.top, .trailing],
fractionalOffset: CGPoint(x: 0.3, y: -0.3))
let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20),
heightDimension: .absolute(20))
let badge = NSCollectionLayoutSupplementaryItem(layoutSize: badgeSize, elementKind: "badge", containerAnchor: badgeAnchor)
let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])

同樣是通過多個類來分別描述 Anchor 的方位药蜻、大小和視圖瓷式,我們就可以非常方便地為 Item 添加自定義錨替饿。

image

NSCollectionLayoutBoundarySupplementaryItem

Headers 和 Footers 是也我們經(jīng)常用到的組件,這次 Compositional Layout 弱化了 Header 和 Footer 的概念蒿往,他們都是 NSCollectionLayoutBoundarySupplementaryItem 盛垦,只不過你可以通過描述其相對于 Section 的位置(top / bottom)來達到過去 Header 和 Footer 的效果。

// NSCollectionLayoutBoundarySupplementaryItem
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: "header", alignment: .top)
let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize, elementKind: "footer", alignment: .bottom)
header.pinToVisibleBounds = true
section.boundarySupplementaryItems = [header, footer]

pinToVisibleBounds 屬性則是用來描述 NSCollectionLayoutBoundarySupplementaryItem 劃出屏幕后是否留在 CollectionView 的最上端瓤漏,也就是之前 Plain style 的 Header 樣式腾夯。

image

NSCollectionLayoutDecorationItem

有沒有遇到過這樣的UI需求?

image

以往要實現(xiàn)這樣的樣式往往會非常復(fù)雜蔬充,而如今我們終于可以自定義 Section 的背景啦蝶俱。

// Section Background Decoration Views
let background = NSCollectionLayoutDecorationItem.background(elementKind: "background")
section.decorationItems = [background]
// Register Our Decoration View with the Layout
layout.register(MyCoolDecorationView.self, forDecorationViewOfKind: "background")

通過NSCollectionLayoutDecorationItem ,我們可以為 Section 的背景添加自定義視圖饥漫,其加載方式和 Item Header Footer 一樣通過榨呆,需要先 register

Estimated Self-Sizing

在添加了如此多自定義特性之后庸队,Compositional Layout 依舊支持自適應(yīng)尺寸积蜻。這極大方便了我們對動態(tài)內(nèi)容的展示,同時對 Dynamic text 這類系統(tǒng)特性也能有更好的支持彻消。

// Estimated Self-Sizing
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44.0))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
header.pinToVisibleBounds = true
elementKind: "header",
alignment: .top)
section.boundarySupplementaryItems = [header, footer]

Nested NSCollectionLayoutGroup

不知道你有沒有發(fā)現(xiàn)竿拆,NSCollectionLayoutGroup 初始化方法中的 subitems 參數(shù)類型為 NSCollectionLayoutItem 數(shù)組,而 NSCollectionLayoutGroup 同樣繼承自 NSCollectionLayoutItem 宾尚,也就是說丙笋,NSCollectionLayoutGroup 內(nèi)可以嵌套 NSCollectionLayoutGroup 。這樣作的目的是煌贴,通過嵌套 Group 我們可以自定義出層級更加復(fù)雜的布局御板。

image

這個 Group 用代碼如何描述?

// Nested NSCollectionLayoutGroup
let leadingItem = NSCollectionLayoutItem(layoutSize: leadingItemSize) let trailingItem = NSCollectionLayoutItem(layoutSize: trailingItemSize)
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize) subitem: trailingItem, count: 2)
let containerGroup = NSCollectionLayoutGroup.horizontal(layoutSize: containerGroupSize, subitems: [leadingItem, trailingGroup])

想一想如此復(fù)雜的布局如果自己去實現(xiàn) UICollectionViewLayout 將會是多么復(fù)雜牛郑,如今通過簡潔而抽象的 Compositional Layout API 我們可以非常直觀的描述這一布局怠肋。

Orthogonal Scrolling Sections

這個特性就是我們前面提到的,讓 Section 可以滾動起來的特性淹朋。

// Orthogonal Scrolling Sections
section.orthogonalScrollingBehavior = .continuous

通過設(shè)置 Section 的 orthogonalScrollingBehavior 參數(shù)灶似,我們可以實現(xiàn)多種不同的滾動方式。

// Orthogonal Scrolling Sections
enum UICollectionLayoutSectionOrthogonalScrollingBehavior: Int {
case none
case continuous
case continuousGroupLeadingBoundary
case paging
case groupPaging
case groupPagingCentered
}

orthogonalScrollingBehavior 參數(shù)是一個 UICollectionLayoutSectionOrthogonalScrollingBehavior 類型的枚舉瑞你,包含了我們在實際開發(fā)者會用到的幾乎所有滾動方式,比如常見的自由滾動希痴,按page滾動者甲,以及按 Group 滾動(包含以 Group Leading 為邊界和以 Group Center 為邊界)。以往要實現(xiàn)類似的效果砌创,我們大多需要自己實現(xiàn) UICollectionViewLayout 或者干脆求助類似 AnimatedCollectionViewLayout 這樣的第三方庫虏缸,如今 Apple 已經(jīng)為你全部實現(xiàn)鲫懒!

image

而如果我希望做一個類似 App Store 中部這樣滾動的布局呢?

image

這會稍稍有些復(fù)雜刽辙。首先窥岩,如果你仔細(xì)閱讀文檔,你會發(fā)現(xiàn) NSCollectionLayoutGroup 有一個我們之前沒有提到的 API 宰缤。

open class func vertical(layoutSize: NSCollectionLayoutSize, subitem: NSCollectionLayoutItem, count: Int) -> Self

它相比默認(rèn)的 API 颂翼,subitem 不再接收數(shù)組而只接收單一的 Item (意味著這個模式下,Group 不支持多種大小的 ItemItem + Group 的組合慨灭,但聰明的你一定想到了可以先構(gòu)建一個組合的 Group 然后傳進這個 API 中)朦乏,同時多了一個 count。這個 count 會讓 Group 嘗試在其限定的大小內(nèi)塞入 count 個數(shù)的 Item 氧骤。最終達到的效果就是類似

let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item, item, item])

不過上面的代碼不會生效呻疹,因為 subitems 關(guān)注的是不同的 Item 的組合,而非實際 Item 的個數(shù)筹陵,因此 subitems 會對數(shù)組內(nèi)的 Item 去重刽锤。因此如果你希望在一個 Group 中塞入多個 Item,后者是你唯一的選擇朦佩。

看到這里你是否對 Group 的作用有了一點感覺并思?上面的例子中,如果我們關(guān)閉 Section 的滾動功能吕粗,那么會是什么樣子的纺荧?

image

每個 Group 中還是會有 3 個 Item,只不過由于 Section 的寬度限制颅筋,下一個 Group 不得不排布到上一個 Group 的下放宙暇,結(jié)果展示出來的還是一個類似 TableView 的布局。當(dāng)我們打開 Section 的滾動模式议泵,奇跡發(fā)生了占贫。由于 Section 可以滾動,因此它存在類似于 ScrollerView 的 ContentView 先口,它的子 View 可以在更大的范圍內(nèi)渲染型奥,因此之后的 Group 可以跟隨在之前的 Group 右側(cè),并最終填充 Section 的整個 ContentView碉京。

現(xiàn)在你該知道 Apple 為什么要引入 Group 的概念了吧厢汹。其實我在看 Advances in Collection View Layout 的時候也是悶的,直到最后看到了 App Store 的例子我才明白了谐宙,為了能夠?qū)崿F(xiàn)多緯度的滾動(實際上是賦予了 Section 滾動的特性)烫葬,原有的層級就不足以描述一個完整的多維度 CollectionView ,需要一個額外的層級來描述位于 SectionItem 的中間層。這樣說可能會略顯生澀搭综,大家可以把現(xiàn)在的 Section 想象成原來的 CollectionView 垢箕,而新的 Group 就是原來的 Section。由于現(xiàn)在 Section 充當(dāng)了之前 CollectionView 的角色被賦予了滾動的特性兑巾,因此需要一個額外的層級來描述之前 Section 所描述的 “一組 Item 的” 關(guān)系 条获。 Group 便由此出現(xiàn)。

可以說 Group 的存在是完全服務(wù)于這個可滾動 Section 的蒋歌∷Ь颍可滾動的 Section 為 CollectionView 增加了一個緯度的信息流,如果你的 CollectionView 沒有多維滾動的需求奋姿,那么你會發(fā)現(xiàn)使用 Compositional Layout 的 Group 是一個完全沒有必要的事情锄开。

復(fù)習(xí)

正如我前面所說,Compositional Layout 的層級關(guān)系依次是 Item > Group > Section > Layout 称诗。


image

理解了這其中的層級關(guān)系和特性萍悴,能夠幫助你寫出更靈活、性能更好的 UI 寓免!

總結(jié)

Compositional Layout 為我們帶來了更加可塑易用的 CollectionView 布局以及多維度瀑布流癣诱,對于 UICollectionView 而言是一個全新的升級,它將賦予 UICollectionView 更多的可能性袜香。不過限于 iOS 13 的版本限制撕予,我們還需要一段時間才能真正用上它,不過我已經(jīng)等不及了蜈首。

image

官方的Demo实抡,幾乎展示了Compositional Layout 的所有布局,支持 iOS 和 macOS欢策。強烈推薦大家跟著代碼和結(jié)果走一遍吆寨!

Using Collection View Compositional Layouts and Diffable Data Sources

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市踩寇,隨后出現(xiàn)的幾起案子啄清,更是在濱河造成了極大的恐慌,老刑警劉巖俺孙,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辣卒,死亡現(xiàn)場離奇詭異,居然都是意外死亡睛榄,警方通過查閱死者的電腦和手機荣茫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來场靴,“玉大人啡莉,你說我怎么就攤上這事。” “怎么了票罐?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長泞边。 經(jīng)常有香客問我该押,道長,這世上最難降的妖魔是什么阵谚? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任蚕礼,我火速辦了婚禮,結(jié)果婚禮上梢什,老公的妹妹穿的比我還像新娘奠蹬。我一直安慰自己,他們只是感情好嗡午,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布囤躁。 她就那樣靜靜地躺著,像睡著了一般荔睹。 火紅的嫁衣襯著肌膚如雪孔飒。 梳的紋絲不亂的頭發(fā)上燥撞,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音,去河邊找鬼宅此。 笑死,一個胖子當(dāng)著我的面吹牛箕慧,可吹牛的內(nèi)容都是我干的歉胶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼劝篷,長吁一口氣:“原來是場噩夢啊……” “哼哨鸭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起携龟,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤兔跌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后峡蟋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坟桅,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年蕊蝗,在試婚紗的時候發(fā)現(xiàn)自己被綠了仅乓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡蓬戚,死狀恐怖夸楣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤豫喧,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布石洗,位于F島的核電站,受9級特大地震影響紧显,放射性物質(zhì)發(fā)生泄漏讲衫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一孵班、第九天 我趴在偏房一處隱蔽的房頂上張望涉兽。 院中可真熱鬧,春花似錦篙程、人聲如沸枷畏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拥诡。三九已至,卻和暖如春郭厌,著一層夾襖步出監(jiān)牢的瞬間袋倔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工折柠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宾娜,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓扇售,卻偏偏與公主長得像前塔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子承冰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容