NSButton wont setFont: when layer backed - objective-c

I have my NSButton layer backed because I wanted to use a custom image, but this seems like it's inhibiting the use of the setFont: method when I need to programmatically change the font, as when I comment out the code for wantsUpdateLayer: and updateLayer:, setFont: works, but when the layer methods are in the code, it does nothing.
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.fontChangeButton = [[CustomButton alloc]initWithFrame:NSMakeRect(82, 60, 190, 113)];
[self.window.contentView addSubview:self.fontChangeButton];
}
- (IBAction)changeFont:(id)sender {
[self.fontChangeButton fontChange];
}
#end
#implementation CustomButton
- (void)fontChange{
[self setFont:[NSFont fontWithName:#"Dosis Bold" size:40]];
}
//when these are commented out, setFont: works, but I need them in for the custom button images
- (BOOL)wantsUpdateLayer{
return YES;
}
- (void)updateLayer{
if (self.state == NSOnState) {
self.layer.contents = [NSImage imageNamed:#"buttonPressed.png"];
}else
self.layer.contents = [NSImage imageNamed:#"buttonUnpressed.png"];
}
This thread offers a workaround, but I'd much rather understand why this is happening and fix it: Can't Change NSButton Font

By overriding -wantsUpdateLayer to return YES you're bypassing calls to -drawRect:. This facility was introduced in 10.8 and exists for efficiency purposes.
There are two things I think should be clarified:
1 - You don't need to override -wantsUpdateLayer to be layer-backed. Just send -setWantsLayer:YES to your button to be layer-backed.
2 - In your example, creating a custom NSButtonCell class might be a better approach to what you're trying to do. Have a look at Apple's documentation on subclassing NSControl and this how to to get started.

Related

Presenting modal dialogs from XIB in Cocoa: best/shortest pattern?

Below is my typical WindowController module for presenting a modal dialog (could be settings, asking username/password, etc) loaded from a XIB. It seems a bit too complex for something like this. Any ideas how this can be done better/with less code?
Never mind that it's asking for a password, it could be anything. What frustrates me most is that I repeat the same pattern in each and every of my XIB-based modal window modules. Which of course means I could define a custom window controller class, but before doing that I need to make sure this is really the best way of doing things.
#import "MyPasswordWindowController.h"
static MyPasswordWindowController* windowController;
#interface MyPasswordWindowController ()
#property (weak) IBOutlet NSSecureTextField *passwordField;
#end
#implementation MyPasswordWindowController
{
NSInteger _dialogCode;
}
- (id)init
{
return [super initWithWindowNibName:#"MyPassword"];
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self.window center];
}
- (void)windowWillClose:(NSNotification*)notification
{
[NSApp stopModalWithCode:_dialogCode];
_dialogCode = 0;
}
- (IBAction)okButtonAction:(NSButton *)sender
{
_dialogCode = 1;
[self.window close];
}
- (IBAction)cancelButtonAction:(NSButton *)sender
{
[self.window close];
}
+ (NSString*)run
{
if (!windowController)
windowController = [MyPasswordWindowController new];
[windowController loadWindow];
windowController.passwordField.stringValue = #"";
if ([NSApp runModalForWindow:windowController.window])
return windowController.passwordField.stringValue;
return nil;
}
The application calls [MyPasswordWindowController run], so from the point of view of the user of this module it looks simple, but not so much when you look inside.
Set tags on your buttons to distinguish them. Have them both target the same action method:
- (IBAction) buttonAction:(NSButton*)sender
{
[NSApp stopModalWithCode:[sender tag]];
[self.window close];
}
Get rid of your _dialogCode instance variable and -windowWillClose: method.
-[NSApplication runModalForWindow:] will already center the window, so you can get rid of your -awakeFromNib method.
Get rid of the invocation of -[NSWindowController loadWindow]. That's an override point. You're not supposed to call it. The documentation is clear on that point. It will be called automatically when you request the window controller's -window.
Get rid of the static instance of MyPasswordWindowController. Just allocate a new one each time. There's no point in keeping the old one around and it can be troublesome to reuse windows.

iOS Change alpha of UIButton when there is an EventTouchDown

I would like the change the alpha of a UIButton when there is an EventTouchDown on it. I'm was hoping there was a way to do it similar to the below code:
[self.myButton setImage:<#(UIImage *)#> forState:UIControlEventTouchDown];
Is there a way to do something like this with "setAlpha"?
Thanks you!!!
You could subclass your UIButton (not recommended, but since all you'd end up doing is overriding setHighlighted or setSelected it shouldn't be much of an issue here) and override the tapping methods to handle your alpha change.
// In your UIButton's subclass
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
if(self.highlighted) {
[self setAlpha:0.5];
}
else {
[self setAlpha:1.0];
}
}
If you need to do something on selected you could do the same as in here

touchesShouldCancelInContentView is not being called

I've overwritten the subclass and it refuses to be call touchesShouldCancelInContentView. I swear I've written code to do this before. I'm overwriting the class so that certain views don't pass along the touch events to the scroll view.
With the class
#interface SlidingWindowScrollView : UIScrollView
{
UIView* noTouchView;
}
#property (nonatomic, retain) UIView* noTouchView;
#end
And the implementation
#implementation SlidingWindowScrollView
#synthesize noTouchView;
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
BOOL shouldCancel = NO;
if(view == noTouchView)
shouldCancel = YES;
NSLog(#"shouldCancel %d", shouldCancel);
return shouldCancel;
}
#end
In the xib I throw down a scrollview and a uiview inside it and write this in the VC view did load.
scrollView.contentSize = CGSizeMake(scrollView.bounds.size.width*2,
scrollView.bounds.size.height);
scrollView.pagingEnabled = YES;
scrollView.canCancelContentTouches = YES;
contentView.frame = CGRectMake(scrollView.frame.size.width + 20,
0,
scrollView.frame.size.width,
scrollView.frame.size.height);
contentView.userInteractionEnabled = YES;
scrollView.noTouchView = contentView;
[scrollView setContentOffset:CGPointMake(scrollView.bounds.size.width, 0) animated:YES];
I don't know what gives. This should be a simple override. I made sure that the custom class was correct in the XIB and that the outlets all said it was my new subclass of UIScrollView. I don't mean to be presumptuous but did something break in the newest iOS API? This seems like it should be easy yet it never calls the function touchesShouldCancelInContentView. I'm baffled. I'd greatly appreciate it someone could prove me wrong and get this function to be called by the scrollView. I suppose it should be noted that the print statement did print but it could be consistently done
If you can get this working on your machine please let me know, because I don't see anything wrong. It's driving me nuts #_# Hope someone can help me out. Thanks.
From what I've seen, you don't get touchesShouldCancelInContentView: or touchesShouldBegin:withEvent:inContentView: callbacks unless you have delaysContentTouches set to NO:
scrollView.delaysContentTouches = NO;
I got stuck on this for a while incase you ever read this, you need to have both the following properties set before touches in touchesShouldCancelInContentView will fire
scrollView.delaysContentTouches = NO;
scrollView.canCancelContentTouches = YES;
Just having scrollView.delaysContentTouches = NO wont fire if scrollView.canCancelContentTouches = NO

UINavigationBar topItem/items seems to double-pop on back

I am managing my own UINavigationBar. I need to do this due to extensive skinning. The documentation for UINavigationController warns that there are limitations to skinning the UINavigationBar when used with a UINavigationController.
I have put in extensive logging and from everything I can tell, pushing the "Back" button in the UINavigationController pops two items off of of the stack instead of one. I get a single delegate callback telling me that it is removing the logical item, but it actually removes that one and one more.
The item added to the UINavigationController in awakeFromNib should never be removed. It is being removed for some reason.
There are two similar questions, but neither have satisfactory answers. The two questions are:
UINavigationBar .items accessor doesn't return the current UINavigationItem
UINavigationBar seems to pop 2 items off stack on "back"
- (void)awakeFromNib {
[headerView setDelegate: self];
[headerView pushNavigationItem: tableDisplay animated: NO];
}
- (void) selectedStory: (NSNotification *)not {
[headerView pushNavigationItem: base animated: NO];
NSLog(#"Selected story: %#", base);
}
- (void) baseNav {
NSLog(#"Current items: %#", [headerView items]);
BaseInnerItem *current = (BaseInnerItem *)[headerView topItem];
[self addSubview: [current view]];
}
- (BOOL)navigationBar: (UINavigationBar *)navigationBar shouldPushItem: (UINavigationItem *)item {
return YES;
}
- (BOOL)navigationBar: (UINavigationBar *)navigationBar shouldPopItem: (UINavigationItem *)item {
return YES;
}
- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item {
NSLog(#"didPushItem: %#", item);
[self baseNav];
}
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item {
NSLog(#"didPopItem: %#", item);
[self baseNav];
}
Edited to add relevant debugging from a single run:
2010-10-13 02:12:45.911 Remix2[17037:207] didPushItem: <TableDisplay: 0x5d41cc0>
2010-10-13 02:12:45.912 Remix2[17037:207] Current items: (
"<TableDisplay: 0x5d41cc0>"
)
2010-10-13 02:12:49.020 Remix2[17037:207] didPushItem: <WebDisplay: 0x591a590>
2010-10-13 02:12:49.021 Remix2[17037:207] Current items: (
"<TableDisplay: 0x5d41cc0>",
"<WebDisplay: 0x591a590>"
)
2010-10-13 02:12:49.023 Remix2[17037:207] Selected story: <WebDisplay: 0x591a590>
2010-10-13 02:12:59.498 Remix2[17037:207] didPopItem: <WebDisplay: 0x591a590>
2010-10-13 02:12:59.499 Remix2[17037:207] Current items: (
)
You always have to call [super awakeFromNib] when your subclass implements that method, per the documentation for -awakeFromNib:
You must call the super implementation of awakeFromNib to give parent classes the opportunity to perform any additional initialization they require
Importantly, however, ...
I don't understand why you have to actually manage your own navigation bar. If you subclass UINavigationBar and only override certain drawing or layout methods such as -drawRect:, -layoutSubviews, etc., then all of the logic behind managing the navigation bar in a navigation controller will just fall back on the original UINaviationBar class.
I've had to do extensive view customization for almost every major UIKit class, but I always left the complicated logic to the original classes, overriding only drawing methods to customize the look and feel.
Incidentally, it's actually much easier to skin an entire app without subclassing at all if all you're doing is using custom image assets. By setting a layer's contents property, you can either customize the look and feel of a UIView-based class on an as-needed basis or throughout your entire app:
#import <QuartzCore/QuartzCore.h>
...
- (void)viewDidLoad
{
[super viewDidLoad];
UIImage * navigationBarContents = [UIImage imageNamed:#"navigation-bar"];
self.navigationController.navigationBar.layer.contents =
(id)navigationBarContents.CGImage;
}
You can set the contents for any class that inherits from UIView: navigation bars, toolbars, buttons, etc. It's a lot easier to manage this way without having to subclass at all.
This appears to be a bug in the implementation of -[UINavigationBar items]
When called from inside the -navigationBar:didPopItem: delegate method, it will omit the last object. You can check this by calling [navigationBar valueForKey:#"_itemStack"] to retrieve the underlying array and see that the expected items are still there.
Adding a dispatch_async inside -navigationBar:didPopItem:method successfully works around the issue in my app.

Selection Highlight in NSCollectionView

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
}
}