NSOutlineView 实现文件管理器层级视图

效果图

Data Model

JZiCloudFileSystemItem.h

@interface JZiCloudFileSystemItem : NSObject

@property (nonatomic,strong) NSString *relativePath;
@property (nonatomic,weak) JZiCloudFileSystemItem *parent;
@property (nonatomic,strong) NSMutableArray *children;
@property (nonatomic,strong) NSMutableArray *childrenFolderOnly;
@property (nonatomic,strong) NSMutableArray *childrenMDOnly;

+ (JZiCloudFileSystemItem *)rootItem;

- (NSInteger)numberOfChildren;// Returns -1 for leaf nodes
- (JZiCloudFileSystemItem *)childAtIndex:(NSUInteger)n; // Invalid to call on leaf nodes

// 当前 JZiCloudFileSystemItem 文件夹内的 Markdown 数量
- (NSInteger)numberOfChildrenMD;
// 当前 JZiCloudFileSystemItem 文件夹内的 文件夹 数量
- (NSInteger)numberOfChildrenFolder;// Returns -1 for leaf nodes
- (JZiCloudFileSystemItem *)childFolderAtIndex:(NSUInteger)n; // Invalid to call on leaf nodes

- (NSString *)fullPath;
- (NSString *)relativePath;
- (void)refresh;

@end

JZiCloudFileSystemItem.m

@implementation JZiCloudFileSystemItem
@synthesize relativePath,parent,children,childrenFolderOnly,childrenMDOnly;

static JZiCloudFileSystemItem *rootItem = nil;
static NSMutableArray *leafNode = nil;

+ (void)initialize {
    if (self == [JZiCloudFileSystemItem class])
    {
        leafNode = [[NSMutableArray alloc] init];
    }
}

- (id)initWithPath:(NSString *)path parent:(JZiCloudFileSystemItem *)parentItem {
    self = [super init];
    if (self) {
        relativePath = [[path lastPathComponent] copy];
        parent = parentItem;
    }
    return self;
}
- (void)refresh
{
    rootItem = nil;
}

+ (JZiCloudFileSystemItem *)rootItem {
    if (rootItem == nil)
    {
        NSString *icloudRoot = [[[JZiCloudStorageManager sharedManager] ubiquitousDocumentsURL] path];
        rootItem = [[JZiCloudFileSystemItem alloc] initWithPath:icloudRoot parent:nil];
    }
    return rootItem;
}


