表格数据管理控制器

NSTableView视图非常适合用于展示大量的结构化数据。

TableViewControllerDemo

我们希望实现一个通用的TableDataNavigationViewController 表格数据导航视图控制器,支持下面功能:

  1. 表列定义可以配置化
  2. 数据列排序
  3. 数据行拖放交换顺序
  4. 数据分页导航显示
  5. 增加删除编辑表数据

表列定义动态配置

我们知道在使用xib设计表格时,不管是老式的Cell-Based 的表格还是现在主推的View-Based的方式,只需要从控件工具箱拖放相关的控件到表格列定义视图上即可用完成,列的定义还是非常方便的。

但这种方法只适合表格的列数比较固定的情况,如果表列数量不固定,是动态变化时候就完全不适用了。
这时候我们就只能使用代码来实现表格列动态配置,来支持列的动态可配置化。

我们来看下NSTableView中关于表列的属性和方法

@property (readonly, copy) NSArray<NSTableColumn *> *tableColumns;
@property (readonly) NSInteger numberOfColumns;
- (void)addTableColumn:(NSTableColumn *)tableColumn;
- (void)removeTableColumn:(NSTableColumn *)tableColumn;
- (NSTableColumn *)tableColumnWithIdentifier:(NSString *)identifier;

可以看出表的所有列存储到只读属性访问tableColumns数组中,同时提供列的增加删除接口方法。

NSTableColumn类型我们看看它的定义

@property (copy) NSString *identifier;
@property CGFloat width;
@property CGFloat minWidth;
@property CGFloat maxWidth;
@property (copy) NSString *title;
@property (getter=isEditable) BOOL editable;

为了能动态创建NSTableColumn,我们需要定义一个表列配置的模型类来指定这些属性,下面一节会专门讨论的表列定义的模型类TableColumnItem。

NSTableViewTableColumnItem

因此动态创建表列的步骤为先通过TableColumnItem模型类定义表列的属性,通过TableColumnItem再创建NSTableColumn,然后将NSTableColumn加入到NSTableView表对象实例中完成。

表列定义的模型

TableColumnItem类用来定义了表头Head定义部分和表数据单元的属性。

@interface TableColumnItem : NSObject
//表头定义部分
@property(nonatomic,strong)NSString       *title;//列标题
@property(nonatomic,strong)NSString       *identifier;//表列Identifier
@property(nonatomic,assign)NSTextAlignment headerAlignment;//列标题的alignment
@property(nonatomic,assign)CGFloat width;//列宽度
@property(nonatomic,assign)CGFloat minWidth;//列最小宽度
@property(nonatomic,assign)CGFloat maxWidth;//列最大宽度
@property(nonatomic,assign)BOOL    editable;//文本是否允许编辑
//下面是表格单元内容部分
@property(nonatomic,assign)TableColumnCellType     cellType;//表格单元视图的类型
@property(nonatomic,strong)NSColor        *textColor;//文本的Color
@property(nonatomic,strong)NSArray *items;//Combox类型的items数据
@end

TableColumnCellType定义了表格列数据支持的视图控件的类型,包括了文本标签,文本编辑控件,下拉选择Combox, 勾选CheckBox, 图片ImageView 5大类。

typedef NS_ENUM(NSInteger, TableColumnCellType) {
    TableColumnCellTypeLabel = 0,
    TableColumnCellTypeTextField = 1,
    TableColumnCellTypeComboBox = 2,
    TableColumnCellTypeCheckBox = 3,
    TableColumnCellTypeImageView = 4
} ;

下面是一个NSTableColumn通过TableColumnItem定义创建的例子。

TableColumnItem *field = [[TableColumnItem alloc]init];
field.title      = @"Field";
field.identifier = @"field";
field.width      = 100;
field.minWidth   = 100;
field.maxWidth   = 120;
field.editable   = YES;
field.headerAlignment = NSLeftTextAlignment;
field.cellType = TableColumnCellTypeTextField;

NSTableColumn *column = [[NSTableColumn alloc]init];
column.identifier = item.identifier;
[column setWidth:item.width];
[column setMinWidth:item.minWidth];
[column setMaxWidth:item.maxWidth];
[column setEditable:item.editable];

NSTableColumn类Category扩展

为了使用方便,我们定义实现了基于TableColumnItem来创建NSTableColumn的Category。

NSTableColumn+Category.h

#import "TableColumnItem.h"
@interface NSTableColumn (Category)
+(id)xx_tableColumnWithItem:(TableColumnItem*)item;
@end

NSTableColumn+Category.m

#import "NSTableColumn+Category.h"
@implementation NSTableColumn (Category)
+ (id)xx_tableColumnWithItem:(TableColumnItem*)item {
    NSTableColumn *column = [[NSTableColumn alloc]init];
    [column columnWithItem:item];
    return column;
}

- (void)columnWithItem:(TableColumnItem*)item {
    self.identifier = item.identifier;
    [self setWidth:item.width];
    [self setMinWidth:item.minWidth];
    [self setMaxWidth:item.maxWidth];
    [self setEditable:item.editable];
    
    [self updateHeaderCellWithItem:item];
}

- (void)updateHeaderCellWithItem:(TableColumnItem*)item{
    if(item.title){
        [[self headerCell] setStringValue:item.title];
    }
    [[self headerCell] setAlignment:item.headerAlignment];
    if([[self headerCell]  isKindOfClass:[NSCell class]]){
        [[self headerCell] setLineBreakMode: NSLineBreakByTruncatingMiddle];
    }
}
@end

NSTableView类Category扩展

同样的实现NSTableView的Category类,根据items创建表的NSTableColumn,然后使用addTableColumn方法完成增加列到NSTableView。

为了能够根据列的identifier获取列的定义TableColumnItem,我们使用关联数组存储了所有的items。

NSTableView+Category.h

