【譯】MacOS NSTableView 教程

原文

表視圖是 macOS 應(yīng)用程序中最普遍的控件之一,熟悉的例子如 Mail 的消息列表和 Spotlight 的搜索結(jié)果。它能以吸引人的方式表現(xiàn)表格數(shù)據(jù)。

NSTableView 將數(shù)據(jù)按行列排列。每一行代表數(shù)據(jù)集合中的一個模型對象,每一列顯示模型對象的一個屬性。

在這個教程里,你將創(chuàng)建一個和 Finder 非常相似的文件瀏覽器。完成這個教程你將學(xué)到很多關(guān)于 table view 的知識。比如:

  • 怎樣填充表視圖
  • 怎樣改變視覺樣式
  • 怎樣響應(yīng)用戶交互,比如選擇和雙擊

準(zhǔn)備好創(chuàng)建你的第一個表視圖了嗎?往下看!

準(zhǔn)備開始

下載 啟動項(xiàng)目,在 Xcode 中打開它。

編譯運(yùn)行,看看開始是什么樣子:


image.png

你有了一個用于創(chuàng)建很酷的文件瀏覽器的空白畫布,起始 APP 已經(jīng)有了一些本教程需要用到的功能。

打開應(yīng)用程序,選擇 File > Open… (或者使用 Command+O 快捷鍵)

image.png

在新打開的彈出窗口中選擇你想打開的文件夾,點(diǎn)擊 Open 按鈕。你將在 Xcode 控制面板中看到:

Represented object: file:///Users/tutorials/FileViewer/FileViewer/ 

這條消息顯示了所選擇文件夾的路徑,程序代碼會把這個 URL 傳遞給視圖控制器。

如果你感到好奇,想學(xué)習(xí)這些是怎么實(shí)現(xiàn)的,你應(yīng)該看看下面這些:

  • Directory.swift:包含了 Directory 結(jié)構(gòu)體的實(shí)現(xiàn),用來讀取文件夾中的內(nèi)容。
  • WindowController.swift:包含了呈現(xiàn)選擇文件夾面板的代碼,并將選擇的文件夾傳遞個 ViewController。
  • ViewController.swift):包含了 ViewController 的實(shí)現(xiàn),這里是你要話費(fèi)時間的地方,這也是你創(chuàng)建表視圖和顯示文件列表的地方。

創(chuàng)建表視圖

打開故事板,選擇 View Controller Scene,從對象庫里拖一個 table view 到視圖里。在視圖層級里有一個叫 Table Container 的容器。

image.png

下一步需要添加一些約束。點(diǎn)擊自動布局工具條上的 Pin 按鈕。在彈出窗口中選擇邊緣約束如下:

  • Top, Bottom, Leading and Trailing:0


    image.png

確保 Update Frames 設(shè)置為 Items of New Constraints,點(diǎn)擊 Add 4 Constraints。
看一下新創(chuàng)建的表視圖的機(jī)構(gòu)。就像名稱顯示的一樣,它通常有下面這樣的機(jī)構(gòu):

  • 它由行和列構(gòu)成
  • 每一行代表數(shù)據(jù)模型集合中的一個條目
  • 每一列代表數(shù)據(jù)模型中的一個屬性
  • 每一寫都有一個表頭
  • 表頭對數(shù)據(jù)列進(jìn)行描述
image.png

如果你對 iOS 中的 UITableView 屬性,那你算是踏進(jìn)了熟悉的水域了,不過 macOS 里更深。實(shí)際上,你可能對構(gòu)造一個 NSTableView 時對象層級里需要構(gòu)造的 UI 對象的數(shù)量感到吃驚。

UITableView 是一個比 UITableView 更老更復(fù)雜的控件。它提供了不同的用戶接口范式,特別是用戶使用鼠標(biāo)可觸控板時。

UITableView 的主要不同是,它有多列的可能,表頭可用來和有何進(jìn)行交互,比如選擇和排序。

同時 NSScrollViewNSClipView 分別用來滾動和剪裁 NSTableView 的內(nèi)容。有兩個 NSScroller 分別用于垂直和水平滾動。還有許多列對象。NSTableView** 擁有許多列對象,這些列都含有標(biāo)題頭部。用戶可以調(diào)整列的大小和尺寸很重要,盡管你可以將其設(shè)置為默認(rèn)禁用。

解剖 NSTableView

Interface Builder 里,你已經(jīng)看到了表視圖層級的復(fù)雜性了。多個類配合起來構(gòu)建表視圖。最終看起來像這樣:

image.png