// Creates, caches, and returns the array of children
// Loads children incrementally
- (NSArray *)children {
    
    if (children == nil)
    {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSString *fullPath = [self fullPath];
        BOOL isDir, valid;
        
        valid = [fileManager fileExistsAtPath:fullPath isDirectory:&isDir];
        
        if (valid && isDir) {
            NSArray *array = [fileManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:fullPath isDirectory:YES]
                                        includingPropertiesForKeys:[NSArray arrayWithObject:NSURLNameKey]
                                                           options:NSDirectoryEnumerationSkipsHiddenFiles
                                                             error:nil];

            
            NSUInteger numChildren, i;
            
            numChildren = [array count];
            children = [[NSMutableArray alloc] initWithCapacity:numChildren];
            
            for (i = 0; i < numChildren; i++)
            {
                JZiCloudFileSystemItem *newChild = [[JZiCloudFileSystemItem alloc]
                                            initWithPath:[[array objectAtIndex:i] path] parent:self];
                [children addObject:newChild];
            }
        }
        else {
            children = leafNode;
        }
    }
    return children;
}
- (NSArray *)childrenFolderOnly
{
    
    if (childrenFolderOnly == nil)
    {
        childrenFolderOnly = [[NSMutableArray alloc] initWithCapacity:[[self children] count]];
        for (NSUInteger i = 0; i < [[self children] count]; i++)
        {
            NSString *path = [[[self children] objectAtIndex:i] fullPath];
            NSURL *url = [NSURL fileURLWithPath:path];
            BOOL isDirectory;
            BOOL fileExistsAtPath = [[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&isDirectory];
            if (isDirectory && fileExistsAtPath)
            {
                [childrenFolderOnly addObject:[[self children] objectAtIndex:i]];
            }
        }
    }
    return childrenFolderOnly;
}

- (NSArray *)childrenMDOnly
{
    if (childrenMDOnly == nil)
    {
        childrenMDOnly = [[NSMutableArray alloc] initWithCapacity:[[self children] count]];
        for (NSUInteger i = 0; i < [[self children] count]; i++)
        {
            NSString *path = [[[self children] objectAtIndex:i] fullPath];
            NSURL *url = [NSURL fileURLWithPath:path];
            BOOL isDirectory;
            BOOL fileExistsAtPath = [[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&isDirectory];
            BOOL isMarkdown = ([[url pathExtension] compare: @"md"] == NSOrderedSame);
            if (fileExistsAtPath & isMarkdown)
            {
                [childrenMDOnly addObject:[[self children] objectAtIndex:i]];
            }
        }
    }
    return childrenMDOnly;
}
- (NSString *)relativePath {
    return relativePath;
}


- (NSString *)fullPath {
    // If no parent, return our own relative path
    if (parent == nil)
    {
        return [[[JZiCloudStorageManager sharedManager] ubiquitousDocumentsURL] path];
    }
    
    // recurse up the hierarchy, prepending each parent’s path
    return [[parent fullPath] stringByAppendingPathComponent:relativePath];
}


- (JZiCloudFileSystemItem *)childAtIndex:(NSUInteger)n {
    return [[self children] objectAtIndex:n];
}
- (JZiCloudFileSystemItem *)childFolderAtIndex:(NSUInteger)n
{
    return [[self childrenFolderOnly] objectAtIndex:n];
}


- (NSInteger)numberOfChildren
{
    NSArray *tmp = [self children];
    return (tmp == leafNode) ? (-1) : [tmp count];
}
- (NSInteger)numberOfChildrenMD
{
    NSArray *tmp = [self childrenMDOnly];
    
    return (tmp == leafNode) ? (-1) : [tmp count];
}
- (NSInteger)numberOfChildrenFolder
{
    NSArray *tmp = [self childrenFolderOnly];
    return (tmp == leafNode) ? (-1) : [tmp count];
}


@end

NSOutlineViewDataSource

#pragma mark - NSOutlineViewDataSource
- (NSInteger)outlineView:(NSOutlineView *)outlineView
  numberOfChildrenOfItem:(id)item
{
    return (item == nil) ? 1 : [item numberOfChildrenFolder];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView
   isItemExpandable:(id)item
{
    return (item == nil) ? YES : ([item numberOfChildrenFolder] != 0);
}
- (id)outlineView:(NSOutlineView *)outlineView
            child:(NSInteger)index
           ofItem:(id)item
{
    return (item == nil) ? [JZiCloudFileSystemItem rootItem] : [(JZiCloudFileSystemItem *)item childFolderAtIndex:index];
}
- (id)outlineView:(NSOutlineView *)outlineView
objectValueForTableColumn:(NSTableColumn *)tableColumn
           byItem:(id)item
{
    return (item == nil) ? @"/" : [item relativePath];
}

NSOutlineViewDelegate

- (NSView *)outlineView:(NSOutlineView *)outlineView
     viewForTableColumn:(NSTableColumn *)tableColumn
                   item:(id)item
{
    if (NO)
    {}
    else
    {
        JZFolderOutlineCellView *view = [self.outlineView makeViewWithIdentifier:@"DataCell" owner:self];
        view.textField.stringValue = [item relativePath];
        view.childCountButton.title = [NSString stringWithFormat:@"%ld",(long)[item numberOfChildrenMD]];
        return view;
    }
    
}
- (CGFloat)outlineView:(NSOutlineView *)outlineView
     heightOfRowByItem:(id)item
{
    return 30.0f;
}

Storyboard

View Based, Identifier = @“DataCell”