#import <Cocoa/Cocoa.h>
@class TableColumnItem;
@interface NSTableView (Category)
//删除所有的列
-(void)xx_removeAllColumns;
//使用items定义的列更新创建列
-(void)xx_updateColumnsWithItems:(NSArray*)items;
//获取指定index索引的列
-(TableColumnItem*)xx_columnItemAtIndex:(NSInteger)colIndex;
//根据identifier获取列
-(TableColumnItem*)xx_columnItemWithIdentifier:(NSString*)identifier;
@end

NSTableView+Category.m

#import "NSTableView+Category.h"
#import "NSTableColumn+Category.h"
#import "TableColumnItem.h"
#import <objc/runtime.h>
static char kTableViewColumnDefObjectKey;

@implementation NSTableView (Category)
- (void)xx_removeAllColumns {
    while([[self tableColumns] count] > 0) {
        [self removeTableColumn:[[self tableColumns] lastObject]];
    }
}

- (void)xx_updateColumnsWithItems:(NSArray*)items {
    if(items.count<=0){
        return;
    }
    if([[self tableColumns] count]>0){
        [self xx_removeAllColumns];
    }
    objc_setAssociatedObject(self, &kTableViewColumnDefObjectKey, items, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    for(TableColumnItem *item in items){
        NSTableColumn *column=[NSTableColumn xx_tableColumnWithItem:item];
        [self addTableColumn:column];
    }
}

- (TableColumnItem*)xx_columnItemWithIdentifier:(NSString*)identifier {
     NSArray *items =  (NSArray *)objc_getAssociatedObject(self, &kTableViewColumnDefObjectKey);
    for(TableColumnItem *item in items){
        if([item.identifier isEqualToString:identifier]) {
            return item;
        }
    }
    return nil;
}

- (TableColumnItem*)xx_columnItemAtIndex:(NSInteger)colIndex {
    NSArray *items =  (NSArray *)objc_getAssociatedObject(self, &kTableViewColumnDefObjectKey);
    if(!items){
        return nil;
    }
    if(colIndex >= items.count){
        return nil;
    }
    return items[colIndex];
}

通过配置定义表列

通用的XIB加载控制器XibViewController

每一个视图控制器都需要一个根视图view做为它的所有子视图的容器。这里我们创建一个空的xib文件管理器XibViewController来负责由xib加载创建root 根视图view。

这样做的好处是其它不需要通过xib创建的视图控制器都可以直接继承这个XibViewController, 新建文件时不再需要勾选基于xib的选项了。

#import <Cocoa/Cocoa.h>
@interface XibViewController : NSViewController
@end
@implementation XibViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if(!nibNameOrNil){
        return [super initWithNibName:@"XibViewController" bundle:nil];
    }
    return [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
}
- (id)init {
    self = [super initWithNibName:@"XibViewController" bundle:nil];
    if (self) {
        
    }
    return self;
}
@end

代码动态配置表列的表格数据导航控制器

TableDataNavigationViewController继承自XibViewController。
我们也同时定义了一个DataNavigationView导航视图dataNavigationView子视图。

tableViewStyleConfig方法中对表格的样式属性做了设置。
tableViewColumnConfig方法中实现了表格列的动态加载。

#import "NoXibTableViewController.h"
#import "NSTableView+Category.h"
#import "TableColumnItem.h"

@interface TableDataNavigationViewController ()
@property(nonatomic,strong)DataNavigationView *dataNavigationView;
@end

@implementation TableDataNavigationViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupAutolayout];
    [self tableViewStyleConfig];
    [self tableViewColumnConfig];
}

- (void)tableViewStyleConfig {
    self.tableView.gridStyleMask =  NSTableViewSolidHorizontalGridLineMask | NSTableViewSolidVerticalGridLineMask;
    self.tableView.usesAlternatingRowBackgroundColors = YES;
}

- (void)tableViewColumnConfig {
    NSArray *items =[self tableColumnItems];
    if(items){
        [self.tableView xx_updateColumnsWithItems:items];
    }
}

- (void)setupAutolayout {
    //设置表视图约束
    [self.tableViewScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view.mas_left).with.offset(0);
        make.right.equalTo(self.view.mas_right).with.offset(0);
        make.top.equalTo(self.view.mas_top).with.offset(0);
        make.bottom.equalTo(self.view.mas_bottom).with.offset(-30);
    }];
    //设置导航数据面板约束
    [self.dataNavigationView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view.mas_left).with.offset(0);
        make.right.equalTo(self.view.mas_right).with.offset(0);
        make.top.equalTo(self.tableViewScrollView.mas_bottom).with.offset(0);
        make.bottom.equalTo(self.view.mas_bottom).with.offset(0);
    }];
}

#pragma mark - Config
- (NSArray*)tableColumnItems {
    TableColumnItem *field = [[TableColumnItem alloc]init];
    field.title      = @"Field";
    field.identifier = @"field";
    field.width      = 100;
    field.minWidth   = 100;
    field.maxWidth   = 120;
    field.editable   = YES;
    field.headerAlignment = NSLeftTextAlignment;
    field.cellType = TableColumnCellTypeTextField;
    
    TableColumnItem *type = [[TableColumnItem alloc]init];
    type.title      = @"Type";
    type.identifier = @"type";
    type.width      = 120;
    type.minWidth   = 120;
    type.maxWidth   = 160;
    type.editable   = YES;
    type.headerAlignment = NSLeftTextAlignment;
    type.cellType = TableColumnCellTypeComboBox;
    type.items = @[@"int",@"varchar",@"bool"];
    
    TableColumnItem *length = [[TableColumnItem alloc]init];
    length.title      = @"Size";
    length.identifier = @"size";
    length.width      = 120;
    length.minWidth   = 120;
    length.maxWidth   = 120;
    length.editable   = YES;
    length.headerAlignment = NSLeftTextAlignment;
    length.cellType = TableColumnCellTypeTextField;
    
    TableColumnItem *primary = [[TableColumnItem alloc]init];
    primary.title      = @"Primary";
    primary.identifier = @"primary";
    primary.width      = 80;
    primary.minWidth   = 80;
    primary.maxWidth   = 120;
    primary.editable   = YES;
    primary.headerAlignment = NSLeftTextAlignment;
    primary.cellType = TableColumnCellTypeCheckBox;
    
    TableColumnItem *image = [[TableColumnItem alloc]init];
    image.title      = @"Image";
    image.identifier = @"image";
    image.width      = 80;
    image.minWidth   = 80;
    image.maxWidth   = 120;
    image.cellType = TableColumnCellTypeImageView;
    
    return @[field,type,length,primary,image];
}

