Skip to content

Latest commit

 

History

History
1408 lines (1406 loc) · 60.9 KB

macOS NSTableView Tutorial.md

File metadata and controls

1408 lines (1406 loc) · 60.9 KB

macOS NSTableView教程

更新记录: 这个macOS NSTableView的教程已由Warren Burton更新到Xcode 8及Swift 3的版本。 原教程 是由Ernesto García撰写的。

Create awesome user interfaces with table views!

使用table view创建超级棒的用户界面!

在macOS应用中,table view是最普遍存在的控件之一,熟悉的例子有邮件信息的列表及Spotlight的搜索结果。它可以让你的Mac以一个迷人的方式,来展现列成表格的数据。

NSTableView 使用行和列来排列数据。每一行代表给出的数据集合中的一个单独的数据模型,每一列展示这个数据模型中的一个特定的属性。

在这篇macOS的NSTableView教程中,你将使用一个table view来创建带有功能的文件查看器,它具有和Finder惊人的相似性。在你完成它之后,你将学到很多关于table view的知识,例如:

  • 怎么填充一个table view。
  • 怎么改变它的视觉风格。
  • 怎么响应类似选择或双击用户交互。

准备好创建你的第一个table view了么?继续阅读!

开始吧

下载 起始的项目 并在 Xcode 中打开它。

运行项目,来查看你会从什么地方开始:

window at start of tutorial

你有一块空白的画布,这是你将要创建很酷的文件查看器的地方。这个起始的app已经包含了一些你需要在这个教程中使用的功能。

当文件打开时,选择 File > Open… (或使用 Command+O 快捷键)。

open a folder

从弹出的新窗口中,选择任意你想要的目录,并点击 Open 按钮。你会在Xcode的控制台中看到像如下的东西:

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

这个信息展示了被选择的目录的路径,在起始项目的代码中传递了URL到view controller中。

如果你很好奇,并想了解更多有关这些东西是怎么实现的,这些是你应该查看的地方:

  • Directory.swift :包含了结构体 Directory 从目录中读取内容的实现。
  • WindowController.swift :包含了展示给你目录选择面板的代码,并传递被选择的目录给 ViewController
  • ViewController.swift :包含了 ViewController 这个类的实现,这也是今天你要花费一些时间的地方。它是你将要创建table view和展示文件列表的地方。

创建Table View

Project Navigator 中打开 Main.storyboard 。选择 View Controller Scene ,然后从 Object Library 放置一个table view到这个view上。这个在view的层级中,名叫 Table Container 的容器早已为你备好。

add a table in interface builder

下一步,你需要添加一些约束。在Auto Layout的工具栏中点击 Pin 按钮。在出现的弹出菜单中,设置全部的边缘约束如下:

  • Top , Bottom , Leading Trailing :0。

constrain the table to its container

务必设置 Update Frames Items of New Constraints ,然后点击 Add 4 Constraints

花几分钟来看一下新创建的table view的结构。正如你可能从它的名称中获取的一样,它遵循典型的table的结构:

  • 由行和列构成。
  • 每一行代表在数据模型集合中的单独的一项。
  • 每一列展示这个model的指定的属性。
  • 每一列也可以有一个列头(header row)。
  • 列头描述了这一列的数据。

table view inner structure

如果你熟悉iOS中的 UITableView ,你会在走熟悉的路上(treading familiar waters),但在macOS中,它们会深更加入。事实上,你会惊讶于构成 NSTableView 的单个的UI对象的层级的数量。

NSTableView 是一个更老且比 UITableView 更复杂的控件,它服务于一个不同的用户交互的范例中,特别地在用户有一个鼠标或触控板的情况下。

相对于 UITableView ,它的主要区别是你可以有多列,及一个可以用来同table view交互的表头,例如排序和选取。

NSScrollView NSClipView 会分别负责滚动和裁剪 NSTableView 的内容。

这里有两个 NSScroller 对象 - 各自负责table在垂直和水平方向上的滚动。

这里也有一些列的对象。一个 NSTableView 有若干的列,这些列都有标题。注意,让用户能够改变列的大小和重新排序是非常重要的,尽管你可以通过设置默认为关闭的来移除这项能力。

剖析NSTableView

