知识共享许可协议本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。本文仅作为个人学习记录使用,欢迎在许可协议范围内转载或使用,请尊重版权并且保留原文链接,谢谢您的理解合作。如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,这样您将能在第一时间获取本站信息。

写在前面

本文不是单独讲NSTableView如何使用,或是如何拖拽,或是其他的,我在参考了Apple的Guide以及一些资料整理后,综合运用NSTableView,NSPasetboard等一些基础技术,来告诉大家如何最简单最快捷的开始用起NSTableView,让新入门的童鞋们能够快速的吧自己的项目run起来,而不是再去花一两周去慢慢看文档了,当然要好好用文档还是少不了滴。

Mac上的TableView

经过一段时间在iOS上的探索后,用自己在cocoa touch开发上半残的经验开始了cocoa开发的历险。在尝试了Button,TextField等基础的东西后,也基于最近在尝试的东西,决定试试当初在iOS开发上卡了好几天才想通怎么弄的TableView了。但是总觉得目前网上关于Mac app开发的资料非常少,主要是iOS太火了吧把Mac的光芒都掩盖了。参考了下Apple关于TableView,OutlineView(为啥是OutlineView,他两是一家子的)的Guide和TableViewPlayground的例子后,不敢说对NSTableView有多少了解,起码怎么用起来是知道了。

在Mac app的开发和iOS app的开发以及使用非常的相似,所以对应iOS上UITableView自然有一个NSTableView为Mac服务。而且其中的使用方法和使用的思想也是非常相似的。因此用过iOS上UITableView的童鞋们,对NSTableView肯定轻松上手了。

成品

上面的图就是今天打算弄的Mac app的成品了,左边是分类,右边是分类下的东西,需要实现的功能包括了,左边选择左边分类的时候,右边显示分类下的东西,右边分类下的东西可以拖拽到左边其他分类中,右边分类下的东西可以拖拽到其他应用中(文本的方式),Add按钮实现在左边分类中添加一个分类并可以编辑名称。会弄的童鞋们肯定觉得这些功能小儿科了,嘛,这个还是挣扎了好几天才能有此结果啊,大家见笑了。

要做些啥

上面说了的功能对应,我们要做下面的事情

  1. 界面

    • 两个TableView,一左一右(这个,是废话了)
  2. 显示和交互

    • 对TableView提供数据
    • 两个TableView的数据交互
    • 添加item
  3. 拖拽操作(Drag and Drop)

    • TableView之间的拖拽
    • 剪贴板的写入

动手吧

那不说废话了,我们开始吧。按照上面的顺序,其实个人感觉也是难易度的顺序了和实现这个程序功能的顺序了,首先把界面拖出来,好不好看大家自己看啦,然后先把最基础的数据显示搞定,都没数据那还有啥拖拽的意思啊,接下来就是实现这个应用的高级功能了——拖拽=_,=+

界面的理解

界面的话这个就太简单了,用interface builder把两个TableView拖到界面上,至于怎么摆放好看的话,那就仁者见仁智者见智了,不过个人建议,可以考虑用一个Vertical Split View或者Horizontal Splite View把两个TableView一左一右或者一上一下放好喽最好还能结合界面布局的Constraints保证拖拽窗口大小的时候,这两Table身材跟着走样。拖拽好的界面就像这样

界面

嗯嗯,我用的就是Vertical Split View了,左边一个右边一个,左边的就是分类,右边的就是分类下的元素

界面树

在界面树上可以看见,一个TableView里面,他包括了这些东西,一个Header也就是俗称的标题栏了,一个TableView也就是一个TableView的TableView(认为他是灵魂吧,或者药的有效成分-,-?),TableView下面还有其他东西,嗯嗯TableCloumn以及下面的TableCell,这两个,就是有效成分的实际构成了,字面上理解,TableCloumn那就是Table的列了,Cell的话跟iOS有点类似,可以理解为一个TableView中元素的一个原型(我是这么理解的了,具体是不是再说啦),看过guide的童鞋发现了,这里用的是Cell based的TableView,还有一种高档的用法是View based的TableView,那个,以后再分解吧。

数据源