這是 NSTableView 的關(guān)鍵部分:

  • Header View:頭部視圖是 NSTableHeaderView 類的實(shí)例。它負(fù)責(zé)在表格的頂部畫表頭。如果你需要自定義的頭部,可以子類化這個類。
  • Row View:行視圖顯示與表中每一行相關(guān)的可視屬性,例如選擇高亮。顯示在表中的每一行都有自己的 Row view 實(shí)例。一個重要的區(qū)別是行不代表你的數(shù)據(jù),那是 Cell 的職責(zé)。它只處理可視屬性,比如選擇顏色和分割符。你可以創(chuàng)建新的行子類來定義不同的表格樣式。
  • Cell Views:Cell 可能是表視圖中最重要的對象。在行和列的交叉點(diǎn)可以找到 Cell,每一個 Cell 都是 NSView 或者 NSTableCellView。它的作用是顯示最終的數(shù)據(jù)。你可以創(chuàng)建自定義的 Cell 類來按自己喜歡的方式顯示內(nèi)容。
  • Column:列被 NSTableViewColumn 類表示,它的任務(wù)是管理列的寬度和行為,比如調(diào)整大小和位置。這不是一個視圖,而是一個控件。你用它來指定列的行為,但是不能控制視覺樣式,因?yàn)楸眍^、行、單元視圖已經(jīng)負(fù)責(zé)這些了。

注意:有兩種模式的 NSTableView。第一種是基于 cell 的,叫 NSCell。它像 NSView,但是更年久更輕量。它是在桌面系統(tǒng)需要以最小花銷繪制控件時出現(xiàn)的。
蘋果建議使用基于 View 的表視圖,但是在 AppKit 中你將看到很多 NSCell,所以,它是什么,它是怎么出現(xiàn)的值得看一下。你可以在蘋果的 Control and Cell Programming Topics 了解更多關(guān)于 NSCell

以上是表視圖結(jié)構(gòu)背后的基本原理?,F(xiàn)在知道了這些,是時候回到 Xcode 在自己的表視圖上做些事情了。

玩弄表視圖的列

Interface Builder 創(chuàng)建表視圖時默認(rèn)是兩列,但是你需要三列來顯示文件的名稱、日期和大小。

回到 Main.storyboard。
選擇 View Controller Scene 里的表視圖。確保你選的是表視圖,而不是包含它的滾動視圖:

image.png

打開屬性檢查器。將 Columns 改為 3。就這么簡單,現(xiàn)在你的表視圖有 3 列了。

下一步,選擇 Selection 部分的 Multiple 復(fù)選框,因?yàn)槟阆M淮芜x擇多個文件。同樣選擇 Highlight 部分的 Alternating Rows 復(fù)選框。啟用這一項(xiàng),是用來告訴表視圖使用交替的行背景顏色,就像 Finder 中一樣。

image.png

從命名行的表頭,使文本更具有描述性。選擇 View Controller Scene 中的第一列。

image.png

打開屬性檢查器,將列的 Title 改為 Name。

image.png

重復(fù)相同的操作,將第二列、第三列的標(biāo)題分別改為 Modification Date 和 Size。

注意:改變列的標(biāo)題有一種替代的方法。你可以雙擊表頭讓它變成可編輯。兩種方法最終結(jié)果是一樣的,所以隨便選一種你喜歡的。

最后,如果你沒有看到尺寸列,選擇 Modification Date 列,大小改為 200。

image.png

編譯運(yùn)行,你應(yīng)該看到下面這樣:

image.png

改變信息的表現(xiàn)方式

在當(dāng)前狀態(tài)下,Table View 有三列,每一個列都包含一個 Cell View,其中有一個文本框用來顯示文本。

它有點(diǎn)單調(diào),給它增加點(diǎn)趣味,在文件名的旁邊顯示文件的圖標(biāo)。這一點(diǎn)提升之后,表格看起來更干凈了。

你需要用一個包含圖片和文本框的新的 Cell 類型來替換第一列中的 Cell。很幸運(yùn),Interface Builder 提供了一個這樣的內(nèi)置 Cell 類型。

選擇 Name 列中的 Table Cell View 刪除它。

image.png

打開對象庫,拖動一個 ** Image & Text Table Cell View into** 到表視圖的第一個列里面,或者拖到 *View Controller Scene tree 里面,剛好放到 Name 列下面。

image.png

現(xiàn)在東西快要成型了!

賦予標(biāo)識符(Assigning Identifiers)

每一個 Cell 類型都需要一個標(biāo)示符。否則編寫代碼時,你不能創(chuàng)建一個關(guān)聯(lián)到指定列的 Cell 視圖。