在Interface Builder中,你已经看到了table view中的view层级的复杂性。很多的类协作来构建table的结构,通常最终看起来会像这样:

components of a table view

这些是构成 NSTableView 的关键部分:

  • Header View :header view是 NSTableHeaderView 的实例。它负责在table的顶部绘制header。如果你需要展示一个定制的header,你可以使用你自己的header的子类。
  • Row View :row view展示了table view中的每一行的可见的属性,像一个选择高亮。展示在table中的每一行都有它自己的row view的实例。一个重大的区别是row不代表你的数据,cell才代表。它只处理可见的数据,例如选择颜色或分隔线。你可以创建新的row的子类来让你的table view风格不同。
  • Cell Views :cell可能是table view中最重要的对象了。在行和列交叉的地方,你会找到一个cell。每个cell都是 NSView NSTableCellView 的子类,它用来负责展示实际的数据。猜下还有什么?你可以创建定制的cell view的类来展示任何你喜欢的内容。
  • Column :它是由 NSTableViewColumn 代表的,负责管理列的宽度和行为,例如调整大小和重新定位。这个类不是一个view,但却是一个controller的类。你可以用它来指定列的行为,但由于header的缘故,你不能控制列的可见的风格。row和cell view把这件事已经覆盖了。

注意: NSTableView有两种模式。第一种是基于被称作 NSCell 的cell。它像是一个 NSView ,但是更老且更轻量。它来自于计算早期的时候,当桌面需要优化,要以最低的消耗绘制时而产生的。

苹果推荐使用基于view的table view,但是你会在AppKit的多个控件中看到 NSCell ,值得去了解它是什么及它从哪里来的。你可以阅读更多的 NSCell 在苹果的 Control and Cell Programming Topics 这篇文档中。

好的,现在一个很好的“小步慢跑”(little jog),了解table view结构背后的基础理论。既然你已了解了那些,现在就是时候返回 Xcode 来继续完成您非常个性化的table view。

玩转table view中的行

默认的,Interface Builder创建的table view会带有两列,但是你需要三列来展示名称,日期和大小的文件信息。

回到 Main.storyboard

View Controller场景 中选择table view。确保你选择的是table view而 不是 包含它的scroll view。

table in the view hierarchy

打开 Attributes Inspector 。改变 Columns 为3。它是如此地简单!你的table view现在就有了三列。

下一步,因为你想一次操作多个文件,在 Selection 中勾选 Multiple 。同时在 Highlight 这里勾选 Alternating Rows 。当它打开时,table view就会使用交替的行的背景颜色,就像Finder一样。

configure table attributes

重命名列头,让它更有描述性。在 View Controller场景 中选择第一列。

configure-column

打开 Attributes Inspector 并改变列的 Title Name

change the header title in attributes

对于第二列和第三列重复同样的操作,分别改变 Title Modification Date Size

注意 :这里有一个可供替代的方法来改变列的标题。你可以直接双击table view的header来让它变得可编辑。两条路都可以得到正确的结果,所以按你更喜欢的那种来就可以了。

最后,如果你还是不能看到Size这列,选择Modification Date这列并改变Width为200。通过使用你的鼠标来回调整 :]

resize modification date if needed

运行项目。这里是你应该看到的:

table with configured headers

改变信息的表现方式

在当前的状态下,这个table view 有三列,每一列包含一个用text field展示的cell view。

但这有一点乏味。所以加工一下吧,在文件名旁边展示一下它的图标。你的table会在这个小升级之后变得更整洁。

你需要用一个包含image和text field的新的cell来替换第一列的cell view。

你是幸运的,因为Interface Builder拥有这种内建类型的cell。

Name 这列中选择 Table Cell View ,并删除它。

delete this view

打开 Object Library 并将一个 Image & Text Table Cell View 拖拽到table view的第一列或 View Controller Scene 的“树”中,就在 Name 这个table view的列下面.

add a new cell type in interface builder

现在你就把它打造成型了!

设定Identifier

每个cell类型都需要有一个被设定的identifier。否则,当你coding时就无法根据指定的列来创建cell view了。

选择第一列的cell view,在 Identity Inspector 中,改变 Identifier NameCellID

edit column identifiers

在第二列及第三列中的cell view上重复同样的操作,分别命名为 DateCellID SizeCellID