运行后界面结果

NSTableViewTableNoData

实现代码或xib创建tableView控制的兼容

#import "NoXibTableViewController.h"
#import "NSTableView+Category.h"
#import "TableColumnItem.h"

@interface TableDataNavigationViewController ()
@property(nonatomic,strong)NSTableView  *tableView;
@property(nonatomic,strong)NSScrollView *tableViewScrollView;
@property(nonatomic,strong)DataNavigationView *dataNavigationView;
@end

@interface TableDataNavigationViewController : NoXibTableViewController
@end

让tableView和dataNavigationView 同时支持xib方式和纯代码实现的黑魔法代码。如果要通过xib界面创建表格的话,实现下面的空方法,返回绑定的IBOutlet变量即可。同时tableViewScrollView、tableView、dataNavigationView 3个方法中都优先判断是否存在xib创建的对象,没有的话才使用代码去创建。

1.返回xib创建的视图IBOutlet绑定的变量

- (NSScrollView*)tableViewXibScrollView{
    return nil;
}
- (NSTableView*)tableXibView{
    return nil;
}

- (DataNavigationView*)dataNavigationXibView{
    return nil;
}

#pragma mark - ivars

- (NSScrollView*)tableViewScrollView{
    if(!_tableViewScrollView){
        _tableViewScrollView = [self tableViewXibScrollView];
        if(_tableViewScrollView){
            return _tableViewScrollView;
        }
        _tableViewScrollView = [[NSScrollView alloc] init];
        [_tableViewScrollView setHasVerticalScroller:NO];
        [_tableViewScrollView setHasHorizontalScroller:NO];
        [_tableViewScrollView setFocusRingType:NSFocusRingTypeNone];
        [_tableViewScrollView setAutohidesScrollers:YES];
        [_tableViewScrollView setBorderType:NSNoBorder];
        [_tableViewScrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
    }
    return _tableViewScrollView;
}

- (NSTableView*)tableView{
    if(!_tableView){
        _tableView = [self tableXibView];
        if(_tableView){
            return _tableView;
        }
        _tableView = [[NSTableView alloc] init];
        [_tableView setAutoresizesSubviews:YES];
        [_tableView setFocusRingType:NSFocusRingTypeNone];
    }
    return _tableView;
}

- (DataNavigationView *)dataNavigationView {
    if(!_dataNavigationView){
        _dataNavigationView = [self dataNavigationXibView];
        if(_dataNavigationView){
            return _dataNavigationView;
        }
        _dataNavigationView =  [[DataNavigationView alloc]init];
    }
    return _dataNavigationView;
}

2.在setupAutolayout布局设置中检查是否有xib创建的tableView。如果有的话,不进行代码控制的自动布局。

- (void)setupAutolayout {
   //如果界面使用xib方式的话,不进行自动布局的处理
    if([self tableXibView]){
        return;
    }
    ....
}

3.覆盖视图控制器init方法加载xib

比如创建了基于xib的XibDemoViewController的控制器。

- (id)init {
    self = [super initWithNibName:@"XibDemoViewController" bundle:nil];
    if (self) {
    }
    return self;
}

下面是XibDemoViewController的界面,顶部多了一个文字label Demo Xib。

TableViewControllerXibDemo

XibDemoViewController中的实现代码

@interface XibDemoViewController ()
@property (weak) IBOutlet DataNavigationView *navigationView;
@property (weak) IBOutlet NSTableView *testTableView;
@end

@implementation XibDemoViewController
- (id)init {
    self = [super initWithNibName:@"XibDemoViewController" bundle:nil];
    if (self) {
    }
    return self;
}

- (NSTableView*)tableXibView{
    return self.testTableView;
}

- (DataNavigationView*)dataNavigationXibView{
    return self.navigationView;
}

表格的数据代理TableDataDelegate

为了缩减视图控制器中代码行数,从类职责单一的角度,我们将表的代理和数据源独立成一个TableDataDelegate类。

TableDataDelegate中包括以下主要方法和属性:

  1. 几个表格数据变化Callback的回调接口
  2. 数据集的操作类接口,包括增加删除修改,数据访问等基本方法
  3. 实现了必需的代理接口
  4. 实现了必需的数据源接口

数据集管理接口

数据集是一个数组,每一行数据元素是一个动态可修改的Mutable的字典对象。

@interface TableDataDelegate : NSObject<NSTableViewDataSource,NSTableViewDelegate>

//Row Selection Changed Callback.
typedef void(^SelectionChangedCallbackBlock)(NSInteger index,  id obj);

//Row Drag Callback.
typedef void(^TableViewRowDragCallbackBlock)(NSInteger sourceRow,NSInteger targetRow);

//Row Edit Object Changed Callback.
typedef void(^RowObjectValueChangedCallbackBlock)(id obj,id oldObj,NSInteger row,NSString *fieldName);

@property(nonatomic,weak)  NSTableView *owner;

@property(nonatomic,copy)SelectionChangedCallbackBlock selectionChangedCallback;

@property(nonatomic,copy)TableViewRowDragCallbackBlock rowDragCallback;

@property(nonatomic,copy)RowObjectValueChangedCallbackBlock rowObjectValueChangedCallback;


-(void)setData:(id)data;

-(void)updateData:(id)item row:(NSInteger)row;

-(void)addData:(id)data;

-(void)deleteData:(id)data;

-(void)deleteDataAtIndex:(NSUInteger)index;

-(void)deleteDataIndexes:(NSIndexSet*)indexSet;

-(void)insertObject:(id)anObject atIndex:(NSUInteger)index;

-(void)exchangeObjectAtIndex:(NSUInteger)idx1 withObjectAtIndex:(NSUInteger)idx2;

-(void)clearData;

-(NSInteger)itemCount;

-(id)itemOfRow:(NSInteger)row;

-(NSArray*)itemsOfIndexSet:(NSIndexSet*)indexSet;

@end

items属性中存储表格当前显示的数据集

NSString * const TableViewDragDataTypeName  = @"TableViewDragDataTypeName";

@interface TableDataDelegate ()<NSTableViewDataSource,NSTableViewDelegate,NSTextFieldDelegate,NSComboBoxDelegate>
@property(nonatomic,strong)NSMutableArray *items;
@end

@implementation TableDataDelegate

- (void)dealloc {
    NSLog(@"TableDataDelegate dealloc");
}

- (id)init {
    self = [super init];
    if(self){
        _items = [[NSMutableArray alloc]initWithCapacity:4];
    }
    return  self;
}

- (void)setData:(id)data {
    if(!data){
        [self clearData];
        return;
    }
    assert([data isKindOfClass:[NSArray class]]);
    self.items = [NSMutableArray arrayWithArray:data];
}

- (void)updateData:(id)item row:(NSInteger)row {
    if(row<=(_items.count-1)){
        self.items[row] = item;
    }
}

- (void)addData:(id)data {
    if(!data){
        return ;
    }
    if([data isKindOfClass:[NSArray class]]){
        [self.items addObjectsFromArray:data];
    }
    else{
        [self.items addObject:data];
    }
}

- (void)addData:(id)data atIndex:(NSInteger)index {
    if(!data){
        return ;
    }
    [self.items insertObject:data atIndex:index];
}

- (void)deleteData:(id)data {
    if([data isKindOfClass:[NSIndexSet class]]){
        [self.items removeObjectsAtIndexes:data];
    }
    else if([data isKindOfClass:[NSArray class]]){
        NSArray *array = data;
        for(id obj in array){
            [self.items removeObject:obj];
        }
    }
    else{
        [self.items removeObject:data];
    }
}

- (void)deleteDataAtIndex:(NSUInteger)index {
    assert(index<=(self.items.count-1));
    [self.items removeObjectAtIndex:index];
}

- (void)deleteDataIndexes:(NSIndexSet*)indexSet {
    [self deleteData:indexSet];
}

- (id)itemOfRow:(NSInteger)row {
    if(row<=(_items.count-1)){
        return _items[row];
    }
    return nil;
}

- (NSArray*)itemsOfIndexSet:(NSIndexSet*)indexSet {
    NSInteger count = indexSet.count;
    if(count<=0){
        return nil;
    }
    NSMutableArray *array =[[NSMutableArray alloc]initWithCapacity:count];
    [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
        id obj = [self itemOfRow:idx];
        [array addObject:obj];
    }
    ];
    return array;
}

- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
    assert(index<=(self.items.count-1));
    [self addData:anObject atIndex:index];
}

- (void)removeObjectAtIndex:(NSUInteger)index {
    assert(index<=(self.items.count-1));
    [self deleteDataAtIndex:index];
}

- (void)exchangeObjectAtIndex:(NSUInteger)idx1 withObjectAtIndex:(NSUInteger)idx2 {
    assert(idx1<=(self.items.count-1));
    assert(idx2<=(self.items.count-1));
    [self.items exchangeObjectAtIndex:idx1 withObjectAtIndex:idx2];
}

- (void)clearData {
    self.items =  [[NSMutableArray alloc]initWithCapacity:4];
}

- (NSInteger)itemCount{
    return [self.items count];
}

数据源代理实现

#pragma  mark - NSTableViewDataSource

返回多少行数据

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    return [self.items count];
}

点击不同的行变化的通知处理,如果注册了selectionChangedCallback回调,会通知

- (void)tableViewSelectionDidChange:(NSNotification *)notification {
    NSInteger row = [notification.object selectedRow];
    if(row>=self.items.count){
        return;
    }
    id data = [self.items objectAtIndex:row];
    NSLog(@"select row notification.object=%@",data);
    if(self.selectionChangedCallback){
        self.selectionChangedCallback(row,data);
    }
}

如果设置了排序的列,会执行排序算法并重新刷新表数据

- (void)tableView:(NSTableView *)aTableView sortDescriptorsDidChange:(NSArray *)oldDescriptors {
    NSArray *sortDescriptors = [aTableView sortDescriptors];
    self.items =[NSMutableArray arrayWithArray:[self.items sortedArrayUsingDescriptors:sortDescriptors]];
    [aTableView reloadData];
}

动态创建表格内容的数据代理方法

这是一个非常关键的方法,实现了根据不同的列定义动态创建单元cell的内容视图。对于每一种类型的视图我们通过类的扩展方法封装实现。

原则上表格的每个Cell单元格都是一个NSTableCellView视图。NSTableCellView继承自NSView,扩展了一些表格背景的属性,默认包括一个NSTextField类型的textField,这就是我们在xib界面上看到创建的表格列里面有一个文本框的原因。

如果不想利用NSTableCellView提供的特性,我们完全可以实现一个自定义的视图,或者使用系统其它视图控件。