選擇第一列中的 cell view,在 Identity Inspector 里將 Identifier 改為 NameCellID

image.png

用同樣的方法將第二個、第三個的標(biāo)識符分別設(shè)置為DateCellIDSizeCellID

填充表格

注意:你可以用兩種方法來填充表格。一種是使用數(shù)據(jù)源和委托協(xié)議,這是你將在本教程中看到的方法;或者使用 Cocoa bindings。這兩種方法不是互斥的,有時你會同時使用它們來得到你想要的。

表視圖現(xiàn)在還不知道任何你想要顯示的數(shù)據(jù)以及怎么顯示它。你將實(shí)現(xiàn)這兩個協(xié)議來提供這些信息:

  • NSTableViewDataSource:告訴 Table View 它需要呈現(xiàn)多少行。
  • NSTableViewDelegate: 提供用來顯示指定行列的 Cell View。
image.png

可視化的過程是表視圖、委托對象和數(shù)據(jù)源之間的合作。

在輔助編輯器中打開 **ViewController.swift **,按住 CTRL 鍵重 Table View 拖到 **ViewController **創(chuàng)建一個 outlet。

image.png

確保類型是 NSTableView,**Connection **是 outlet,命名為 tableView。

image.png

現(xiàn)在你可以在代碼中用 outlet 來引用 Table View 了。

切換到標(biāo)準(zhǔn)編輯器,打開 ViewController.swift。實(shí)現(xiàn)需要的數(shù)據(jù)源方法 ViewController,在類的末尾添加以下代碼:

extension ViewController: NSTableViewDataSource {
  
  func numberOfRows(in tableView: NSTableView) -> Int {
    return directoryItems?.count ?? 0
  }

}

它創(chuàng)建了一個遵循 NSTableViewDataSource協(xié)議的一個擴(kuò)展,實(shí)現(xiàn)了所需的 numberOfRows(in:) 方法來返回文件將中的文件數(shù)目,也就是 directoryItems 數(shù)組的大小。

現(xiàn)在來實(shí)現(xiàn)委托,將以下代碼添加到 ViewController.swift

extension ViewController: NSTableViewDelegate {

  fileprivate enum CellIdentifiers {
    static let NameCell = "NameCellID"
    static let DateCell = "DateCellID"
    static let SizeCell = "SizeCellID"
  }

  func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {

    var image: NSImage?
    var text: String = ""
    var cellIdentifier: String = ""

    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .long
    dateFormatter.timeStyle = .long
    
    // 1
    guard let item = directoryItems?[row] else {
      return nil
    }

    // 2
    if tableColumn == tableView.tableColumns[0] {
      image = item.icon
      text = item.name
      cellIdentifier = CellIdentifiers.NameCell
    } else if tableColumn == tableView.tableColumns[1] {
      text = dateFormatter.string(from: item.date)
      cellIdentifier = CellIdentifiers.DateCell
    } else if tableColumn == tableView.tableColumns[2] {
      text = item.isFolder ? "--" : sizeFormatter.string(fromByteCount: item.size)
      cellIdentifier = CellIdentifiers.SizeCell
    }

    // 3
    if let cell = tableView.make(withIdentifier: cellIdentifier, owner: nil) as? NSTableCellView {
      cell.textField?.stringValue = text
      cell.imageView?.image = image ?? nil
      return cell
    }
    return nil
  }

}

這些代碼聲明了一個遵循 NSTableViewDelegate協(xié)議的擴(kuò)展,它實(shí)現(xiàn)了 tableView(_:viewFor:row) 方法。它將被調(diào)用來獲取每一行每一列合適的 cell 類型。

這個方法內(nèi)容很多,讓我們一步一步分解:

  1. 如果沒有數(shù)據(jù)顯示,不返回 Cell。
  2. 根據(jù)列顯示的位置 (Name, Date or Size),設(shè)置它們的標(biāo)識符、文本和圖片。
  3. 通過調(diào)用 make(withIdentifier:owner:) 獲取一個 Cell view。這個方法創(chuàng)建或者重用一個帶有那個標(biāo)識符的 Cell。然后用上一步的信息填充它并返回。

下一步,在 viewDidLoad 里添加以下代碼:

tableView.delegate = self
tableView.dataSource = self

這里告訴表視圖委托對象和數(shù)據(jù)源是 view controller。

最后一步是當(dāng)新的文件夾被選擇時更新表視圖。

首先在 ViewController 里添加下面的方法:

func reloadFileList() {
  directoryItems = directory?.contentsOrderedBy(sortOrder, ascending: sortAscending)
  tableView.reloadData()
}

這個幫助方法刷新文件列表。

首先它調(diào)用 directorycontentsOrderedBy(_:ascending) 方法,返回排好序的文件數(shù)組。然后它調(diào)用表視圖的 reloadData 方法刷新它。

注意,你只需要在新的文件夾被選擇時調(diào)用它。

representedObject 的觀察器 didSet 里替換這行代碼:

print("Represented object: \(url)")

用這行替換:

directory = Directory(folderURL: url)
reloadFileList()

你剛創(chuàng)建了一個指向文件夾 URL 的 Directory 實(shí)例,然后它調(diào)用 reloadFileList() 方法刷新表視圖的數(shù)據(jù)。

編譯運(yùn)行。

使用菜單 File > Open…Command+O 快捷鍵打開一個文件夾,然后魔法發(fā)生了。現(xiàn)在表中填滿了你選擇的文件夾的內(nèi)容。調(diào)整列的大小查看文件和文件夾的所有信息。

image.png

做的好!

表視圖交互

在這一節(jié),你將做一些有關(guān)交互的工作來增強(qiáng)有 UI。

響應(yīng)用戶選擇

當(dāng)用戶選擇一個或多個文件時,應(yīng)用程序應(yīng)該更新底部的信息來顯示文件夾中的文件數(shù)和選擇了多少。

為了讓 Table View 選擇項(xiàng)改變時能被通知,需要在委托對象里實(shí)現(xiàn) tableViewSelectionDidChange 方法。當(dāng) Table View 檢測到選擇改變時,它會調(diào)用這個方法。

ViewController里添加以下代碼:

func updateStatus() {
  
  let text: String
  
  // 1
  let itemsSelected = tableView.selectedRowIndexes.count
  
  // 2
  if (directoryItems == nil) {
    text = "No Items"
  }
  else if(itemsSelected == 0) {
    text = "\(directoryItems!.count) items"
  }
  else {
    text = "\(itemsSelected) of \(directoryItems!.count) selected"
  }
  // 3
  statusLabel.stringValue = text
}

這個方法根據(jù)用戶選擇更新狀態(tài)欄標(biāo)簽。

  1. Table View 的 selectedRowIndexes屬性包含了被選擇的條目索引。要知道選擇了多少,只需要獲取數(shù)組的數(shù)目就可以了。

  2. 基于條目數(shù)量生成信息字符串。

  3. 設(shè)置狀態(tài)標(biāo)簽。

現(xiàn)在你只需要在用戶選擇時調(diào)用該方法就可以了。在 Table View 的委托擴(kuò)展中添加以下代碼:

func tableViewSelectionDidChange(_ notification: Notification) {
  updateStatus()
}

當(dāng)選擇改變時,Table View 調(diào)用該方法,然后更新狀態(tài)欄。

編譯運(yùn)行。

image.png

自己試一試。選擇一個或多個文件,觀看狀態(tài)欄改變信息文本來反映你的選擇。

響應(yīng)雙擊

在 macOS 中,雙擊通常意味著觸發(fā)了一個操作,你的程序需要執(zhí)行它。

例如,當(dāng)你在處理文件時,你期望雙擊文件時打開它的默認(rèn)應(yīng)用程序,對于文件夾,你希望查看它的內(nèi)容。

現(xiàn)在你將要實(shí)現(xiàn)雙擊的響應(yīng)。

雙擊消息不是由 Table View 的委托對象發(fā)出的。相反,它們是作為一個操作傳到 Table View 的目標(biāo) target。為了在 view controller 里接受這些消息,需要設(shè)置表視圖的 targetdoubleAction 屬性。

注意:在 Cocoa 里,Target-action 是一種大部分控件用來通知事件的模式。如果你對它不熟悉,你可以在蘋果的 Cocoa Application Competencies for macOS 文檔的 Target-Action
部分學(xué)習(xí)它。

在 ViewController 的 viewDidLoad() 里添加以下代碼:

tableView.target = self
tableView.doubleAction = #selector(tableViewDoubleClick(_:))

這告訴 Table View 視圖控制器將成為它的操作的目標(biāo),然后設(shè)置雙擊是調(diào)用的方法。

添加 tableViewDoubleClick 方法的實(shí)現(xiàn):

func tableViewDoubleClick(_ sender:AnyObject) {
  
  // 1
  guard tableView.selectedRow >= 0,     
      let item = directoryItems?[tableView.selectedRow] else {
    return
  }
  
  if item.isFolder {
    // 2
    self.representedObject = item.url as Any
  }
  else {
    // 3
    NSWorkspace.shared().open(item.url as URL)
  }
}

