iOS13 Compositional Layout

前言

UITableView 和 UICollectionView 是咱們開發者最經常使用的控件了,大量的流式佈局須要這兩個控件來實現,所以這兩個控件也是 Apple 重點優化的對象。在往屆 WWDC 中,咱們已經受益於 UITableViewDataSourcePrefetching 、優化版 Autolayout 等帶來的性能提高,以及 UITableViewDragDelegate 帶來的原生拖拽功能。今年,Apple 帶來了全新的 Compositional Layout 。它將完全顛覆 UICollectionView 的佈局體驗,大大拓展 UICollectionView 的可塑性。數組

背景

早期的 App 設計相對簡單,使用 UICollectionViewFlowLayout 能夠應付大多數使用場景。而隨着應用的發展,愈來愈多的頁面趨於複雜化,UICollectionViewFlowLayout 在面對複雜佈局每每會顯得力不從心,或者很是複雜,須要進行大量的計算和判斷。而自由度更高的 UICollectionViewLayout 則有着更高的接入門檻,稍有不慎還容易出現各類各樣的 bug 。 app

咱們就拿 App Store爲例,它包含了大小不一的 Item ,以及能夠上下、左右滑動的交互。假如你是開發者,你會如何搭建這個 UI ?你可能會使用多個 UICollectionView 嵌套在一個 UIScrollerView 中,由於 UICollectionView 的滾動軸只能有一個(橫向 / 豎向)。但若是我告訴你,在新版 iOS 13 中,這個頁面只使用了一個 UICollectionView ,你會有什麼感受。你必定很好奇它是怎麼作到的。其中的祕密就是 Compositional Layout 。

介紹

Compositional Layout 是這次隨 iOS 13 一同發佈的全新 UICollectionView 佈局。它的目標有三個:ide

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

爲了達到上面這三個目標,Compositional Layout 在原有 UICollectionViewLayout Item Section 的基礎上,增長了一層 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)
複製代碼

能夠看到,爲了可以將複雜的佈局描述清楚,咱們須要建立多個類來分別描述 ItemGroupSection 的大小、間距等屬性。fetch

如何解讀上面這段代碼?優化

  1. 首先 Item 的高度爲44定高,寬度是父視圖(Group)寬度的 100% 。
  2. Group 的尺寸描述使用了和 Item 徹底相同的的 size ,即高度爲44定高,寬度是父視圖(Section)寬度的 100% 。
  3. Section 的寬度是 UICollectionView的寬度,高度默認爲其 Group 全部元素渲染出來的總高度。
  4. 最終,咱們會經過 Frame 或 AutoLayout 對 UICollectionView 進行尺寸設置。

經過上面的解析,你可以在腦中勾畫出這個 UICollectionView 長什麼樣子嗎?好吧,其實我也不能,但好在我可以跑一下代碼看下實際但結果。動畫

結果就是一個相似 UITableView 的佈局。

好吧,我認可這有點難。由於咱們看代碼的順序都是從上而下,但假如 Compositional Layout 層級的尺寸依賴於父視圖,咱們就不得不結合父視圖和自身的佈局來推倒出最終的佈局,這須要必定的空間想象力。ui

在上面這個例子中,每個 「UITableViewCell」 就是一個 Item ,也是一個 Group ,而整個 「UITableViewCell」 只包含了一個 Sectionspa

因此看到這裏你必定會好奇,咱們爲何須要 Group 這麼一個東西?請保持耐心,要解答這個問題須要看到留到最後。

核心佈局

咱們先來談談最基礎的核心佈局。 在詳細介紹 Compositional Layout 中用到的四大類以前,咱們須要先來了解一下,一個新的用於描述尺寸大小的類。

NSCollectionLayoutDimension

過去,咱們可使用 CGSize 來描述一個固定大小的 Item 。後來,咱們擁有了 estimatedItemSize 來描述一個動態計算大小的 Item ,而且給它一個預估的值。但更多的時候,爲了適配不一樣的屏幕尺寸,咱們須要根據屏幕的寬度手動計算出 Item 的大小(好比限定一行只顯示3個 Item )。