下面的代码在为了简单起见,大都直接返回了系统控件。只有创建NSComboBox时使用了NSTableCellView做为根视图。原因是如果直接返回NSComboBox,NSComboBox视图的大小控制上有问题。

#pragma mark - NSTableViewDelegate

- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
    //获取row数据
    NSDictionary *data = [self itemOfRow:row];
    //表格列的标识
    NSString *identifier = tableColumn.identifier;
    //单元格数据
    NSString *value = data[identifier];
    
    //根据表格列的标识,创建单元视图
    NSView *view = [tableView makeViewWithIdentifier:identifier owner:self];
    
    TableColumnItem *tableColumnItem = [tableView xx_columnItemWithIdentifier:identifier];
    
    switch (tableColumnItem.cellType) {
        case TableColumnCellTypeCheckBox:
        {
            NSButton *checkBoxField;
            if(!view){
                checkBoxField =  [[NSButton alloc]initCheckBoxWithItem:tableColumnItem];
                view = checkBoxField ;
            }
            else{
                checkBoxField = (NSButton*)view;
            }
            [checkBoxField setTarget:self];
            [checkBoxField setAction:@selector(checkBoxChick:)];
            if(value){
                checkBoxField.state = [value integerValue];
            }
            
        }
            break;
            
        case TableColumnCellTypeComboBox:
        {
            
            NSComboBox *comboBoxField;
            if(!view){
                view = [[NSTableCellView alloc]init];
                comboBoxField =  [[NSComboBox alloc]initComboBoxWithItem:tableColumnItem];
                comboBoxField.delegate = self;
                [view addSubview:comboBoxField];
                [comboBoxField mas_makeConstraints:^(MASConstraintMaker *make) {
                    make.left.equalTo(view.mas_left).with.offset(2);
                    make.right.equalTo(view.mas_right).with.offset(-2);
                    make.centerY.equalTo(view.mas_centerY).with.offset(0);
                    
                }];
            }
            else{
                comboBoxField = (NSComboBox*)view.subviews[0];
            }
            
            NSArray *items = tableColumnItem.items;
            if(items){
                [comboBoxField addItemsWithObjectValues:items];
            }
            if(value) {
                comboBoxField.stringValue = value;
            }
        }
            break;
            
        case TableColumnCellTypeImageView:
        {
            
            NSImageView *imageField;
            //如果不存在,创建新的textField
            if(!view){
                imageField =  [[NSImageView alloc]initImageViewWithItem:tableColumnItem];
                
                view = imageField ;
            }
            else{
                imageField = (NSImageView*)view;
            }
            
            if(value){
                //更新单元格的image
                imageField.image = (NSImage*)value;
            }
        }
            break;
            
            
        default: //默认都是文本控件
        {
            NSTextField *textField;
            //如果不存在,创建新的textField
            if(!view){
                textField =  [[NSTextField alloc]initTextFieldWithItem:tableColumnItem];
                textField.delegate = self;
                view = textField ;
            }
            else{
                textField = (NSTextField*)view;
            }
            
            textField.stringValue  = @"";
            if(value){
                //更新单元格的文本
                textField.stringValue = value;
            }
        }
            break;
    }
    
    return view;
}

NSTextField的扩展实现

#import "NSTextField+Category.h"
#import "TableColumnItem.h"
@implementation NSTextField (Category)

- (instancetype)initTextFieldWithItem:(TableColumnItem*)item; {
    self = [super init];
    [self setBezeled:NO];
    [self setDrawsBackground:NO];
    [self setEditable:item.editable];
    [self setSelectable:item.editable];
    self.identifier = item.identifier;
    return self;
}
@end

NSButton的扩展实现

#import "NSButton+Category.h"
#import "TableColumnItem.h"
@implementation NSButton (Category)

- (instancetype)initCheckBoxWithItem:(TableColumnItem*)item {
    
    self = [super init];
    [self setButtonType:NSSwitchButton];
    [self setBezelStyle:NSRegularSquareBezelStyle];
    [self setTitle:@""];
    [[self cell] setBordered:NO];
    self.identifier = item.identifier;
    return self;
}

@end

NSComboBox的扩展实现

#import "NSComboBox+Category.h"
#import "TableColumnItem.h"
@implementation NSComboBox (Category)
- (instancetype)initComboBoxWithItem:(TableColumnItem*)item {
    self = [super init];
    if (self) {
        // Initialization code here.
        [self setEditable:item.editable];
        [self setBordered:NO];
        [self setBezeled:NO];
        [self setBezelStyle:NSTextFieldRoundedBezel];
         self.identifier = item.identifier;
    }
    return self;
}
@end

NSImageView的扩展实现

#import "NSImageView+Category.h"
#import "TableColumnItem.h"
@implementation NSImageView(Category)

- (instancetype)initImageViewWithItem:(TableColumnItem*)item {
    self = [super init];
    if (self) {
        self.identifier = item.identifier;
    }
    return self;
}
@end

表格内容单元子视图的事件响应

分别为NSTextField、NSComboBox、NSButton子视图提供事件处理方法,将变化的数据存储到行数据字典中。

同时如果注册了行数据回调、则会执行回调处理

//文本输入框变化处理事件
- (void)controlTextDidChange:(NSNotification *)aNotification{
    NSTextField *field = aNotification.object;
    NSString *identifier = field.identifier;
    NSInteger row = [self.owner selectedRow];
    NSLog(@"field text = %@",field.stringValue);
    NSMutableDictionary *data = [self itemOfRow:row];
    NSMutableDictionary *oldData = [data mutableCopy];
    
    if(field.stringValue){
        data[identifier]    = field.stringValue;
    }
    
    if(self.rowObjectValueChangedCallback){
        self.rowObjectValueChangedCallback(data, oldData,row,identifier);
    }
}