這是以上代碼的一步一步分解:

  1. 如果 Table View 的選擇時空的,它上面也不做就返回。同時注意,在 Table View 的空白區(qū)域雙擊會導(dǎo)致 tableView.selectedRow 返回 -1。

  2. 如果它是一個文件夾,representedObject 屬性的值被設(shè)置為條目的 URL。然后 Table View 刷新顯示文件夾的內(nèi)容。

  3. 如果它是一個文件,通過調(diào)用 NSWorkspaceopenURL 方法在默認(rèn)應(yīng)用程序里打開它。

編譯運(yùn)行,看看你的手藝!

在任意文件上雙擊,看看它是怎么打開的。然后雙擊一個文件夾,看看 Table View 怎么刷新和顯示文件夾內(nèi)容的。

哇哦!等等,你剛剛是不是做了一個 Finder 的 DIY 版本?肯定是!

排序數(shù)據(jù)

任何人都喜歡好的排序,在下一節(jié)你將學(xué)習(xí)怎樣根據(jù)用戶的選擇對 Table View 排序。

表的最好的特色之一是雙擊或單擊一個特定的列來排序。點(diǎn)擊將按升序排序,再次單擊將按降序排序。

實(shí)現(xiàn)這個特殊的 UI 很容易,因?yàn)?NSTableView將大部分功能打包好了。

Sort descriptors 是你用來處理這一點(diǎn)的東西,它只是簡單的指定了所需屬性和排序順序的 NSSortDescriptor 類的一個實(shí)例。

設(shè)置好描述符(descriptors)后,會發(fā)生這些:當(dāng)在表頭點(diǎn)擊時將通過委托對象通知你那個屬性將用來排序數(shù)據(jù)。

一旦你設(shè)置了 sort descriptors,Table View 將提供所有的 UI 來處理排序,例如可點(diǎn)擊的表頭、箭頭和被選擇的描述符的通知。但是根據(jù)信息來排序和刷新表格來響應(yīng)排序是你自己的責(zé)任。

現(xiàn)在你將學(xué)習(xí)怎么做這些!

image.png

viewDidLoad 里添加以下代碼來創(chuàng)建排序描述符 (sort descriptors):

// 1
let descriptorName = NSSortDescriptor(key: Directory.FileOrder.Name.rawValue, ascending: true)
let descriptorDate = NSSortDescriptor(key: Directory.FileOrder.Date.rawValue, ascending: true)
let descriptorSize = NSSortDescriptor(key: Directory.FileOrder.Size.rawValue, ascending: true)

// 2
tableView.tableColumns[0].sortDescriptorPrototype = descriptorName
tableView.tableColumns[1].sortDescriptorPrototype = descriptorDate
tableView.tableColumns[2].sortDescriptorPrototype = descriptorSize

這些代碼做了這些事:

  1. 為每一列創(chuàng)建了一個排序描述符,(Name, Date or Size) 表明了可以根據(jù)哪一個屬性來排序文件列表。

  2. 通過設(shè)置 sortDescriptorPrototype屬性為每一列添加排序描述符。

當(dāng)用在任意一列的表頭點(diǎn)擊時,Table View 將調(diào)用數(shù)據(jù)源的 tableView(_:sortDescriptorsDidChange:) 方法,在這個方法里將根據(jù)提供的描述符來排序數(shù)據(jù)。

在數(shù)據(jù)源擴(kuò)展中添加以下代碼:

func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
  // 1
  guard let sortDescriptor = tableView.sortDescriptors.first else {
    return
  }
  if let order = Directory.FileOrder(rawValue: sortDescriptor.key!) {
    // 2
    sortOrder = order
    sortAscending = sortDescriptor.ascending
    reloadFileList()
  }
}

這些代碼完成以下事情:

  1. 獲取用戶點(diǎn)擊的表頭相關(guān)的第一個排序描述符。
  2. 為視同控制器的 sortOrder 和 sortAscending 屬性賦值,然后調(diào)用 reloadFileList() 方法。你早些時候創(chuàng)建了這個方法來獲得一個排序的文件數(shù)組,并通知表視圖更新數(shù)據(jù)。

編譯運(yùn)行.

image.png

點(diǎn)擊任意表頭看你的表視圖排序,再次點(diǎn)擊同一個表頭讓它在升序和降序之間切換。

你用 Table View 創(chuàng)建了一個很棒的文件瀏覽器。祝賀你!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容