填充table view

注意: 你有两种方法可以填充tableview:一种使用datasource和delegate协议,你将在这个教程中看到它们;另一种通过 Cocoa bindings 来完成。这两种技术不是互斥的,你可以同时使用他们来得到你想要的。

这个table view当前并不知道你需要展示的数据,和怎么来展示,但这确实需要形成回路。所以你要实现这两个协议来提供这些信息:

  • NSTableViewDataSource :告诉table view有多少行需要展示。
  • NSTableViewDelegate :提供将要展示在指定行和列上的view cell。

population flow of a table view

这个形象化的过程是关于table view,delegate和data source之间的协作:

  1. 这个table view调用data source的方法 numberOfRows(in:) ,它会返回table将要展示的行的数量。
  2. 这个table view为每一行和列调用delegate的方法 tableView(\_:viewFor:row:) 。delegate会创建这个位置上的view,并用恰当的数据填充它,然后返回给table view。

为了在table view中展示你的数据,两个方法都必须实现。

Assistant editor 中打开 ViewController.swift 按住control拖拽 table view到 ViewController 类的实现中来插入一个outlet。

add an outlet to the tableview

确认 Type NSTableView Connection Outlet 。将outlet命名为: tableView

define the outlet

你现在就可以在代码中使用outlet引用table view了。

切回到 Standard Editor ,并打开 ViewController.swift 。通过添加这些代码到 ViewController 类的最后来实现提供数据源的方法:

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

这创建了一个遵守 NSTableViewDataSource 协议的extension,并实现了要求的方法 numberOfRows(in:) 来返回目录中文件的数量,它就是 directoryItems 数组的大小。

现在你需要实现delegate。添加下列的extension到 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 协议的extension,并实现了方法 tableView(\_:viewFor:row) 。它接下来会被table view的每一行和每一列调用,来得到恰当的cell。

这个方法中还有很多需要做的事,这里有一个一步步的项(breakdown):

  1. 如果没有数据要展示,它应该不返回cell。
  2. 基于cell将要展示的列(Name,Date或Size),它要设置cell identifier,文本和图像。
  3. 它通过调用 make(withIdentifier:owner:) 来得到一个cell。这个方法通过那个identifier来创建或复用一个cell,然后使用之前一步提供的数据来填充它,并返回。

接下来,在 viewDidLoad() 中添加这个代码:

tableView.delegate = self
tableView.dataSource = self

这里你告诉table view它的data source和delegate是这个view controller。

最后一步是当一个新的目录被选中时,告诉tableview刷新数据。

首先,添加这个方法到 ViewController 的实现中:

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

这个助手方法可以更新文件的列表。

首先,它调用了 directory 的方法 contentsOrderedBy(\_:ascending) ,并返回了一个包含目录文件的有序的数组。然后调用了table view的方法 reloadData() 来告诉它进行刷新。

注意你仅需要在一个新的目录被选中时调用这个方法。

来到 representedObject 的观察者 didSet 中,替换这行代码:

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

为:

directory = Directory(folderURL: url)
reloadFileList()

你刚创建了一个 Directory 的示例,指向目录的URL,它调用 reloadFileList() 方法来更新table view的数据。

运行项目。

使用菜单上的 File > Open… Command+O 快捷键来打开一个目录,观察魔法的发生!现在table已经被你刚选择目录的内容填满了。调整列的大小来查看每一个文件或目录的所有信息。

your table now shows content

Nice job!

table view的交互

在这一部分,你将与一些交互共事,来提升UI。

响应用户的选择

当用户选择了一个或多个文件,这个应用应当更新底部栏的信息,来展示目录中文件的个数,以及被选中的个数。

为了在选择发生变化时收到通知,你需要在delegate中实现 tableViewSelectionDidChange(\_:) 。这个方法将在table view探测到选择发生变化时被调用。

添加这个代码到 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
}

这个方法基于用户的选择更新了status label。

  1. table view的属性 selectedRowIndexes 包含了选择的行的序号。要得知有多少项被选择了,只需获取这个array的count。
  2. 基于项目的个数,构建了有益的文本字符串。
  3. 设置状态label的文本。

现在,你只需要当用户改变了table view的选择时,调用这个方法。在table view的delegate extension中添加下列代码:

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

