Selection Highlight in NSCollectionView - objective-c

I have a working NSCollectionView with one minor, but critical, exception. Getting and highlighting the selected item within the collection.
I've had all this working prior to Snow Leopard, but something appears to have changed and I can't quite place my finger on it, so I took my NSCollectionView right back to a basic test and followed Apple's documentation for creating an NSCollectionView here:
http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/CollectionViews/Introduction/Introduction.html
The collection view works fine following the quick start guide. However, this guide doesn't discuss selection other than "There are such features as incorporating image views, setting objects as selectable or not selectable and changing colors if they are selected".
Using this as an example I went to the next step of binding the Array Controller to the NSCollectionView with the controller key selectionIndexes, thinking that this would bind any selection I make between the NSCollectionView and the array controller and thus firing off a KVO notification. I also set the NSCollectionView to be selectable in IB.
There appears to be no selection delegate for NSCollectionView and unlike most Cocoa UI views, there appears to be no default selected highlight.
So my problem really comes down to a related issue, but two distinct questions.
How do I capture a selection of an item?
How do I show a highlight of an item?
NSCollectionView's programming guides seem to be few and far between and most searches via Google appear to pull up pre-Snow Leopard implementations, or use the view in a separate XIB file.
For the latter (separate XIB file for the view), I don't see why this should be a pre-requisite otherwise I would have suspected that Apple would not have included the view in the same bundle as the collection view item.
I know this is going to be a "can't see the wood for the trees" issue - so I'm prepared for the "doh!" moment.
As usual, any and all help much appreciated.
Update 1
OK, so I figured finding the selected item(s), but have yet to figure the highlighting. For the interested on figuring the selected items (assuming you are following the Apple guide):
In the controller (in my test case the App Delegate) I added the following:
In awakeFromNib
[personArrayController addObserver:self
forKeyPath:#"selectionIndexes"
options:NSKeyValueObservingOptionNew
context:nil];
New Method
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if([keyPath isEqualTo:#"selectionIndexes"])
{
if([[personArrayController selectedObjects] count] > 0)
{
if ([[personArrayController selectedObjects] count] == 1)
{
personModel * pm = (PersonModel *)
[[personArrayController selectedObjects] objectAtIndex:0];
NSLog(#"Only 1 selected: %#", [pm name]);
}
else
{
// More than one selected - iterate if need be
}
}
}
Don't forget to dealloc for non-GC
-(void)dealloc
{
[personArrayController removeObserver:self
forKeyPath:#"selectionIndexes"];
[super dealloc];
}
Still searching for the highlight resolution...
Update 2
Took Macatomy's advice but still had an issue. Posting the relevant class methods to see where I've gone wrong.
MyView.h
#import <Cocoa/Cocoa.h>
#interface MyView : NSView {
BOOL selected;
}
#property (readwrite) BOOL selected;
#end
MyView.m
#import "MyView.h"
#implementation MyView
#synthesize selected;
-(id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
-(void)drawRect:(NSRect)dirtyRect
{
NSRect outerFrame = NSMakeRect(0, 0, 143, 104);
NSRect selectedFrame = NSInsetRect(outerFrame, 2, 2);
if (selected)
[[NSColor yellowColor] set];
else
[[NSColor redColor] set];
[NSBezierPath strokeRect:selectedFrame];
}
#end
MyCollectionViewItem.h
#import <Cocoa/Cocoa.h>
#class MyView;
#interface MyCollectionViewItem : NSCollectionViewItem {
}
#end
"MyCollectionViewItem.m*
#import "MyCollectionViewItem.h"
#import "MyView.h"
#implementation MyCollectionViewItem
-(void)setSelected:(BOOL)flag
{
[(MyView *)[self view] setSelected:flag];
[(MyView *)[self view] setNeedsDisplay:YES];
}
#end

If a different background color will suffice as a highlight, you could simply use an NSBox as the root item for you collection item view.
Fill the NSBox with the highlight color of your choice.
Set the NSBox to Custom so the fill will work.
Set the NSBox to transparent.
Bind the transparency attribute of the NSBox to the selected attribute of File Owner(Collection Item)
Set the value transformer for the transparent binding to NSNegateBoolean.
I tried to attach Interface builder screenshots but I was rejected bcos I'm a newbie :-(

Its not too hard to do. Make sure "Selection" is enabled for the NSCollectionView in Interface Builder. Then in the NSView subclass that you are using for your prototype view, declare a property called "selected" :
#property (readwrite) BOOL selected;
UPDATED CODE HERE: (added super call)
Subclass NSCollectionViewItem and override -setSelected:
- (void)setSelected:(BOOL)flag
{
[super setSelected:flag];
[(PrototypeView*)[self view] setSelected:flag];
[(PrototypeView*)[self view] setNeedsDisplay:YES];
}
Then you need to add code in your prototype view's drawRect: method to draw the highlight:
- (void)drawRect:(NSRect)dirtyRect
{
if (selected) {
[[NSColor blueColor] set];
NSRectFill([self bounds]);
}
}
That just simply fills the view in blue when its selected, but that can be customized to draw the highlight any way you want. I've used this in my own apps and it works great.

You can also go another way, if you're not subclassing NSView for your protoype view.
In your subclassed NSCollectionViewItem override setSelected:
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
if (selected)
self.view.layer.backgroundColor = [NSColor redColor].CGColor;
else
self.view.layer.backgroundColor = [NSColor clearColor].CGColor;
}
And of course, as said by all the wise people before me, make sure "Selection" is enabled for the NSCollectionView in Interface Builder.

In your NSCollectionViewItem subclass, override isSelected and change background color of the layer. Test in macOS 10.14 and Swift 4.2
class Cell: NSCollectionViewItem {
override func loadView() {
self.view = NSView()
self.view.wantsLayer = true
}
override var isSelected: Bool {
didSet {
self.view.layer?.backgroundColor = isSelected ? NSColor.gray.cgColor : NSColor.clear.cgColor
}
}
}

Since none of the existing answers worked super well for me, here is my take on it. Change the subclass of the CollectionView item to SelectableCollectionViewItem. Here is it's code. Comes with a bindable textColor property for hooking your text label textColor binding to.
#implementation SelectableCollectionViewItem
+ (NSSet *)keyPathsForValuesAffectingTextColor
{
return [NSSet setWithObjects:#"selected", nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.wantsLayer = YES;
}
- (void) viewDidAppear
{
// seems the inital selection state is not done by Apple in a KVO compliant manner, update background color manually
[self updateBackgroundColorForSelectionState:self.isSelected];
}
- (void)updateBackgroundColorForSelectionState:(BOOL)flag
{
if (flag)
{
self.view.layer.backgroundColor = [[NSColor alternateSelectedControlColor] CGColor];
}
else
{
self.view.layer.backgroundColor = [[NSColor clearColor] CGColor];
}
}
- (void)setSelected:(BOOL)flag
{
[super setSelected:flag];
[self updateBackgroundColorForSelectionState:flag];
}
- (NSColor*) textColor
{
return self.selected ? [NSColor whiteColor] : [NSColor textColor];
}

In my case I wanted an image(check mark) to indicate selection of object. Drag an ImageWell to the Collection Item nib. Set the desired image and mark it as hidden. Go to bindings inspector and bind hidden attribute to Collection View Item.
(In my case I had created a separate nib for CollectionViewItem, so its binded to File's owner. If this is not the case and Item view is in the same nib as the CollectionView then bind to Collection View Item)
Set model key path as selected and Value transformer to NSNegateBoolean. Thats it now whenever the individual cells/items are selected the image will be visible, hence indicating the selection.
Adding to Alter's answer.
To set NSBox as root item. Simply create a new IB document(say CollectionItem) and drag an NSBox to the empty area. Now add all the elements as required inside the box. Now click on File's Owner and set Custom Class as NSCollectionViewItem.
And in the nib where NSCollectionView is added change the nib name for CollectionViewItem
In the NSBox, bind the remaining elements to Files Owner. For a label it would be similar to :
Now to get the highlight color as Alter mentioned in his answer, set desired color combination in the Fill Color option, set the NSBox to transparent and bind the transparency attribute as below:
Now when Collection View Items are selected you should be able to see the fill color of the box.

This was awesome, thanks alot! i was struggling with this!
To clarify for to others:
[(PrototypeView*)[self view] setSelected:flag];
[(PrototypeView*)[self view] setNeedsDisplay:YES];
Replace PrototypeView* with the name of your prototype class name.

In case you are digging around for the updated Swift solution, see this response.
class MyViewItem: NSCollectionViewItem {
override var isSelected: Bool {
didSet {
self.view.layer?.backgroundColor = (isSelected ? NSColor.blue.cgColor : NSColor.clear.cgColor)
}
}
etc...
}

Here is the complete Swift NSCollectionViewItem with selection. Don't forget to set the NSCollectioView to selectable in IB or programmatically.
Tested under macOS Mojave (10.14) and High Sierra (10.13.6).
import Cocoa
class CollectionViewItem: NSCollectionViewItem {
private var selectionColor : CGColor {
let selectionColor : NSColor = (isSelected ? .alternateSelectedControlColor : .clear)
return selectionColor.cgColor
}
override var isSelected: Bool {
didSet {
super.isSelected = isSelected
updateSelection()
// Do other stuff if needed
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.wantsLayer = true
updateSelection()
}
override func prepareForReuse() {
super.prepareForReuse()
updateSelection()
}
private func updateSelection() {
view.layer?.backgroundColor = self.selectionColor
}
}

Related

How to create a custom NSView for NSSavePanel in Cocoa MacOS objective C?

I need to add a save extension selector with a text label next to it to my NSSavePanel. In the screenshot attached I try to demonstrate that I succeeded in adding an NSComboBox to my panel with the function setAccessoryView. However I have no idea how to create a custom NSView, which includes both an NSComboBox and an NSTextView or equivalent. I found no tutorials on the internet (or if I found one it was extremely outdated) showing how to create custom NSViews in objective-C in Cocoa on MacOS.
How can I create a custom NSView containing a combobox and a text label? Or how can I add two "stock" NSViews to the same NSSavePanel? Please be as detailed in your answer as possible, as I have very limited objective-c experience.
You asked how to create an NSView in Objective-C with an NSTextField and an NSComboBox as subviews.
Basically, you could define them in Interface Builder and programmatically set the resulting view in Objective-C as the accessoryView of the NSSavePanel. Alternatively, the custom NSView could be created entirely in Objective-C, which is probably the easier option here.
After instantiating an NSView, you can use addSubview: to add an NSTextField and an NSComboBox accordingly. Then you can use NSLayoutConstraints to set up Auto Layout, which takes care of sizing the accessoryView and arranging the subviews properly based on the width of the dialog.
If you create the views programmatically and use Auto Layout, you must explicitly set translatesAutoresizingMaskIntoConstraints to NO.
Should you want to set the allowedContentTypes, a textual mapping of the displayed extension to UTType via a NSDictionary might be useful.
If you set the delegate of the NSComboBox to self, then you will be informed about changes of the user selection in the NSComboBox via comboBoxSelectionDidChange:.
If the things discussed are implemented appropriately in code, it might look something like this for a self-contained example:
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "ViewController.h"
#interface ViewController () <NSComboBoxDelegate>
#property (nonatomic, strong) NSSavePanel *savePanel;
#property (nonatomic, strong) NSDictionary<NSString *, UTType*> *typeMapping;
#end
#implementation ViewController
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
_typeMapping = #{
#"jpeg": UTTypeJPEG,
#"png": UTTypePNG,
#"tiff": UTTypeTIFF
};
}
return self;
}
- (NSView *)accessoryView {
NSTextField *label = [NSTextField labelWithString:#"Filetypes:"];
label.textColor = NSColor.lightGrayColor;
label.font = [NSFont systemFontOfSize:NSFont.smallSystemFontSize];
label.alignment = NSTextAlignmentRight;
NSComboBox *comboBox = [NSComboBox new];
comboBox.editable = NO;
for (NSString *extension in self.typeMapping.allKeys) {
[comboBox addItemWithObjectValue:extension];
}
[comboBox setDelegate:self];
NSView *view = [NSView new];
[view addSubview:label];
[view addSubview:comboBox];
comboBox.translatesAutoresizingMaskIntoConstraints = NO;
label.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:#[
[label.bottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:-12],
[label.widthAnchor constraintEqualToConstant:64.0],
[label.leadingAnchor constraintEqualToAnchor:view.leadingAnchor constant:0.0],
[comboBox.topAnchor constraintEqualToAnchor:view.topAnchor constant:8.0],
[comboBox.leadingAnchor constraintEqualToAnchor:label.trailingAnchor constant:8.0],
[comboBox.bottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:-8.0],
[comboBox.trailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:-20.0],
]];
return view;
}
- (void)comboBoxSelectionDidChange:(NSNotification *)notification {
NSComboBox *comboBox = notification.object;
NSString *selectedItem = comboBox.objectValueOfSelectedItem;
NSLog(#"### set allowedContentTypes to %# (%#)", selectedItem, self.typeMapping[selectedItem]);
[self.savePanel setAllowedContentTypes:#[ self.typeMapping[selectedItem] ]];
}
- (IBAction)onSave:(id)sender {
NSWindow *window = NSApplication.sharedApplication.windows.firstObject;
self.savePanel = [NSSavePanel new];
self.savePanel.accessoryView = [self accessoryView];
[self.savePanel beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
if (result != NSModalResponseOK) {
return;
}
NSURL *fileURL = self.savePanel.URL;
NSLog(#"### selectedFile: %#", fileURL);
}];
}
- (void)setRepresentedObject:(id)representedObject {
[super setRepresentedObject:representedObject];
}
#end
Finally, a screenshot of the above demo code in action looks like this:
Press Cmd-N to add a new file to your project. Choose a View file to add a xib file that has a custom view.
Open the xib file and add the controls to the custom view. Press the Add button in the project window toolbar to access the user interface elements.
Use the NSNib class to load the xib file and get the custom view.

Custom selection style for view based "Source List" NSOutlineView

I'm using a view based NSOutlineView that has it's selectionHighlightStyle set to NSTableViewSelectionHighlightStyleSourceList.
I want to overwrite the selection style (background) for certain rows and draw a different color/gradient.
What I tried so far is creating a custom NSTableRowView and returning it via outlineView:rowViewForItem:.
I verified that my custom row views are created and returned by the outline view delegate.
However, none of the methods I'm overwriting in the custom row view are being called.
I tried to overwrite drawBackgroundInRect:, drawSelectionInRect:, drawSeparatorInRect: and even drawRect:. None of those are called, ever.
I'm suspecting the outline view to be doing some custom "magic" when it's set to the source list style, but I've not found anything in the documentation that indicates that a custom NSTableRowView wouldn't be honored at all in this case.
AppKit adds separate NSVisualEffectView with custom material to row view for drawing background when using NSTableViewSelectionHighlightStyleSourceList. I've come up with the following workaround which uses zero private APIs, but can break later if Apple implements some other way of highlighting rows.
#class CustomHighlightRowSelectionView;
#interface CustomHighlightRowView : NSTableRowView
#property (nonatomic, strong) CustomHighlightRowSelectionView *selectionView;
#end
#interface CustomHighlightRowSelectionView : NSView
#property (nonatomic, getter=isEmphasized) BOOL emphasized;
#property (nonatomic, getter=isSelected) BOOL selected;
#end
#implementation CustomHighlightRowView
- (CustomHighlightRowSelectionView *)selectionView
{
if (!_selectionView)
{
_selectionView = [[CustomHighlightRowSelectionView alloc] initWithFrame:NSZeroRect];
}
return _selectionView;
}
- (void)setEmphasized:(BOOL)emphasized
{
[super setEmphasized:emphasized];
self.selectionView.emphasized = emphasized;
}
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
self.selectionView.selected = selected;
}
- (void)addSubview:(NSView *)aView positioned:(NSWindowOrderingMode)place relativeTo:(NSView *)otherView
{
if (![aView isKindOfClass:[NSVisualEffectView class]])
{
[super addSubview:aView positioned:place relativeTo:otherView];
}
else
{
if (!self.selectionView.superview)
{
[super addSubview:self.selectionView positioned:place relativeTo:otherView];
self.selectionView.frame = self.bounds;
}
}
}
- (void)setFrame:(NSRect)frame
{
[super setFrame:frame];
self.selectionView.frame = self.bounds;
}
- (void)setBounds:(NSRect)bounds
{
[super setBounds:bounds];
self.selectionView.frame = self.bounds;
}
#end
#implementation CustomHighlightRowSelectionView
- (void)setEmphasized:(BOOL)emphasized
{
_emphasized = emphasized;
[self setNeedsDisplay:YES];
}
- (void)setSelected:(BOOL)selected
{
_selected = selected;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect
{
if (!self.selected)
{
return;
}
NSColor *fillColor = self.emphasized ? [NSColor alternateSelectedControlColor] : [NSColor secondarySelectedControlColor];
[fillColor setFill];
NSRectFill(dirtyRect);
}
#end
Are you using Yosemite?
From Apple's document Adopting Advanced Features of the new UI in Yosemite
When selectionHighlightStyle ==
NSTableViewSelectionHighlightStyleSourceList • Selection is now a
special blue material that does behind window blending
- The material size and drawing can not be customized
If you set it to NSTableViewSelectionHighlightStyleRegular and override the drawRect, it should work.
You'll need to overwrite -selectionHighlightStyle in your NSTableRowView subclass:
- (NSTableViewSelectionHighlightStyle)selectionHighlightStyle
{
return NSTableViewSelectionHighlightStyleRegular;
}
That way, the table view can be used in source list style but with a customized row selection. I wanted to have the source list under Yosemite in my project but with the user-selected color from the System Preferences.
Edit: I just noticed doing it this way causes text fields and image views inside the cell view to have an artifact like border looking very odd and ugly.

Is there a way for Interface Builder to render IBDesignable views which don't override drawRect:

I very rarely override drawRect in my UIView subclasses, usually preferring to set layer.contents with pre-rendering images and often employing multiple sublayers or subviews and manipulating these based on input parameters. Is there a way for IB to render these more complex view stacks?
Thanks, #zisoft for the clueing me in on prepareForInterfaceBuilder. There a few nuances with Interface Builder's render cycle which were the source of my issues and are worth noting...
Confirmed: You don't need to use -drawRect.
Setting images on UIButton control states works. Arbitrary layer stacks seem to work if a few things are kept in mind...
IB uses initWithFrame:
..not initWithCoder. awakeFromNib is also NOT called.
init... is only called once per session
I.e. once per re-compile whenever you make a change in the file. When you change IBInspectable properties, init is NOT called again. However...
prepareForInterfaceBuilder is called on every property change
It's like having KVO on all your IBInspectables as well as other built-in properties. You can test this yourself by having the your _setup method called, first only from your init.. method. Changing an IBInspectable will have no effect. Then add the call as well to prepareForInterfaceBuilder. Whahla! Note, your runtime code will probably need some additional KVO since it won't be calling the prepareForIB method. More on this below...
init... is too soon to draw, set layer content, etc.
At least with my UIButton subclass, calling [self setImage:img forState:UIControlStateNormal] has no effect in IB. You need to call it from prepareForInterfaceBuilder or via a KVO hook.
When IB fails to render, it doesn't blank our your component but rather keeps the last successful version.
Can be confusing at times when you are making changes that have no effect. Check the build logs.
Tip: Keep Activity Monitor nearby
I get hangs all the time on a couple different support processes and they take the whole machine down with them. Apply Force Quit liberally.
(UPDATE: This hasn't really been true since XCode6 came out of beta. It seldom hangs anymore)
UPDATE
6.3.1 seems to not like KVO in the IB version. Now you seem to need a flag to catch Interface Builder and not set up the KVOs. This is ok as the prepareForInterfaceBuilder method effectively KVOs all the IBInspectable properties. It's unfortunate that this behaviour isn't mirrored somehow at runtime thus requiring the manual KVO. See the updated sample code below.
UIButton subclass example
Below is some example code of a working IBDesignable UIButton subclass. ~~Note, prepareForInterfaceBuilder isn't actually required as KVO listens for changes to our relevant properties and triggers a redraw.~~ UPDATE: See point 8 above.
IB_DESIGNABLE
#interface SBR_InstrumentLeftHUDBigButton : UIButton
#property (nonatomic, strong) IBInspectable NSString *topText;
#property (nonatomic) IBInspectable CGFloat topTextSize;
#property (nonatomic, strong) IBInspectable NSString *bottomText;
#property (nonatomic) IBInspectable CGFloat bottomTextSize;
#property (nonatomic, strong) IBInspectable UIColor *borderColor;
#property (nonatomic, strong) IBInspectable UIColor *textColor;
#end
#implementation HUDBigButton
{
BOOL _isInterfaceBuilder;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self _setup];
}
return self;
}
//---------------------------------------------------------------------
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self _setup];
}
return self;
}
//---------------------------------------------------------------------
- (void)_setup
{
// Defaults.
_topTextSize = 11.5;
_bottomTextSize = 18;
_borderColor = UIColor.whiteColor;
_textColor = UIColor.whiteColor;
}
//---------------------------------------------------------------------
- (void)prepareForInterfaceBuilder
{
[super prepareForInterfaceBuilder];
_isInterfaceBuilder = YES;
[self _render];
}
//---------------------------------------------------------------------
- (void)awakeFromNib
{
[super awakeFromNib];
if (!_isInterfaceBuilder) { // shouldn't be required but jic...
// KVO to update the visuals
#weakify(self);
[self
bk_addObserverForKeyPaths:#[#"topText",
#"topTextSize",
#"bottomText",
#"bottomTextSize",
#"borderColor",
#"textColor"]
task:^(id obj, NSDictionary *keyPath) {
#strongify(self);
[self _render];
}];
}
}
//---------------------------------------------------------------------
- (void)dealloc
{
if (!_isInterfaceBuilder) {
[self bk_removeAllBlockObservers];
}
}
//---------------------------------------------------------------------
- (void)_render
{
UIImage *img = [SBR_Drawing imageOfHUDButtonWithFrame:self.bounds
edgeColor:_borderColor
buttonTextColor:_textColor
topText:_topText
topTextSize:_topTextSize
bottomText:_bottomText
bottomTextSize:_bottomTextSize];
[self setImage:img forState:UIControlStateNormal];
}
#end
This answer is related to overriding drawRect, but maybe it can give some ideas:
I have a custom UIView class which has complex drawings in drawRect. You have to take care about references which are not available during design time, i.e. UIApplication. For that, I override prepareForInterfaceBuilder where I set a boolean flag which I use in drawRect to distinguish between runtime and design time:
#IBDesignable class myView: UIView {
// Flag for InterfaceBuilder
var isInterfaceBuilder: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
// Initialization code
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func prepareForInterfaceBuilder() {
self.isInterfaceBuilder = true
}
override func drawRect(rect: CGRect)
{
// rounded cornders
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
// your drawing stuff here
if !self.isInterfaceBuilder {
// code for runtime
...
}
}
}
An here is how it looks in InterfaceBuilder:
You do not have to use drawRect, instead you can create your custom interface in a xib file, load it in initWithCoder and initWithFrame and it will be live rendering in IB after adding IBDesignable. Check this short tutorial: https://www.youtube.com/watch?v=L97MdpaF3Xg
I think layoutSubviews is the simplest mechanism.
Here is a (much) simpler example in Swift:
#IBDesignable
class LiveLayers : UIView {
var circle:UIBezierPath {
return UIBezierPath(ovalInRect: self.bounds)
}
var newLayer:CAShapeLayer {
let shape = CAShapeLayer()
self.layer.addSublayer(shape)
return shape
}
lazy var myLayer:CAShapeLayer = self.newLayer
// IBInspectable proeprties here...
#IBInspectable var pathLength:CGFloat = 0.0 { didSet {
self.setNeedsLayout()
}}
override func layoutSubviews() {
myLayer.frame = self.bounds // etc
myLayer.path = self.circle.CGPath
myLayer.strokeEnd = self.pathLength
}
}
I haven't tested this snippet, but have used patterns like this before. Note the use of the lazy property delegating to a computed property to simplify initial configuration.
To elaborate upon Hari Karam Singh's answer, this slideshow explains further:
http://www.splinter.com.au/presentations/ibdesignable/
Then if you aren't seeing your changes show up in Interface Builder, try these menus:
Xcode->Editor->Automatically Refresh Views
Xcode->Editor->Refresh All Views
Xcode->Editor->Debug Selected Views
Unfortunately, debugging my view froze Xcode, but it should work for small projects (YMMV).
In my case, there were two problems:
I did not implement initWithFrame in custom view: (Usually initWithCoder: is called when you initialize via IB, but for some reason initWithFrame: is needed for IBDesignable only. Is not called during runtime when you implement via IB)
My custom view's nib was loading from mainBundle: [NSBundle bundleForClass:[self class]] was needed.
I believe you can implement prepareForInterfaceBuilder and do your core animation work in there to get it to show up in IB.
I've done some fancy things with subclasses of UIButton that do their own core animation layer work to draw borders or backgrounds, and they live render in interface builder just fine, so i imagine if you're subclassing UIView directly, then prepareForInterfaceBuilder is all you'll need to do differently. Keep in mind though that the method is only ever executed by IB
Edited to include code as requested
I have something similar to, but not exactly like this (sorry I can't give you what I really do, but it's a work thing)
class BorderButton: UIButton {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit(){
layer.borderWidth = 1
layer.borderColor = self.tintColor?.CGColor
layer.cornerRadius = 5
}
override func tintColorDidChange() {
layer.borderColor = self.tintColor?.CGColor
}
override var highlighted: Bool {
willSet {
if(newValue){
layer.backgroundColor = UIColor(white: 100, alpha: 1).CGColor
} else {
layer.backgroundColor = UIColor.clearColor().CGColor
}
}
}
}
I override both initWithCoder and initWithFrame because I want to be able to use the component in code or in IB (and as other answers state, you have to implement initWithFrame to make IB happy.
Then in commonInit I set up the core animation stuff to draw a border and make it pretty.
I also implement a willSet for the highlighted variable to change the background color because I hate when buttons draw borders, but don't provide feedback when pressed (i hate it when the pressed button looks like the unpressed button)
Swift 3 macro
#if TARGET_INTERFACE_BUILDER
#else
#endif
and class with function which is called when IB renders storyboard
#IBDesignable
class CustomView: UIView
{
#IBInspectable
public var isCool: Bool = true {
didSet {
#if TARGET_INTERFACE_BUILDER
#else
#endif
}
}
override func prepareForInterfaceBuilder() {
// code
}
}
IBInspectable can be used with types below
Int, CGFloat, Double, String, Bool, CGPoint, CGSize, CGRect, UIColor, UIImage

Automatically click segmentControl with index 0 on ViewDidAppear

I am hoping to "automate" a click on the segmentController with the index of 0.
My tabBar-based app has multiple segmentControllers in a tab in the ViewDidAppear method, I would like to automatically have it "click" the first segmented controller.
if (segmentController.selectedSegmentIndex == 0) {
//stuff here
}
if (segmentController.selectedSegmentIndex == 1) {
//stuff here
}
Does anyone know how I might accomplish this? Thank you!
If you're creating it programmatically, you could lazy load it like this:
#interface ExampleViewController : UIViewController
#property (nonatomic, strong) UISegmentedControl *segmentedControl;
- (void)segmentedControlClicked:(UISegmentedControl *)segmentedControl;
#end
#implementation ExampleViewController
- (UISegmentedControl *)segmentedControl
{
if (!_segmentedControl)
{
NSArray *items = #[#"First", #"Second", #"Third"];
_segmentedControl = [[UISegmentedControl alloc] initWithItems:items];
[_segmentedControl addTarget:self
action:#selector(segmentedControlClicked:)
forControlEvents:UIControlEventValueChanged];
[_segmentedControl setSelectedSegmentIndex:0]; // Set Default selection
CGRect frame = _segmentedControl.frame;
frame.origin = CGPointMake(0.0f, 0.0f); // Move to wherever you need it
[self.view addSubview:_segmentedControl];
}
return _segmentedControl;
}
- (void)segmentedControlClicked:(UISegmentedControl *)segmentedControl
{
// Whatever your code is goes here...
}
#end
If you're wanting a method to be called also initially, you can call it within your viewDidLoad: method as such:
- (void)viewDidLoad
{
[self.segmentedControl setSelectedSegmentIndex:0]; // Set desired default index (optional if set in lazy load as shown above)
[self segmentedControlClicked:self.segmentedControl];
}
This would hence simulate a click on desired default index.
Be careful putting the above into viewDidAppear: (you could if you really wanted to) because anytime the view comes to the front, this method will be called (in example, if this view controller presents a modal view controller, once the modal is dismissed, this view controller's viewDidAppear: method will be called).
Cheers!
Set the selectedSegmentIndex property on your UISegmentedControl in your viewDidAppear (or viewDidLoad) method.
self.segmentedController.selectedSegemntIndex = 1;
UISegmentedControl Reference

Disable scrolling in NSTableView

Is there a simple way to disable scrolling of an NSTableView.
It seems there isn't any property on
[myTableView enclosingScrollView] or [[myTableView enclosingScrollView] contentView] to disable it.
This works for me: subclass NSScrollView, setup and override via:
- (id)initWithFrame:(NSRect)frameRect; // in case you generate the scroll view manually
- (void)awakeFromNib; // in case you generate the scroll view via IB
- (void)hideScrollers; // programmatically hide the scrollers, so it works all the time
- (void)scrollWheel:(NSEvent *)theEvent; // disable scrolling
#interface MyScrollView : NSScrollView
#end
#import "MyScrollView.h"
#implementation MyScrollView
- (id)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
if (self) {
[self hideScrollers];
}
return self;
}
- (void)awakeFromNib
{
[self hideScrollers];
}
- (void)hideScrollers
{
// Hide the scrollers. You may want to do this if you're syncing the scrolling
// this NSScrollView with another one.
[self setHasHorizontalScroller:NO];
[self setHasVerticalScroller:NO];
}
- (void)scrollWheel:(NSEvent *)theEvent
{
// Do nothing: disable scrolling altogether
}
#end
I hope this helps.
Here's the best solution in my opinion:
Swift 5
import Cocoa
#IBDesignable
#objc(BCLDisablableScrollView)
public class DisablableScrollView: NSScrollView {
#IBInspectable
#objc(enabled)
public var isEnabled: Bool = true
public override func scrollWheel(with event: NSEvent) {
if isEnabled {
super.scrollWheel(with: event)
}
else {
nextResponder?.scrollWheel(with: event)
}
}
}
Simply replace any NSScrollView with DisablableScrollView (or BCLDisablableScrollView if you still use ObjC) and you're done. Simply set isEnabled in code or in IB and it will work as expected.
The main advantage that this has is for nested scroll views; disabling children without sending the event to the next responder will also effectively disable parents while the cursor is over the disabled child.
Here are all advantages of this approach listed out:
✅ Disables scrolling
✅ Does so programmatically, behaving normally by default
✅ Does not interrupt scrolling a parent view
✅ Interface Builder integration
✅ Drop-in replacement for NSScrollView
✅ Swift and Objective-C Compatible
Thanks to #titusmagnus for the answer, but I made one modification so as not to break scrolling when when the "disabled" scrollView is nested within another scrollView: You can't scroll the outer scrollView while the cursor is within the bounds of the inner scrollView. If you do this...
- (void)scrollWheel:(NSEvent *)theEvent
{
[self.nextResponder scrollWheel:theEvent];
// Do nothing: disable scrolling altogether
}
...then the "disabled" scrollView will pass the scroll event up to the outer scrollView and its scrolling will not get stuck down inside its subviews.
Works for me:
- (void)scrollWheel:(NSEvent *)theEvent
{
[super scrollWheel:theEvent];
if ([theEvent deltaY] != 0)
{
[[self nextResponder] scrollWheel:theEvent];
}
}
There is no simple direct way (meaning, there's no property like UITableView's scrollEnabled that you can set), but i found this answer helpful in the past.
One other thing you could try (not sure about this) is subclassing NSTableView and override -scrollWheel and -swipeWithEvent so they do nothing. Hope this helps