如何用簡潔優雅的方式去描述上面三種場景呢?答案是 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 添加了根據父視圖的比例來描述尺寸的 fractionalWidth / fractionalHeight 的方法,並將定值、自適應、比例這三大描述方式統一分裝了起來。

咱們來看一個例子。

let size = NSCollectionLayoutDimension(widthDimension: .fractionalWidth(0.25), 
                                       heightDimension: .fractionalWidth(0.25))
}
複製代碼

如圖,使用簡單的描述,咱們就能夠獲得以父視圖( Item 的父視圖爲 Group)爲基準的比例尺寸。它不只能夠被用於描述 Item 的大小,一樣也能夠用於 Group

瞭解完這個基礎以後,讓咱們看看 NSCollectionLayoutDimension 是如何在 Compositional Layout 中發揮做用的。

  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 佈局。它提供了垂直 / 水平兩種方向。同時你也能夠實現 NSCollectionLayoutGroupCustomItemProvider 自定義 Group 的佈局方式。

    它一樣接收一個 NSCollectionLayoutDimension ,用於肯定 Group 的大小。須要注意的是,當 Item 使用了 fractionalWidth / fractionalHeight 時, Group 的大小會影響 Item 的大小。

    此外,它還有一個 subitems 參數,類型爲 NSCollectionLayoutItem 數組,用於傳遞 Item

  4. NSCollectionLayoutSection

    class NSCollectionLayoutSection {
        convenience init(layoutGroup: NSCollectionLayoutGroup) 
        var contentInsets: NSDirectionalEdgeInsets
    }
    複製代碼

    用於描述 Section 佈局信息。一樣能夠經過修改 contentInsets 來改變 Section 的邊距。

以上就是用於描述 Compositional Layout 用到的四個類。經過對佈局的精確描述,咱們就可以獲得可塑性很是強的 UICollectionView 佈局,而無需重寫複雜的 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 添加自定義錨。

NSCollectionLayoutBoundarySupplementaryItem

Headers 和 Footers 是也咱們常常用到的組件,此次 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 樣式。

NSCollectionLayoutDecorationItem

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

以往要實現這樣的樣式每每會很是複雜,而現在咱們終於能夠自定義 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 依舊支持自適應尺寸。這極大方便了咱們對動態內容的展現,同時對 Dynamic text 這類系統特性也能有更好的支持。

// 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

不知道你有沒有發現,NSCollectionLayoutGroup 初始化方法中的 subitems 參數類型爲 NSCollectionLayoutItem 數組,而 NSCollectionLayoutGroup 一樣繼承自 NSCollectionLayoutItem ,也就是說,NSCollectionLayoutGroup 內能夠嵌套 NSCollectionLayoutGroup 。這樣做的目的是,經過嵌套 Group 咱們能夠自定義出層級更加複雜的佈局。

這個 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])
複製代碼

想想如此複雜的佈局若是本身去實現 UICollectionViewLayout 將會是多麼複雜,現在經過簡潔而抽象的 Compositional Layout API 咱們能夠很是直觀的描述這一佈局。

Orthogonal Scrolling Sections

這個特性就是咱們前面提到的,讓 Section 能夠滾動起來的特性。

// Orthogonal Scrolling Sections
section.orthogonalScrollingBehavior = .continuous
複製代碼

經過設置 Section 的 orthogonalScrollingBehavior 參數,咱們能夠實現多種不一樣的滾動方式。

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

orthogonalScrollingBehavior 參數是一個 UICollectionLayoutSectionOrthogonalScrollingBehavior 類型的枚舉,包含了咱們在實際開發者會用到的幾乎全部滾動方式,好比常見的自由滾動,按page滾動,以及按 Group 滾動(包含以 Group Leading 爲邊界和以 Group Center 爲邊界)。以往要實現相似的效果,咱們大多須要本身實現 UICollectionViewLayout 或者乾脆求助相似 AnimatedCollectionViewLayout 這樣的第三方庫,現在 Apple 已經爲你所有實現!