完成了界面,那就可以run了,只是run起来,啥东西都没有,那么接下来,我们先把TableView的数据源给搞定,这样至少可以看到列表里面的东西,不过在实现TableView的数据源之前,还需要做的一个事情是实现应用的数据模型,小伙伴们是不是觉得果然低调奢华有内涵了啊=_,=+

先不忙实现对TableView的操作,我们这里先实现两个东西,一个叫SimpleObj顾名思义——简单对象,另一个叫DataEntity一样的,数据管理类。

SimpleObj类是我实现了用于模拟实际使用过程中,可能并不是直接使用一个字符串作为传入的东西,那还是用一个简单的类来实现吧,so叫SimpleObj了

DataEntity这个东西,由于我是从其他语言转过来的新童鞋,所以习惯了其他的东西里面管理数据的类,都叫XXXManager之类的,不过参看了下TableViewPlayground后,觉得应该稍微与时俱进就叫XXXXEntity了,这个类,实现了对数据的初始化以及其他的一些操作。

数据模型——SimpleObj

好我们先来看简单的SimpleObj怎么实现的

首先我们看下.h的interface文件

1
2
3
4
5
@interface SimpleObject : NSObject
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *type;
@property (strong, nonatomic) NSString *category;
@end

似乎灰常简单啊(魂淡这就是你说的有内涵么)嘛,低调低调,自然implement文件也不会很复杂see~

数据管理类——DataEntity

接下来我们看看DataEntity啦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#import "SimpleObject.h"

@interface DataEntity : NSObject
@property (strong, nonatomic) NSMutableArray *categories;
@property (strong, nonatomic) NSMutableDictionary *items;

- (NSArray *)itemsForCategory:(NSString *)category;
- (NSArray *)removeItem:(NSString *)item inCategory:(NSString *)category;
- (NSArray *)addItem:(NSString *)item inCategory:(NSString *)category;
- (void)moveItem:(SimpleObject *)item toCategory:(NSString *)category;

+ (id)sharedInstance;

@end

这里需要解释下了,categories就是左边TableView需要显示的数据,items是右边将要显示的数据,因为在应用中需要修改,所以我分别使用了NSMutableArray和NSMutableDictionary来存储,
接下来的几个方法分别实现了根据category查找items(用于左边单击选择时在右边显示item用),删除item,添加item,移动item到新的category,还有实现单例模式需要的sharedInstance。这些方法都不细看了,毕竟今天的主角是NSTableView。接下来我们看看实现文件(代码大家看工程文件吧,太长了就不贴了)就开始打boss啦~~

实现NSTableView显示数据

完成了,我们的SimpleObj和DataEntity后,就可以在TableView里面把我们的categories和items显示到ui上了,那么如何进行呢,我们如果需要在一个TableView里面显示数据,肯定很关心这两个问题:

  1. 我的Table里面有多少行呢?
  2. 每行要显示些啥东西呢?

每个NSTableView在显示之前都会问他的NSTableViewDataSourceDelegete下面这两个问题:

  1. – numberOfRowsInTableView:
  2. – tableView:objectValueForTableColumn:row:

第一个告诉TableView你有多少行,第二个告诉他每行有些啥东西。所以实现了这两个东西,TableView就知道他自己要干啥了,这两个方法是左边TableView的Delegate需要告诉他的事情。

基本的数据展示方法

我们先来看左边的TableView的这两个方法是是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
DataEntity *dataEntity = [DataEntity sharedInstance];
return dataEntity.categories.count;
}

- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
DataEntity *dataEntity = [DataEntity sharedInstance];
return [dataEntity.categories objectAtIndex:row];
}

左边TableView里面需要显示categories里面的各个分类,所以在numberOfRowsInTableView:里面就返回了categories的数量,由于我们的categories只是NSString类型的字符串可以直接返回给TableView进行显示,所以我们在tableView:objectValueForTableColumn:row:中就直接返回categories中第row个元素给TableView去显示。这样运行程序左边的Table就大功告成了。

接着是右边的TableView了,右边的TableView显示左边categories下的items,DataEntity中的items是一个NSMutableDictionary,将categories中的一个分类对应到一个存放items的数组。接下来我们就要让右边的TableView也能显示东西了。

实现交互