//comboBox选择框处理事件
- (void)comboBoxSelectionDidChange:(NSNotification *)aNotification {
    NSComboBox *field = aNotification.object;
    NSString *identifier = field.identifier;
    NSInteger row = [self.owner selectedRow];
    NSLog(@"field text = %@",field.stringValue);
    NSMutableDictionary *data = [self itemOfRow:row];
    NSMutableDictionary *oldData = [data mutableCopy];
    if(field.stringValue){
        data[identifier]    = field.stringValue;
    }
    if(self.rowObjectValueChangedCallback){
        self.rowObjectValueChangedCallback(data, oldData,row,identifier);
    }
}

//Check Box 选择处理事件
- (IBAction)checkBoxChick:(id)sender {
    NSButton *button = (NSButton *)sender;
    NSLog(@"Form checkBoxChick=%ld",button.state);
    NSString *identifier = button.identifier;
    NSInteger row = [self.owner selectedRow];
    NSMutableDictionary *data = [self itemOfRow:row];
    NSMutableDictionary *oldData = [data mutableCopy];
    data[identifier]    = @(button.state);
    
    if(self.rowObjectValueChangedCallback){
        self.rowObjectValueChangedCallback(data, oldData,row,identifier);
    }
}

TableDataDelegate的使用

如果TableDataDelegate方法或接口不满足需要,我们可以基于它灵活扩展增加或覆盖方法实现。

我们定义一个TableDataNavigationViewDelegate类继承TableDataDelegate,实现了表格行高度的代理方法。

#import "TableDataNavigationViewDelegate.h"
@implementation TableDataNavigationViewDelegate 

- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row {
    return 24;
}

@end

在前面的TableDataNavigationViewController的基础上,使用TableDataDelegate做为tableView的代理和数据源来管理数据。

先在头文件中增加tableDataDelegate代理属性类和datas测试数据数组

@interface TableDataNavigationViewController ()
@property(nonatomic,strong)DataNavigationView *dataNavigationView;h
@property(nonatomic,strong)TableDataNavigationViewDelegate  *tableDataDelegate;
@property(nonatomic,strong)NSMutableArray *datas;
@end

配置表格的代理

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupAutolayout];
    // Do view setup here.
    [self tableViewStyleConfig];
    [self tableViewColumnConfig];
    [self tableDelegateConfig];
}

- (void)tableDelegateConfig {
    self.tableView.delegate   = self.tableDataDelegate;
    self.tableView.dataSource = self.tableDataDelegate;
}

-(TableDataNavigationViewDelegate*)tableDataDelegate {
    if(!_tableDataDelegate) {
        _tableDataDelegate = [[TableDataNavigationViewDelegate alloc]init];
        _tableDataDelegate.owner = self.tableView;
    }
    return _tableDataDelegate;
}

创建测试数据

- (void)makeTestDatas {
    
    NSImage *image = [NSImage imageNamed:NSImageNameComputer];
    NSImage *folderImage = [NSImage imageNamed:NSImageNameFolder];
    NSImage *advancedImage = [NSImage imageNamed:NSImageNameAdvanced];
    NSImage *unavailableImage = [NSImage imageNamed:NSImageNameStatusUnavailable];
    
    NSDictionary *row1 = @{ @"field":@"name",@"type":@"text",@"size":@"10",@"primary":@(1) , @"image":image};
    
    ....... 此处省略多行类似的数据
    
     NSArray *datas = @[row1,row2,row3,row4,row5,row6,row7,row8,row9,row10,row11,row12,row13,row14];
    
    self.datas = [NSMutableArray array];
    for(NSDictionary *data in datas) {
        NSMutableDictionary *dataTemp =  [NSMutableDictionary dictionaryWithDictionary:data];
        [self.datas addObject:dataTemp];
    }
}

加载数据:在controller的viewDidLoad方法中调用fetchData即可实现数据的加载显示了。

#pragma mark -- Data

数据加工处理在后台线程,执行表格数据更新UI在主线程

- (void)fetchData {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self.tableDataDelegate setData:self.datas];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];      
        });
    });
    
}

数据列排序

可以对NSTableColumn配置排序规则来实现表格点击表头排序功能。

通过NSSortDescriptor来定义排序规则。

ascending表示升序还是降序排列。selector表示排序方法选择。此处的compare为一个系统实现的默认排序算法。

NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier ascending:YES selector:@selector(compare:)];

[tableColumn setSortDescriptorPrototype:sortDescriptor];

也可以自己实现一个排序算法

NSSortDescriptor *sortStates = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier
                       ascending:NO
                       comparator:^(id obj1, id obj2) {
                       if (obj1 < obj2) {
                                   return  NSOrderedAscending;
                       }
                       if (obj1 > obj2) {
                                   return NSOrderedDescending;                                       
                       }                                              
                      return NSOrderedSame;
}];

下面是对表格每一个列都指定使用默认的排序算法

- (void)tableViewSortColumnsConfig {
    for (NSTableColumn *tableColumn in self.tableView.tableColumns ) {
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:tableColumn.identifier ascending:YES selector:@selector(compare:)];
        [tableColumn setSortDescriptorPrototype:sortDescriptor];
    }
}

仅仅定义了列的排序算法还是不够的,最终的数据排序更新UI,必需在TableDataDelegate类中添加NSTableViewDataSource的排序代理方法。

- (void)tableView:(NSTableView *)aTableView sortDescriptorsDidChange:(NSArray *)oldDescriptors {
    NSArray *sortDescriptors = [aTableView sortDescriptors];
    self.items =[NSMutableArray arrayWithArray:[self.items sortedArrayUsingDescriptors:sortDescriptors]];
    [aTableView reloadData];
}

数据行拖放交换顺序

首先需要注册NSTableView的拖放事件。

TableViewDragDataTypeName为一个自定义的字符串key,用来存储拖放事件发生时的关键数据,对于NSTableView来说,存储拖放的行索引序号。