而若是我但願作一個相似 App Store 中部這樣滾動的佈局呢?

這會稍稍有些複雜。首先,若是你仔細閱讀文檔,你會發現 NSCollectionLayoutGroup 有一個咱們以前沒有提到的 API 。

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

它相比默認的 API ,subitem 再也不接收數組而只接收單一的 Item (意味着這個模式下,Group 不支持多種大小的 ItemItem + Group 的組合,但聰明的你必定想到了能夠先構建一個組合的 Group 而後傳進這個 API 中),同時多了一個 count。這個 count 會讓 Group 嘗試在其限定的大小內塞入 count 個數的 Item 。最終達到的效果就是相似

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

不過上面的代碼不會生效,由於 subitems 關注的是不一樣的 Item 的組合,而非實際 Item 的個數,所以 subitems 會對數組內的 Item 去重。所以若是你但願在一個 Group 中塞入多個 Item,後者是你惟一的選擇。

看到這裏你是否對 Group 的做用有了一點感受?上面的例子中,若是咱們關閉 Section 的滾動功能,那麼會是什麼樣子的?

每一個 Group 中仍是會有 3 個 Item,只不過因爲 Section 的寬度限制,下一個 Group 不得不排布到上一個 Group 的下放,結果展現出來的仍是一個相似 TableView 的佈局。當咱們打開 Section 的滾動模式,奇蹟發生了。因爲 Section 能夠滾動,所以它存在相似於 ScrollerView 的 ContentView ,它的子 View 能夠在更大的範圍內渲染,所以以後的 Group 能夠跟隨在以前的 Group 右側,並最終填充 Section 的整個 ContentView。

如今你該知道 Apple 爲何要引入 Group 的概念了吧。其實我在看 Advances in Collection View Layout 的時候也是悶的,直到最後看到了 App Store 的例子我才明白了,爲了可以實現多緯度的滾動(其實是賦予了 Section 滾動的特性),原有的層級就不足以描述一個完整的多維度 CollectionView ,須要一個額外的層級來描述位於 SectionItem 的中間層。這樣說可能會略顯生澀,你們能夠把如今的 Section 想象成原來的 CollectionView ,而新的 Group 就是原來的 Section。因爲如今 Section 充當了以前 CollectionView 的角色被賦予了滾動的特性,所以須要一個額外的層級來描述以前 Section 所描述的 「一組 Item 的」 關係 。 Group 便由此出現。

能夠說 Group 的存在是徹底服務於這個可滾動 Section 的。可滾動的 Section 爲 CollectionView 增長了一個緯度的信息流,若是你的 CollectionView 沒有多維滾動的需求,那麼你會發現 Compositional Layout 中 Group 的存在是一個徹底沒有必要的事情。

複習

正如我前面所說,Compositional Layout 的層級關係依次是 Item > Group > Section > Layout

理解了這其中的層級關係和特性,可以幫助你寫出更靈活、性能更好的 UI !

總結

Compositional Layout 爲咱們帶來了更加可塑易用的 CollectionView 佈局以及多維度瀑布流,對於 UICollectionView 而言是一個全新的升級,它將賦予 UICollectionView 更多的可能性。一個注意的點是,iOS 13上的 App Store 已經用上了新的 Compositional Layout ,不過在 iPad 上旋轉動畫的性能不是很好,可見目前版本的 Compositional Layout 還有待優化的控件。不過限於 iOS 13 的版本限制,咱們還須要一段時間才能真正用上它,但我已經等不及了。

官方的Demo,幾乎展現了Compositional Layout 的全部佈局,支持 iOS 和 macOS。強烈推薦你們跟着代碼和結果走一遍!

Using Collection View Compositional Layouts and Diffable Data Sources

相關文章
相關標籤/搜索