一开始我们说过,右边的东西要跟着左边一起动,所以右边除了需要显示table里面的东西以外,还需要从左边获取现在正在显示的category是哪个。这个怎么做到呢,根据我半残的iOS经验应该用protocol,那么我们先确定我们需要什么样的东西,需要一个什么样的代理来告诉右边TableView我选择了谁。

在左边TableView的interface中定义下面的接口,并增加@property (weak) id<SelectedItemDidChangeDelegate> itemChangeDelegate;属性,在左边Table选择后通过itemChangeDelegate通知右边的TableView告诉他我改了改选中的东西哦~

1
2
3
4
5

@protocol SelectedItemDidChangeDelegate <NSObject>
- (void)selectedItemDidChangeTo:(NSString *)item;
@end

接下来在左边实现tableViewSelectionDidChange:方法,通过itemChangeDelegate通知右边的Table选择的item发生了变化。

1
2
3
4
5
6
7
8
9
10

- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
if ([_itemChangeDelegate respondsToSelector:@selector(selectedItemDidChangeTo:)]) {
DataEntity *dataEntity = [DataEntity sharedInstance];
NSString *selectedItem = [dataEntity.categories objectAtIndex:[_tableview selectedRow]];
[_itemChangeDelegate selectedItemDidChangeTo:selectedItem];
}
}

这样每次对左边的Table进行了选择之后,右边都会知道左边选了啥并响应这一事件。接下来我们看右边怎么回应左边的呼唤。

为了回应左边Table的呼唤,右边Table的代理需要先实现SelectedItemDidChangeDelegate接口,并实现该接口所定义的selectedItemDidChangeTo:方法,我们来看看右边怎么实现这两个方法以及TableView每次会问的两个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

- (void)selectedItemDidChangeTo:(NSString *)item
{
_selectedCategory = item;
[_rightTableView reloadData];
}

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
NSInteger itemCount = 0;
if (_selectedCategory) {
DataEntity *dataEntity = [DataEntity sharedInstance];
itemCount = [dataEntity itemsForCategory:_selectedCategory].count;
}
return itemCount;
}

- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
SimpleObject *item = nil;
if (_selectedCategory) {
DataEntity *dataEntity = [DataEntity sharedInstance];
item = [[dataEntity itemsForCategory:_selectedCategory] objectAtIndex:row];
}
return item.description;
}

我们看到在每次TableView询问那两个老问题的时候,右边TableView的Delegate都要先看看_selectedCategory到底是啥,这个_selectedCategory也就是左边TableView告诉通过selectedItemDidChangeTo:告诉右边TableView我现在选了谁,接下来通过TableView的reloadData:方法告诉TableView加载数据,这时候右边的Delegate就会告诉TableView新的数据是些啥了。

添加item

TableView交互部分最后一个要做的事情是在TableView里面添加item,也就是下面那个Add按钮要做的事情了。TableView提供了insertRowsAtIndexes:withAnimation:方法,可以在TableView中添加新的行,我们来看怎么实现。

1
2
3
4
5
6
7
8
9
10

- (IBAction)addSth:(id)sender {
[_tableview beginUpdates];
DataEntity *de = [DataEntity sharedInstance];
NSString *nt = @"Enter Name";
[de.categories insertObject:nt atIndex:0];
[_tableview insertRowsAtIndexes:[NSIndexSet indexSetWithIndex:0] withAnimation:NSTableViewAnimationEffectGap];
[_tableview endUpdates];
}

上面的代码首先告诉TableView现在beginUpdates也就是我要添加新东西啦,接着在我的categories里面也要加上这个item否则之后会出现out of index的问题,接着通过insertRowsAtIndexes:withAnimation:添加一个item到table里面,NSTableViewAnimationEffectGap告诉TableView添加时需要一个简单的动画。这样就实现了最简单的添加操作,但是我们要在添加的时候能够就修改item的名字要怎么办呢?经过翻看skd文档后,找到了editColumn:row:withEvent:select:这个方法,这个方法会告诉TableView我要编辑你的第某行的item。

1
2
3
4
5
6
7
8
9
10
11

- (IBAction)addSth:(id)sender {
[_tableview beginUpdates];
DataEntity *de = [DataEntity sharedInstance];
NSString *nt = @"Enter Name";
[de.categories insertObject:nt atIndex:0];
[_tableview insertRowsAtIndexes:[NSIndexSet indexSetWithIndex:0] withAnimation:NSTableViewAnimationEffectGap];
[_tableview editColumn:0 row:0 withEvent:nil select:YES];
[_tableview endUpdates];
}

在编辑完成后TableView需要告诉他的Delegate我完成编辑了,那么需要在Delegate中实现tableView:setObjectValue:forTableColumn:row:方法,接收编辑事件。

1
2
3
4
5
6
7

- (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
DataEntity *de = [DataEntity sharedInstance];
[de.categories setObject:object atIndexedSubscript:row];
}

在这里我做的只是在完成修改时,同步更新categories中的数据,以保持TableView和DataSource中的东西一致。

拖拽操作

这样我们的应用程序已经基本成型了,其实TableView使用到这里基本算是结束了,平时的使用已经基本能够满足了。接下来就是一个比较高级的部分Drag and Drop了,为啥说他高级呢,一开始我想着拖拽无非就是左边拖了,右边接收就可以了,从TableViewPlayground里面来看却发现这个操作其实不简单,拖拽里面除了拖拽的东西相互交互以外还需要实现对Mac中剪贴板的写入和读取,个人理解是Mac在所有的拖拽里面都使用了剪贴板,以保持应用程序自身和应用之间交互数据的简洁和统一。

拖拽到其他应用(剪贴板写入)

现在这样已经能应对很多场景了,能够在一个应用里面分享数据了,接下来我们要在应用程序之间拖拽来进行数据交换了。

这个虽然说起来复杂,但是其实实现起来很简单,首先我们需要在TableView告诉大家,在拖拽时候需要写入什么对象到剪贴板里面,在拖拽某行时,告诉TableView应该写入什么对象

1
2
3
4
5
6
7
8

- (id <NSPasteboardWriting>)tableView:(NSTableView *)tableView pasteboardWriterForRow:(NSInteger)row
{
DataEntity *dataEntity = [DataEntity sharedInstance];
SimpleObject *item = [[dataEntity itemsForCategory:_selectedCategory] objectAtIndex:row];
return item;
}

在上面我们看见了,这个方法需要放回一个id <NSPasteboardWriting>的对象,所以对于SimpleObj我们也要实现这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

- (NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard
{
NSArray *writableTypes = nil;
writableTypes = [NSArray arrayWithObject:NSPasteboardTypeString];
return writableTypes;
}

- (id)pasteboardPropertyListForType:(NSString *)type
{
if ([type isEqualToString:NSPasteboardTypeString]) {
return self.description;
} else if ([type isEqualToString:@"SimpleObject"]) {
return self.description;
}

return nil;
}

在上面实现NSPasteboardWriting中的writableTypesForPasteboard:pasteboardPropertyListForType:方法。

writableTypesForPasteboard:方法告诉写入剪贴板的对象,我们需要写入些什么样的对象类型,将这些对象类型放到一个数组中,这样就可以告诉剪贴板,这个对象会有哪些类型写入。

pasteboardPropertyListForType:方法告诉剪贴板什么样的类型,返回什么样的数据,这里我们就没有复杂操作了,都返回字符串了。

不过右边要能拖拽之前要先告诉大家,我能拖拽哦~,那么需要在AppDelegate里面执行下面的代码设定是右边的拖拽源

1
2
3

[_rightTableView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:NO];

这样就实现了可以在应用程序之间交换数据了,现在可以将item拖拽出来,拖拽出来的东西,将按照pasteboardPropertyListForType:中的的代码,表现为实际的对象。

TableView之间的拖拽

接下来先来做TableView之间的拖拽,TableView之间拖拽,也就是应用程序内的数据交互,这个操作可以通过剪贴板,也可以通过TableView之间的接口来实现,这里思考了好会儿,觉得如果用剪贴板可能太复杂了,也没啥必要所以就先使用TableView来实现了。

使用TableView交互来实现的话需要通过接口来交互数据。

右边:我拖了个东西来给你哦,快接着吧。

左边:嗯嗯,我先来看看我能不能接收呢

左边:好嘞,接着啦~

右边:接着了啊,好那我吧这边东西干掉啦~

嗯嗯,我们就要让左边和右边的TableView这么对话了,那么我们先来看交互的接口如何定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

//告诉右边我接着了啊
@protocol ItemDragFinishedDelegate <NSObject>

- (void)finishDrag;

@end

//告诉左边我要拖个东西给你了哦
@protocol RightTableviewItemDraggingDelegate <NSObject>

- (void)dragItem:(SimpleObject *)item fromCategory:(NSString *)category;

@end

接下来我们来看如何实现上面左边和右边的对话吧,先来看右边怎么告诉左边的TableView我要给你东西了

1
2
3
4
5
6
7
8
9
10

- (void)tableView:(NSTableView *)tableView draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint forRowIndexes:(NSIndexSet *)rowIndexes
{
if([_draggingDestenation respondsToSelector:@selector(dragItem:fromCategory:)]) {
DataEntity *dataEntity = [DataEntity sharedInstance];
NSArray *items = [dataEntity itemsForCategory:_selectedCategory];
[_draggingDestenation dragItem:[items objectAtIndex:[rowIndexes lastIndex]] fromCategory:_selectedCategory];
}
}

上面代码响应了拖拽的事件,并且检测_draggingDestenation是否能够响应RightTableviewItemDraggingDelegate定义的事件,如果响应的话,告诉他我要把item拖给你的某个category了哦。左边先看看自己能不能接收这个拖拽的东西,TableView的Delegate中的tableView:validateDrop:proposedRow:proposedDropOperation:就是实现这个事情的

1
2
3
4
5
6
7
8
9

- (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id<NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation
{
if (dropOperation == NSTableViewDropAbove) {
return NSDragOperationNone;
}
return NSDragOperationMove;
}

上面的代码实现了对拖拽到左边的item的验证是否可以拖拽,我们的左边只需要拖拽到左边的category上,而不能插入到左边的列表中。那么在上面我们看到了对dropOperation有一个验证的操作,如果是NSTableViewDropAbove就返回NSDragOperationNone,否则就返回NSDragOperationMove,那这个DropAbove这个还是很容易理解的字面意思上看,就是说这个拖拽的事情啊,是在发生在行的上面(原文Specifies that the drop should occur above the specified row),返回None就表示不允许他拖放上去。还有一种是NSTableViewDropOn,表示这个拖拽发生在拖拽的行,下面图分别表示了两种DragOperation

NSTableViewDropOn

上面是DragOn的情况

NSTableViewDragAbove

上面是DragAbove的情况

好,左边先看看自己能不能接受了以后,就开始接受拖拽了,TableViewDelegate的tableView:acceptDrop:row:dropOperation:就是用来告诉TableView如何去接受Drop事件的了,那来看看怎么弄吧

1
2
3
4
5
6
7
8
9
10
11

- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id<NSDraggingInfo>)info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)dropOperation
{
DataEntity *dataEntity = [DataEntity sharedInstance];
[dataEntity moveItem:_currentDraggingItem toCategory:[dataEntity.categories objectAtIndex:row]];
if ([_itemDragFinishedDelegate respondsToSelector:@selector(finishDrag)]) {
[_itemDragFinishedDelegate finishDrag];
}
return YES;
}

同样,在完成了拖拽的处理以后,他会问一问_itemDragFinishedDelegate(也就是右边的TableView)你能不能接收我告诉你我接受完了啊,告诉他I finishDrag啦

接着右边实现finishDrag方法,把还在右边显示的item干掉了

1
2
3
4
5
6

- (void)finishDrag
{
[_tableiew reloadData];
}

其实我们在Drag的过程中就已经完成把item从一个category移到另一个了,所以当finishDrag后就只需要重新加载数据就可以了。

写在最后

额,写的挺长的,也不知道对大家有没有点帮助,这里综合了NSTableView的一些基础用法,综合起来内容挺多的。完整的代码在这里下载myTV.zip

最后欢迎大家订阅我的微信公众号 Little Code

公众号

  • 公众号主要发一些开发相关的技术文章
  • 谈谈自己对技术的理解,经验
  • 也许会谈谈人生的感悟
  • 本人不是很高产,但是力求保证质量和原创