NSString * const TableViewDragDataTypeName = @TableViewDragDataTypeName;

- (void)registerRowDrag {
  [self.tableView registerForDraggedTypes:[NSArray arrayWithObject:TableViewDragDataTypeName]];
}

2.在TableDataDelegate中实现NSTableViewDataSource数据源中代理方法

允许拖放

- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)op {

    return NSDragOperationEvery;
}

将表格行编号copy到剪切板对象

- (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard*)pboard {
    // Copy the row numbers to the pasteboard.
    NSData *zNSIndexSetData = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];
    [pboard declareTypes:[NSArray arrayWithObject:TableViewDragDataTypeName] owner:self];
    [pboard setData:zNSIndexSetData forType:TableViewDragDataTypeName];
    return YES;
}

拖放结束后,从剪切板对象获取到拖放的dragRow.

- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id <NSDraggingInfo>)info
              row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation {
    
    NSPasteboard* pboard = [info draggingPasteboard];
    NSData* rowData = [pboard dataForType:TableViewDragDataTypeName];
    NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
    NSInteger dragRow = [rowIndexes firstIndex];
    
    NSInteger count = [self itemCount];
    if(count<=1){
        return YES;
    }
    NSLog(@"dragRow = %ld row=%ld count=%ld",dragRow,row,count);
    /*drag row inside table cell row*/
    if(row<=count-1){
        [self exchangeObjectAtIndex:dragRow withObjectAtIndex:row];
        [tableView noteNumberOfRowsChanged];
        [tableView reloadData];
        if(self.rowDragCallback){
            self.rowDragCallback(row,dragRow);
        }
        return YES;
    }
    else{
        /*drag row index out of row count*/
        id zData = [[self itemOfRow:dragRow]mutableCopy];
        [self insertObject:zData atIndex:row];
        count = [self itemCount];
        [self deleteDataAtIndex:dragRow];
        count = [self itemCount];
        [tableView noteNumberOfRowsChanged];
        [tableView reloadData];
        if(self.rowDragCallback){
            self.rowDragCallback(row,dragRow);
        }
        return YES;
    }
}

拖放的源dragRow,拖放的目标row这2个参数有了以后,就可以对表的数据源数组做源dragRow和目标row交换,然后在重新加载reload即可完成。

数据分页显示控制

分页控制器

负责管理总页数、当前页索引、向前、向后、第一页、最后一页的翻页控制。

DataPageManager为分页控制器类,定义了分页处理相关属性和方法。

PaginatorDelegate分页协议定义了分页后的数据获取回调接口,应用可以实现这个接口完成分页数据的加载。它同时也定义了总的数据量数目的接口,辅助DataPageManager通过computePageNumbers完成计算分页。

DataPageManager定义pageSize属性为读写,因此外部使用者可以规定每页的数据大小。


@protocol PaginatorDelegate
@required
- (void)paginator:(id)paginator requestDataWithPage:(NSInteger)page pageSize:(NSInteger)pageSize;
- (NSInteger)totalNumberOfData:(id)paginator;
@end

@interface DataPageManager : NSObject
@property (weak) id <PaginatorDelegate>delegate;
@property(nonatomic,readonly)NSInteger  page;//current page index
@property(nonatomic,assign) NSInteger  pageSize;//row numbers of each page
@property(nonatomic,readonly)NSInteger  pages;//total pages
@property(nonatomic,readonly)NSInteger  total;//total data rows
- (id)initWithPageSize:(NSInteger)pageSize delegate:(id<PaginatorDelegate>)paginatorDelegate;
- (BOOL)isFirstPage;
- (BOOL)isLastPage;
- (BOOL)goPage:(NSInteger)index;
- (BOOL)goNextPage;
- (BOOL)goPrePage;
- (BOOL)goFirstPage;
- (BOOL)goLastPage;
- (void)reset;
- (void)refreshCurrentPage;
- (void)computePageNumbers;
@end

goPage:方法完成page的有效性检查,保存当前的页,执行分页数据回调。goNextPage,goPrePage,goFirstPag,goLastPage仅仅是调用goPage:方法完成数据导航的。

下面是几个关键代码的实现。

#import "DataPageManager.h"

@implementation DataPageManager

- (id)initWithPageSize:(NSInteger)pageSize delegate:(id<PaginatorDelegate>)paginatorDelegate {
    self = [super init];
    if(self){
        _page = 0;
        _delegate = paginatorDelegate;
        _pageSize = pageSize;
    }
    return self;
}

- (BOOL)goPage:(NSInteger)index {
    if(self.pages >0 && index<=self.pages-1){
        self.page = index;
        if(self.delegate) {
            [self.delegate paginator:self requestDataWithPage:self.page pageSize:self.pageSize];
        }
        return YES;
    }
    return NO;
}

- (BOOL)goFirstPage {
    return [self goPage:0];
}

- (BOOL)goLastPage {
    if(self.pages>1){
        return [self goPage:self.pages-1];
    }
    return NO;
}

- (BOOL)goNextPage {
    if(![self isLastPage]){
        return [self goPage:self.page+1];
    }
    return NO;
}

- (BOOL)goPrePage {
    if(![self isFirstPage]){
        return [self goPage:self.page-1];
    }
    return NO;
}

- (void)computePageNumbers {
    [self reset];
    self.total = [self.delegate totalNumberOfData:self];
    if(self.pageSize>0){
        self.pages = ceil((double)self.total/(double)self.pageSize);
    }
    else{
        self.pages = 0;
    }
}

分页数据获取

/*
 一个基本的数组分页算法
 */