当选择改变时,table view就会调用这个方法,然后更新状态文本。

运行项目。

selection label now configured

自己来试一下;在table view中选择一个或多个文件,并观察情报信息的改变,反映你的选择。

响应双击

在macOS中,双击通常意味着用户触发了一个动作,你的程序就需要执行它。

例如,当你正在处理文件,你通常期望双击这个文件来已它默认的应用打开它;对于目录,则你期望查看它的内容。

现在你就要实现双击的响应。

双击的通知不是通过table view的delegate来发送,而是发送一个动作给table view的target。但是为了在view controller中接收到那些通知,你需要设定 target doubleAction 两个属性。

注意 :Target-action是一个被大多数Cocoa的控件使用的模式。如果你不熟悉这个模式,你可以在苹果的 Cocoa Application Competencies for macOS 这个文档中学习 Target-Action 这一部分。

ViewController viewDidLoad() 方法中添加下列代码:

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

这告诉了table view这个view controller将变成它的动作的target,这样这个方法就会在双击后被调用。

添加 tableViewDoubleClick(\_:) 方法的实现:

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的空的区域将使得 tableView.selectedRow 的值等于1。
  2. 如果是一个目录,它会设置 representedObject 属性为它的URL。然后这个table view会刷新来显示那个目录的内容。
  3. 如果这一项是一个文件,它会通过调用 NSWorkspace 的方法 openURL() 来使用默认的应用打开它。

运行项目以检查你的作品。

双击任何文件并观察它是怎么用默认的应用打开的。现在,选择一个目录并观察table view怎么更新及展示那个目录的内容。

咳,稍等,你只是创造了一个DIY版本的Finder?确实看起来是这样!

数据排序

每个人都喜欢好的排序,在这一部分你将学习怎么基于用户的选择给table view排序。

table最好的特性之一,就是通过单击或双击指定的一列来排序。单击会以升序排列,双击则以降序排列。

实现这个特定的UI是非常容易的,因为 NSTableView 打包了大多数直接“开箱即用”(out of the box)的功能。

Sort descriptors 是你用来处理这个的 NSSortDescriptor 类的简单的实例,它指定了要求的属性并排序。

在设置descriptor之后,将会发生:点击table view的一个列头,将通知你,通过代理,哪个属性将被使用,用户就能够给数据排序。

一旦你设置了sort descriptor,table view就会提供全部的UI来处理排序,像是可点击的header,箭头,以及哪个sort descriptor被选择了的通知。然而,基于那些信息排序,然后刷新table view来反映新的顺序是你的责任。

现在你将学到怎么去做:

Woot!

viewDidLoad() 中添加下列代码来创建sort descriptor:

// 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. 为每一列创建sort descriptor,使用一个键(Name,Date或Size)来完成,它指示了可以让文件列表被排序的那个属性。
  2. 通过设置 sortDescriptorPrototype 属性,为每一列添加sort descriptor。

当用户点击任一列头时,这个table view会调用data source的方法 tableView(\_:sortDescriptorsDidChange:) ,app就会基于提供的descriptor来进行排序。

添加下列的代码到data source的extension中:

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. 获取符合用户点击的列头的第一个sort descriptor。
  2. 给view controller的 sortOrder sortAscending 属性赋值,然后调用 reloadFileList() 。你提早设置它来获取一个已排好序的文件的列表,并告诉table view来重载数据。

运行项目。

sortable-columns

点击任一header来查看你的table view排序数据。再次点击相同的header来在升序和降序之间切换。

你已经使用table view构建了一个很好的文件查看器。恭喜!

从这儿去向哪里?

你可以在 这里 下载完整版本的项目。

这个macOS NSTableView教程覆盖了相当多的内容,你现在应当感到更多的自信,来以你的能力使用table view来组织数据。除此以外,你还学到了:

  • table view的基本结构,包括header、row、column和cell的单独的特质。
  • 怎样添加列来展示更多的数据。
  • 怎么为之后的引用鉴别多种元件。
  • 怎样在table中加载数据。
  • 怎样响应各种用户交互。

这里有更多你可以使用table view来做的事,让你可以为你的app构建高雅的UI。如果你想要关于它更多的内容,可以参考下列资源: