本文翻譯自 raywenderlich.com 的 NSCollectionView Tutorial,已諮詢對方網站,可至多翻譯 10 篇文章。
但願各位有英語閱讀能力的話,仍是 先打賞 而後去閱讀英文原吧,畢竟不管是 Xcode,抑或是官方的文檔,仍是各類最前沿的資訊都只有英文版本。
綜上,此翻譯版本僅供參考,謝絕轉載。macos相關連接:
NSCollectionView 進階教程:原文 / 譯文(翻譯中)
零基礎 macOS 應用開發教程(一):原文 / 譯文swift
更新信息: 此 NSCollectionView 教程已由 Gabriel Miro 更新至 Xcode 8 和 Swift 3.segmentfault
Collection View 是顯示一系列相同類型數據的最佳方式。Mac 中自帶的 Finder 和 Photos 就是使用了它:經過一個 Collection View 來展現全部的文件和圖片。設計模式
NSCollectionView
最先在 OS X 10.5 被推出,它能夠很是方便地佈局一組具備相同大小的 item,並把它們展現在一個能夠滑動的 Scroll View 中。api
在 OS X 10.11 El Capitan 中,參照 iOS 上的 UICollectionView
,NSCollectionView
被全面進行了升級。數組
macOS 10.12 Sierra 則給予了它「收起分區」(就像 Finder 裏那樣)和「固定標題」兩項新功能,使得它和 iOS 的差距進一步減少了。瀏覽器
在這個 NSCollectionView 的入門教程中,你將會創造一個叫 SlideMagic 的 app,它是一個只屬於你的網格狀的圖片瀏覽 app。app
這個教程假定你已經基本瞭解過了 macOS app 的開發,若是你還未曾瞭解過,raywenderlich.com 上提供了不少很棒的 macOS 開發教程,你能夠先去看看那些。框架
固然還有我本身翻的《零基礎 macOS 應用開發教程》系列ide
你將會編寫的 SlidesMagic app 是一個簡單的圖片瀏覽器,它很酷,可是,可別由於它太酷了而一不當心在把玩的時候把本身 Mac 上的照片刪了哦?~
這個 app 會從獲取一個文件夾裏的全部圖片,而後用一個極其優雅的 Collection View 來把它們顯示出來。完成了的 app 長這樣:
下載這個項目的起步代碼,編譯並運行:
此時,這個 app 看起來只是一個空蕩蕩的窗口,但這些起步代碼包含了一些「隱藏功能」,這是後面使它成爲一個圖片瀏覽器的基礎。
SlidesMagic 啓動的時候,會自動加載系統中 Desktop Pictures 目錄下的全部圖片,在 Xcode 的控制檯輸出中,咱們能夠看到這些文件的名字。
控制檯中輸出的列表代表,起步代碼中 Model 的加載邏輯代碼已經能夠正常工做了,你能夠在這個 app 的 File → Open Another Folder… 菜單中打開另外一個目錄。
起步代碼提供了一些與 Collection Views 無直接關聯的代碼。
這個 app 擁有兩個主要的 Controller:
WindowController.swift:
windowDidLoad()
:在左半邊的屏幕上設置主窗口的大小;openAnotherFolder(_:)
:提供一個標準的「打開」對話框來供用戶選擇文件夾;ViewController.swift:
viewDidLoad()
打開 Desktop Pictures 目錄做爲默認目錄。NSCollectionView
是今天的主角,它將會在幾個關鍵的組成部分的幫助下,顯示許多 item。
NSCollectionViewLayout
:明確了 Collection View 的佈局方式,它是一個抽象的類,全部用來表示 CollectionView 佈局的實類都繼承自它。
NSCollectionViewFlowLayout
:提供了一個靈活的網格狀的佈局。對於絕大多數 app,這種佈局方式都適用。
NSCollectionViewGridLayout
:爲了兼容 OS X 10.11 和之前的版本所保留的佈局方式,對於新建立的 app 不推薦使用。
Section 和 IndexPath
:前者容許你把 item 分紅若干個 section(分區),每一個 section 包含了一組有序的 item。每一個 item 都和一個索引相關聯,這個索引是一個由一對整數(section,item)構成的 IndexPath
實例。默認狀況下,當你不須要給 item 分區時,這個 Collection View 仍然會擁有一個 section。
就像其餘許多 Cocoa 框架同樣,Collection View 中的 item 也遵照着 MVC 設計模式。
Model 和 View:這個 item 的內容來自 Model 的數據對象。每一個單獨的對象都經過在 Collection View 中建立本身的 View 來把本身顯示出來。這些 View 的結構由一個單獨的 nib 文件(文件擴展名是 .xib)來定義。
Controller:上面提到的 nib 文件是一個由 NSCollectionViewItem 管理的 NSViewController
的子類。它負責與 Model 對象進行通訊並控制 Collection View 的顯示。一般狀況下,你會編寫一個 NSCollectionViewItem
的子類。當你須要不一樣類型的 item 的時候,你須要爲每一個分支定義一個不一樣的子類,並建立一個 nib。
要在 Collection View 中顯示不一樣於普通 item 額外的信息,你須要額外的 item。
最直觀的例子就是分區的標題和腳註。
NSCollectionViewDataSource
:用 item 和額外的 item 來填充 Collection View。NSCollectionViewDelegate
:處理拖放相關的事件,以及選中狀態和高亮。NSCollectionViewDelegateFlowLayout
:容許你自定義你的網格視圖。注意:填充一個 Collection View 的方法有二:數據源和 Cocoa 綁定。這個教程將會使用數據源。
打開 Main.storyboard。前往控件庫,向 View Controller Scene 中拖動一個 Collection View。
注意:你或許注意到了,Interface Builder 爲咱們添加了三個 View,而不是一個。這是由於 Collection View 是嵌入在一個 Scroll View 中的,然後者又自帶一個 Clip View 子視圖。這仨視圖各不相同,所以當本教程須要你選擇 Collection View 的時候,切記不要錯選了 Scroll View 或 Clip View。
調整 Bordered Scroll View 的大小,使它填滿它的父視圖的全部空間。而後選擇 Xcode 菜單欄上的 Editor → Resolve Auto Layout Issues → Add Missing Constraints 來添加 Auto Layout 約束條件。
你須要在 ViewController 中添加一個 Outlet 來訪問界面上的 Collection View。打開 ViewController.swift,在 ViewController
類的定義中添加如下代碼:
@IBOutlet weak var collectionView: NSCollectionView!
打開 Main.storyboard,並選擇 View Controller Scene 中的 View Controller。
打開鏈接檢查器,在 Outlets 部分中找到 collectionView,拖動它旁邊的小圓圈到畫布中的 Collection View 上。
你如今有兩種選擇:在 Interface Builder 中設置好主要的佈局屬性,或者在代碼中手動編寫。
在 SlidesMagic 這個項目中,咱們選擇手動編寫代碼。
打開 ViewController.swift,把這些方法添加到 ViewController
中:
fileprivate func configureCollectionView() { // 1 let flowLayout = NSCollectionViewFlowLayout() flowLayout.itemSize = NSSize(width: 160.0, height: 140.0) flowLayout.sectionInset = EdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) flowLayout.minimumInteritemSpacing = 20.0 flowLayout.minimumLineSpacing = 20.0 collectionView.collectionViewLayout = flowLayout // 2 view.wantsLayer = true // 3 collectionView.layer?.backgroundColor = NSColor.black.cgColor }
這些代碼的做用是:
NSCollectionViewFlowLayout
,配置它的基本屬性,並設置 NSCollectionView
的 collectionViewLayout
;NSCollectionView
是基於層的,因此你須要把它的父視圖的 wantsLayer
設置爲 true
;你須要在試圖加載完成時調用這個方法,因此在 viewDidLoad()
方法的最後插入:
configureCollectionView()
編譯並運行:
此時,你的 Collection View 已經擁有了一個黑色的背景,並配置好了佈局。
先在你須要建立一個 NSCollectionViewItem
的子類並把 Model 裏的數據們顯示出來。
點擊 Xcode 菜單欄上的 File → New → File…,選擇 macOS → Source → Cocoa Class 並點擊 Next。
把 Class 填寫 CollectionViewItem,Subclass of 填寫 NSCollectionViewItem
,並勾選 Also create XIB for user interface。
點擊 Next,而後在對話框中的 Group 中選擇 Controllers,並點擊 Create。
打開 CollectionViewItem.swift,把裏邊的內容所有替換爲:
import Cocoa class CollectionViewItem: NSCollectionViewItem { // 1 var imageFile: ImageFile? { didSet { guard isViewLoaded else { return } if let imageFile = imageFile { imageView?.image = imageFile.thumbnail textField?.stringValue = imageFile.fileName } else { imageView?.image = nil textField?.stringValue = "" } } } // 2 override func viewDidLoad() { super.viewDidLoad() view.wantsLayer = true view.layer?.backgroundColor = NSColor.lightGray.cgColor } }
這些代碼的功能是:
imageFile
屬性,用來訪問須要展現的 Model 對象。當你爲 imageFile
屬性賦值時,它的 didSet
屬性觀察器會設置這個 item 的 Image 和 Label;你在 CollectionViewItem.swift 時勾選了「Also create a XIB(同時建立一個 XIB)」,爲了更清楚地整理文件,把 CollectionViewItem.xib 拖動到 Main.storyboard 下方的 Resources 分組中。
Nib 文件中的 View 就是每一個 item 所顯示出來的根視圖,你須要添加一個 Image View 來顯示圖片,以及一個 Label 來顯示文件名。
打開 CollectionViewItem.xib,添加一個 NSImageView
:
再來添加一個 Label:
選中 Label,在屬性檢查器中設置以下屬性:
儘管 Nib 文件的 File’s Owner 如今是 CollectionViewItem
,它還只是一個佔位符。當 Nib 文件被實例化時,它還會須要一個「真正的」NSCollectionViewItem
的實例。
從控件庫中拖動一個 Collection View Item 到文檔大綱中,選中它,在 身份檢查器中把它的 Class 設置爲 CollectionViewItem
。
在 xib 中,你須要把 View 的層次關係鏈接到 CollectionViewItem 的 Outlet 中,在 CollectionViewItem.xib 中:
view
的 outlet 拖動到文檔大綱中的 View 上;imageView
和 textField
的 outlet 鏈接到文檔大綱中的 Image View 和 Label 中。你須要實現 Collection View 的數據源協議,說人話就是:
打開 ViewController.swift 並在文件的末尾添加這些擴展代碼:
extension ViewController : NSCollectionViewDataSource { // 1 func numberOfSections(in collectionView: NSCollectionView) -> Int { return imageDirectoryLoader.numberOfSections } // 2 func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return imageDirectoryLoader.numberOfItemsInSection(section) } // 3 func collectionView(_ itemForRepresentedObjectAtcollectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { // 4 let item = collectionView.makeItem(withIdentifier: "CollectionViewItem", for: indexPath) guard let collectionViewItem = item as? CollectionViewItem else {return item} // 5 let imageFile = imageDirectoryLoader.imageFileForIndexPath(indexPath) collectionViewItem.imageFile = imageFile return item } }
NSCollectionViewDataSource
協議必須實現的方法之一,在這個方法中你須要返回某個分區容納的 item 的數量;indexPath
返回一個 item;identifier
參數,它會根據所須要的 item 的類型來試圖重複使用一個 item,若是沒有 item 可供重複使用,它會新建一個 item;IndexPath
獲取 Model 對象,設置 Image 和 Label 的內容。注意: Collection View 具備一項能力:循環使用已經生成了的 Item,以此來減輕數據源過大時的內存壓力。從界面上移出去的 item 就是被重複使用的 item。
接下來我發須要定義數據源:
打開 Main.storyboard,選中 Collection View。
打開鏈接檢查器,在 Outlets 部分中找到 dataSource,拖動它旁邊的小圓圈到文檔大綱裏的 View Controller 上。
編譯並運行,你的 Collection View 如今應該能顯示 Desktop Pictures 目錄中的圖片了:
哈哈哈,折騰了半天都是值得的✌️~
若是你還看不見任何圖片,你可能遺漏了一些小細節:
dataSource
的 Outlet 了嗎?NSCollectionViewItem
,並把它的類設置爲 CollectionViewItem
了嗎?makeItemWithIdentifier
中的 identifier
參數的值和 nib 的名字同樣嗎?要顯示另外一個目錄中的圖片,你能夠在 app 的菜單欄上點擊 File → Open Another Folder…,而後選擇一個存有 JPG 或 PNG 格式的圖片的目錄。
但時此時窗口中的東西彷佛什麼變化都沒有,仍是顯示着 Desktop Pictures 目錄中的圖片。儘管 Xcode 裏的控制檯中已經打印出了新目錄裏的文件名稱。
你須要調用 Collection View 的 reloadData()
方法來刷新它的 item。
打開 ViewController.swift 並把這些代碼添加到 loadDataForNewFolderWithUrl(_:)
方法中:
collectionView.reloadData()
編譯並運行,如今你應該能看到窗口中已經能顯示正確的照片了。
SlidesMagic app 如今已經能夠作一些很神奇的事兒了,但咱們要更進一步 —— 爲 Collection View 加入分區。
首先,你須要在主視圖的最底部加入一個複選框,來容許你切換是否啓用分組。
打開 Main.storyboard,而後在文檔大綱中選中 Scroll View 的約束條件,在尺寸檢查器中吧它的 Constant 修改成 30.
這會把 Collection View 擡高一些些,騰出地方來放置複選框。
如今,從控件庫中拖動一個 Check Box Button到畫布中 Collection View 下方的空間裏,選中它,在屬性檢查器中把它的 Title 設置爲 Show Sections,而後把 State 設置爲 Off。
接下來,點擊 Pin 按鈕更新它的 Auto Layout 約束條件:Top 設置爲 8,Leading 設置爲 20。而後點擊 Update Frames: Items of New Constraints 和 Add 2 Constraints
編譯並運行,如今 app 的底部看起來應該是這樣的:
當你點擊這個複選框的時候,你的 app 須要改變 Collection View 的外觀。
打開 ViewController.swift 在 ViewController
類的最後添加這些代碼:
@IBAction func showHideSections(sender: NSButton) { let show = sender.state // 1 imageDirectoryLoader.singleSectionMode = (show == NSOffState) // 2 imageDirectoryLoader.setupDataForUrls(nil) // 3 collectionView.reloadData() }
這些代碼會:
nil
參數表示跳過圖像加載 —— 畢竟圖片仍是那些圖片,只是佈局發生了改變;若是你好奇圖片是按照是按照什麼規則進行分組的,在 ImageDirectoryLoader
中找到 sectionLengthArray
,這個數組裏的數字設置了各個分組的裏最多能夠容納多少個 item。這個數組是隨機生成的,只是用來用做演示。
如今,打開 Main.storyboard。在文檔大綱中按住 Control⌃ 鍵的同時把 Show Sections 拖動到 View Controller 上。在彈出的黑色窗口中點擊 showHideSections:。你能夠在鏈接檢查器裏查看你是否鏈接成功了。
編譯並運行,勾選 Show Sections 來查看佈局的變化。
爲了更好地區分各個分區,打開 ViewController.swift,編輯 configureCollectionView()
方法裏的 sectionInset
屬性。
把這一行:
flowLayout.sectionInset = EdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0)
替換成這個:
flowLayout.sectionInset = EdgeInsets(top: 30.0, left: 20.0, bottom: 30.0, right: 20.0)
編譯並運行,勾選 Show Sections,能夠看到各個分區之間已經有了分隔。
另外一種區分各個分區邊界的方法是爲每一個分區添加一個標題或腳註。
你須要一個自定義的 NSView
類,並實現相應的數據源方法來爲 Collection View 添加一個標題
要建立一個標題,在 Xcode 的菜單欄點擊 File → New → File…。選擇 macOS → User Interface → View,並點擊 Next。
文件名輸入 HeaderView.xib,Group 選擇 Resources。
點擊 Create。
打開 HeaderView.xib 並選中 Custom View。在尺寸檢查器中把 Width 設置爲 500,Height 設置爲 40。
從 Object Library 拖動一個 Label 到 Custom View 的左半邊。打開屬性檢查器,設置它的 Title 爲 Section Number,設置 Font Size 爲 16。
再拖動一個 Label 到 Custom View 的右半邊。設置它的 Title 爲 Image Count,設置 Alignment 爲 Right。
選中 Section Number Label,點擊 Pin 按鈕,設置它的 Top 約束爲 12,Leading 約束爲 20。點擊 Update Frames: Items of New Constraints 和 Add 2 Constraints。
接下來,設置 Image Count Label 的 Top 約束爲 11,Trailing 約束爲 20,別忘了點擊 Update Frames: Items of New Constraints 和 Add 2 Constraints。
如今咱們的標題應該看起來像這樣:
如今咱們的標題 UI 已經準備好了,咱們還須要爲它建立一個子類。
在 Xcode 的菜單欄點擊 File → New → File…。選擇 macOS → Source → Cocoa Class,並點擊 Next。把它的類名設置爲 HeaderView
,並讓它繼承自 NSView
,點擊 Next,並在 Group 中選擇 Views。點擊 Create。
打開 HeaderView.swift 而後把裏邊的全部內容替換爲:
// 1 @IBOutlet weak var sectionTitle: NSTextField! @IBOutlet weak var imageCount: NSTextField! // 2 override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) NSColor(calibratedWhite: 0.8 , alpha: 0.8).set() NSRectFillUsingOperation(dirtyRect, NSCompositingOperation.sourceOver) }
這裏的代碼作了這些事兒:
要把 outlet 鏈接至 Label,打開 HeaderView.xib 並選中 Custom View。在 Identity Inspector 中把 Class 設置爲 HeaderView。
在文檔大綱視圖中,按住 Control⌃ 鍵的同時點擊 Header View。在彈出的黑色窗口中,拖動 imageCount 到 Images Count 上來鏈接 outlet。
對第二個 Label 進行一樣的操做,拖動 sectionTitle 到畫布中的 Section Number Label 上。
你的標題已經徹底準備好上戰場了,你須要實現 collectionView(_:viewForSupplementaryElementOfKind:at:)
,把這個標題視圖傳遞給 Collection View:
打開 ViewController.swift 並把這些方法添加到 NSCollectionViewDataSource
extension 中:
func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> NSView { // 1 let view = collectionView.makeSupplementaryView(ofKind: NSCollectionElementKindSectionHeader, withIdentifier: "HeaderView", for: indexPath) as! HeaderView // 2 view.sectionTitle.stringValue = "Section \(indexPath.section)" let numberOfItemsInSection = imageDirectoryLoader.numberOfItemsInSection(indexPath.section) view.imageCount.stringValue = "\(numberOfItemsInSection) image files" return view }
Collection View 會在它須要數據源的時候調用這個方法,併爲每一個分區設置標題。這個方法作了這些:
makeSupplementaryViewOfKind(_:withIdentifier:for:)
來從 nib 文件實例化一個名字是 withIdentifier
的 HeaderView
對象;在 ViewController.swift 的最後,添加這個 NSCollectionViewDelegateFlowLayout
擴展:
extension ViewController : NSCollectionViewDelegateFlowLayout { func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> NSSize { return imageDirectoryLoader.singleSectionMode ? NSZeroSize : NSSize(width: 1000, height: 40) } }
上面這個方法實際上是不是必須的,但當你須要設置標題的時候就必須寫上了,由於 Flow Layout(流式佈局)的代理須要你提供各個分區的標題的大小。
若是沒有實現這個方法,你將看不到標題,由於它們的尺寸都是 0。此外,它還會忽略你指定的寬度,而是把標題的寬度設置爲 Collection View 的寬度。
在這個例子中,當 Collection View 只有一個分區的時候,這個方法返回的標題尺寸是 0,不然他會返回 40.
對於使用了 NSCollectionViewDelegateFlowLayout
的 Collection View,你須要把 ViewController
鏈接到 NSCollectionView
的 delegate
。
打開 Main.storyboard 並選中 Collection View。打開鏈接檢查器,在 Outlets 部分中找到 delegate。拖動他旁邊的小圓點到文檔大綱中的 View Controller 上。
編譯並運行,勾選 Show Sections,能夠看到一個個標題把分區區分開來:
macOS 10.12 中的 NSCollectionViewFlowLayout
新加入了兩個屬性:sectionHeadersPinToVisibleBounds
和 sectionFootersPinToVisibleBounds
。
當 sectionHeadersPinToVisibleBounds
設置爲 true
,最上端的分區的標題將會固定在頂端,而不會移出界面之外。當你繼續向下滾動時,下一個標題會把它頂走。這種效果通常被稱爲「sticky headers(固定標題)」或「floating headers(浮動標題)」。
把 sectionFootersPinToVisibleBounds
設置爲 true
則會把腳註固定在底部。
打開 ViewController.swift,在 configureCollectionView()
方法的底部加入這個方法:
flowLayout.sectionHeadersPinToVisibleBounds = true
編譯並運行,勾選 Show Sections 並向下滾動一些,你能夠看到第一個區域已經有一些圖片被移出屏幕了,但標題仍是固定在頂部:
注意:若是你的 app 須要支持 OS X 10.11 或更老的版本,你須要經過重寫
layoutAttributesForElements(in:)
方法來「手動」實現固定標題。你能夠查看[Advanced Collection Views in OS X Tutorial]這篇教程(我正在翻譯~)。
爲了顯示一個 item 的被選中狀態,你須要設置一個白色的邊框,沒有被選中的項目將不會顯示這個邊框。
首先,你須要讓咱們的 Collection View 支持選中。打開 Main.storyboard,選中 Collection View 並在屬性檢查器裏,勾選 Selectable。
勾選 Selectable 開啓了選擇功能,意味着你能夠經過點擊一個 item 來選中它。若是你點擊另外一個 item,將會取消選擇以前的那個 item 並選中新的 item。
當你選中一個 item:
IndexPath
會被添加到 NSCollectionView
的 selectionIndexPaths
屬性;isSelected
屬性會被設置爲 true
。打開 CollectionViewItem.swift。在 viewDidLoad()
方法的最後追加:
// 1 view.layer?.borderColor = NSColor.white.cgColor // 2 view.layer?.borderWidth = 0.0
這段代碼:
borderWidth
設置爲 0.0
來確保邊框不可見 —— 也就是沒被選中。要在每次 isSelected
被設置時改變 borderWidth
,咱們須要把這些代碼添加到 CollectionViewItem
類中:
override var isSelected: Bool { didSet { view.layer?.borderWidth = isSelected ? 5.0 : 0.0 } }
每次 isSelected
發生了改變,didSet
將會根據新的值來設置邊框的寬度。
編譯並運行。點擊一個項目來選中它,你將會看見它周圍出現了邊框。哈哈哈,神奇✨!
點擊這裏下載最終完成了的 SlideMagic。
在這個 NSCollectionView 入門教程中,你瞭解瞭如何建立你的第一個 Collection View,瞭解了錯綜複雜的數據源 API 和如何處理分區。至此你已經學到了不少,但其實這僅僅是個開始,Collection View 還有不少功能等待你去發掘。這裏有不少值得去探索的東西:
NSCollectionViewFlowLayout
你能夠在咱們的《NSCollectionView 進階教程》(原文|譯文)中瞭解更多。