- (NSArray*)findByPage:(NSInteger)index pageSize:(NSInteger)pageSize {
    NSInteger  count = [self.datas count];
    if(count<=0){
        return [NSArray array];
    }
    
    NSInteger  fromIndex = index * pageSize;
    NSInteger  toIndex   = fromIndex + pageSize;
    
    NSInteger  validPageSize = pageSize;
    
    if(toIndex>count){
        validPageSize = count - fromIndex ;
    }
    NSArray *subDatas = [self.datas subarrayWithRange:NSMakeRange(fromIndex, validPageSize)];
    return subDatas;
}

分页数据查询,更新表数据。数据GCD后台查询数据,主线程更新UI。

- (void)fetchData {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *tableDefs = [self.pageManager currentDatas];
        [self.dataDelegate setData:tableDefs];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];
            [self updatePageInfo];
        });
    });
    
}

分页代理协议回调方法

- (void)paginator:(id)paginator requestDataWithPage:(NSInteger)page pageSize:(NSInteger)pageSize {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *datas =  [self findByPage:page pageSize:pageSize];
        [self.dataDelegate setData:datas];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];
            [self updatePageInfo];
        });
    });
}

- (NSInteger)totalNumberOfData:(id)paginator {
    return  [self.datas count];
}

分页导航视图

在TableDataNavigationViewController中定义DataPageManager属性变量

@property(nonatomic,strong)DataPageManager *pageManager;

- (DataPageManager*)pageManager{
    if(!_pageManager){
        _pageManager = [[DataPageManager alloc]initWithPageSize:kPageSize delegate:self];
    }
    return _pageManager;
}

- (void)computePageNumbers {
    [self.pageManager computePageNumbers];
}

在viewDidLoad方法中进行分页相关初始化

}

使用DataNavigationView配置功能按钮实现分页导航视图界面,注册按钮响应事件如下:

- (IBAction)toolButtonClicked:(id)sender {
    NSButton *button = sender;
    NSLog(@"button.tag %ld",button.tag);
    DataNavigationViewButtonActionType actionType = button.tag;
    switch (actionType) {
        case DataNavigationViewAddActionType:
        {
            [self addNewData];
        }
            break;
        case DataNavigationViewRemoveActionType:
        {
            [self reomoveSelectedData];
        }
            break;
        case DataNavigationViewRefreshActionType:
        {
            [self.pageManager refreshCurrentPage];
        }
            break;
        case DataNavigationViewFirstPageActionType:
        {
            [self.pageManager goFirstPage];
        }
            break;
        case DataNavigationViewPrePageActionType:
        {
            [self.pageManager goPrePage];
        }
            break;
        case DataNavigationViewNextPageActionType:
        {
            [self.pageManager goNextPage];
        }
            break;
        case DataNavigationViewLastPageActionType:
        {
            [self.pageManager goLastPage];
        }
            break;
        default:
            break;
    }
}


可以看出代码相当简单,由分页控制器实现翻页控制,然后调用refreshDataView->fetchData来实现数据查询刷新UI。

表增删修改数据管理

增加数据

  1. 创建一个新的行数据对象,添加到数据源。
  2. 重新加载表。
  3. 更新分页导航控件上的总数据行数。
  4. 设置新建数据所在行的第一列为选中状态
- (void)addNewData {
    NSMutableDictionary *data = [NSMutableDictionary dictionary];
    [self.datas addObject:data];
    [self.tableDataDelegate addData:data];
    [self.tableView reloadData];
    [self computePageNumbers];
    [self updatePageInfo];
    [self.tableView xx_setEditFoucusAtColumn:0];
}

NSTableView扩展中增加实现编辑时选择当前行所在列功能方法

- (void)xx_setEditFoucusAtColumn:(NSInteger)columnIndex {
    if(self.numberOfRows<=0){
        return;
    }
    [self selectRowIndexes:[NSIndexSet indexSetWithIndex:[self numberOfRows]-1] byExtendingSelection:NO];
    [self editColumn:columnIndex row:([self numberOfRows] - 1) withEvent:nil select:YES];
}

删除数据

获取表格选中的行selectedRow,调用tableView的removeRowsAtIndexes方法删除选择的行。获取selectedRow在数据源中对应的数据对象,先从数据库删除,在从数据源删除,重新加载表更新分页数据。

- (void)reomoveSelectedData {
    
    NSInteger selectedRow = self.tableView.selectedRow;
    //没有行选择,不执行删除操作
    if(selectedRow==NSNotFound){
        return;
    }
    //删除选中的行失去焦点
    [self.tableView xx_setLostEditFoucus];
    
    //开始删除
    [self.tableView beginUpdates];
    //以指定的动画风格执行删除
    NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:selectedRow];
    [self.tableView removeRowsAtIndexes:indexes withAnimation:NSTableViewAnimationSlideUp];
    //完成删除
    [self.tableView endUpdates];

    
    id data = [self.tableDataDelegate itemOfRow:selectedRow];
    
    [self.tableDataDelegate deleteDataAtIndex:selectedRow];
    
    [self.datas removeObject:data];
    //更新页数据
    [self computePageNumbers];
    [self updatePageInfo];
    
}

NSTableView扩展中增加失去焦点功能方法

- (void)xx_setLostEditFoucus {
    NSInteger row = [self selectedRow];
    if(row<0){
        return;
    }
    NSIndexSet *indexes = [self selectedRowIndexes];
    [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
        [self deselectRow:row];
    }
    ];
    NSInteger col = [self selectedColumn];
    [self deselectColumn:col];
}

编辑表格数据

在TableDataDelegate代理中已经实现表数据编辑的基本处理,只要注册了代理回调方法rowObjectValueChangedCallback,就可以实现数据变化后逻辑处理。

在TableDataNavigationViewController中定义代理的行数据变化块

- (void)registerDataDelegateCallback {
    self.tableDataDelegate.rowObjectValueChangedCallback = ^(id obj,id oldObj,NSInteger row,NSString *fieldName){
        
        //(自处可以实现数据有效性检查或者数据存储等业务逻辑处理)

    };
    
}