How to create NSCollectionView programmatically from scratch?

前端 未结 3 994
傲寒
傲寒 2020-12-13 02:32

NSCollectionView remains one of the most mysterious parts of the Cocoa API that I\'ve ever seen. Documentation is poor and there are many moving parts, many of

相关标签:
3条回答
  • 2020-12-13 02:41

    I’m not sure there’s much insight in creating a collection view programmatically and without bindings, but here it goes.

    Introduction

    There are essentially four components when using a collection view:

    • View: a subclass of NSView, responsible for displaying information;
    • The collection view itself;
    • View controller: a subclass of NSCollectionViewItem that serves as the collection view item prototype;
    • Model: an array of objects.

    Usually a view is designed in Interface Builder, and a model is mediated by Cocoa bindings.

    Doing it programmatically:

    Constants

    static const NSSize buttonSize = {80, 20};
    static const NSSize itemSize = {100, 40};
    static const NSPoint buttonOrigin = {10, 10};
    

    View

    This is a standard view (a custom view in Interface Builder parlance) containing a button. Note that the view has fixed size.

    @interface BVView : NSView
    @property (weak) NSButton *button;
    @end
    
    @implementation BVView
    @synthesize button;
    - (id)initWithFrame:(NSRect)frameRect {
        self = [super initWithFrame:(NSRect){frameRect.origin, itemSize}];
        if (self) {
            NSButton *newButton = [[NSButton alloc] 
                initWithFrame:(NSRect){buttonOrigin, buttonSize}];
            [self addSubview:newButton];
            self.button = newButton;
        }
        return self;
    }
    @end
    

    View Controller (Prototype)

    Normally a view controller loads its view from a nib file. In the rare cases where the view controller doesn’t obtain its view from a nib file, the developer must either send it -setView: before -view is received by the view controller, or override -loadView. The following code does the latter.

    View controllers receive the corresponding model object via -setRepresentedObject:. I’ve overridden it so as to update the button title whenever the model object changes. Note that this can be accomplished by using Cocoa bindings without any code at all.

    Note that none of this code is specific to collection views — it’s general view controller behaviour.

    @interface BVPrototype : NSCollectionViewItem
    @end
    
    @implementation BVPrototype
    - (void)loadView {
        [self setView:[[BVView alloc] initWithFrame:NSZeroRect]];
    }
    - (void)setRepresentedObject:(id)representedObject {
        [super setRepresentedObject:representedObject];
        [[(BVView *)[self view] button] setTitle:representedObject];
    }
    @end
    

    Model

    A simple array of strings representing button titles:

    @property (strong) NSArray *titles;
    self.titles = [NSArray arrayWithObjects:@"Case", @"Molly", @"Armitage",
        @"Hideo", @"The Finn", @"Maelcum", @"Wintermute", @"Neuromancer", nil];
    

    Collection View

    So far, the only relation that’s been established is the view (BVView) used by the item prototype (BVPrototype). The collection view must be informed of the prototype it should be using as well as the model from which to obtain data.

    NSCollectionView *cv = [[NSCollectionView alloc]
        initWithFrame:[[[self window] contentView] frame]]; 
    [cv setItemPrototype:[BVPrototype new]];
    [cv setContent:[self titles]];
    

    Full Source Code for the Application Delegate

    #import "BVAppDelegate.h"
    
    
    static const NSSize buttonSize = { 80, 20 };
    static const NSSize itemSize = { 100, 40 };
    static const NSPoint buttonOrigin = { 10, 10 };
    
    
    @interface BVView : NSView
    @property (weak) NSButton *button;
    @end
    
    @implementation BVView
    @synthesize button;
    - (id)initWithFrame:(NSRect)frameRect {
        self = [super initWithFrame:(NSRect){frameRect.origin, itemSize}];
        if (self) {
            NSButton *newButton = [[NSButton alloc]
                initWithFrame:(NSRect){buttonOrigin, buttonSize}];
            [self addSubview:newButton];
            self.button = newButton;
        }
        return self;
    }
    @end
    
    
    @interface BVPrototype : NSCollectionViewItem
    @end
    
    @implementation BVPrototype
    - (void)loadView {
        [self setView:[[BVView alloc] initWithFrame:NSZeroRect]];
    }
    - (void)setRepresentedObject:(id)representedObject {
        [super setRepresentedObject:representedObject];
        [[(BVView *)[self view] button] setTitle:representedObject];
    }
    @end
    
    
    @interface BVAppDelegate ()
    @property (strong) NSArray *titles;
    @end
    
    @implementation BVAppDelegate
    
    @synthesize window = _window;
    @synthesize titles;
    
    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
        self.titles = [NSArray arrayWithObjects:@"Case", @"Molly", @"Armitage",
            @"Hideo", @"The Finn", @"Maelcum", @"Wintermute", @"Neuromancer", nil];
    
        NSCollectionView *cv = [[NSCollectionView alloc]
            initWithFrame:[[[self window] contentView] frame]]; 
        [cv setItemPrototype:[BVPrototype new]];
        [cv setContent:[self titles]];
    
        [cv setAutoresizingMask:(NSViewMinXMargin
                                 | NSViewWidthSizable
                                 | NSViewMaxXMargin
                                 | NSViewMinYMargin
                                 | NSViewHeightSizable
                                 | NSViewMaxYMargin)];
        [[[self window] contentView] addSubview:cv];
    }
    
    @end
    
    0 讨论(0)
  • 2020-12-13 02:52

    To answer brigadir's question on how to bind to a mutable array.

    zero'th - make titles an NSMutableArray

    first - bind the array to your items

    [cv bind:NSContentBinding 
        toObject:self 
        withKeyPath:@"titles" 
        options:NULL];
    

    Second - when altering titles, make sure to modify the proxy.

    e.g.

    NSMutableArray *kvcTitles = [self mutableArrayValueForKey:@"titles"];
    [kvcTitles removeLastObject];
    
    0 讨论(0)
  • 2020-12-13 03:02

    @Bavarious You did an excellent job there. This was just an amazing tutorial which I sometimes miss at the Apple Docs.

    I rewrote Bavarious' code in Swift (v2) for anyone who's interested:

    // AppDelegate.swift:

    import Cocoa
    
    let buttonSize:NSSize = NSSize(width: 80, height: 20)
    let itemSize:NSSize = NSSize(width: 100, height: 40)
    let buttonOrigin:NSPoint = NSPoint(x: 10, y: 10)
    
    let titles:[String] = ["Case", "Molly", "Armitage", "Hideo", "The Finn", "Maelcum", "Wintermute", "Neuromancer"]
    
    @NSApplicationMain
    class AppDelegate: NSObject, NSApplicationDelegate {
    
        @IBOutlet weak var window: NSWindow!
    
        func applicationDidFinishLaunching(aNotification: NSNotification) {
            let cv = NSCollectionView(frame: self.window.contentView!.frame)
            cv.itemPrototype = BVTemplate()
            cv.content = titles
    
            cv.autoresizingMask = NSAutoresizingMaskOptions.ViewMinXMargin
                .union(NSAutoresizingMaskOptions.ViewWidthSizable)
                .union(NSAutoresizingMaskOptions.ViewMaxXMargin)
                .union(NSAutoresizingMaskOptions.ViewMinYMargin)
                .union(NSAutoresizingMaskOptions.ViewMaxYMargin)
                .union(NSAutoresizingMaskOptions.ViewHeightSizable)
    
            window.contentView!.addSubview(cv)
        }
    
        func applicationWillTerminate(aNotification: NSNotification) {
            // Insert code here to tear down your application
        }
    }
    

    // BVTemplate.swift:

    import Cocoa
    
    class BVTemplate: NSCollectionViewItem {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do view setup here.
        }
    
        override func loadView() {
            print("loadingView")
            self.view = BVView(frame: NSZeroRect)
        }
    
        override var representedObject:AnyObject? {
            didSet {
                if let representedString = representedObject as? String {
                    (self.view as! BVView).button?.title = representedString
                }
            }
        }
    }
    

    // BVView.swift:

    import Cocoa
    
    class BVView: NSView {
    
        var button:NSButton?
    
        override init(frame frameRect: NSRect) {
            super.init(frame: NSRect(origin: frameRect.origin, size: itemSize))
            let newButton:NSButton = NSButton(frame: NSRect(origin: buttonOrigin, size: buttonSize))
            self.addSubview(newButton)
            self.button = newButton
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
        }
    }
    
    0 讨论(0)
提交回复
热议问题