NSButton Failing to Reflect a Change of its Image

[object Object]In a project I am developing with Xcode a 2d layout of the faces of Rubik's cube is displayed (see the attached Screenshot). In the center of each face's representation is a NSColorWell. The other eight cubes on that face are represented as NSButtons displaying an NSImage of the appropriate color. In response to the user selecting a new color the program resets the eight Button's image to one of the new color.

The problem is that this change is not reflected on the display even if the NSButton is sent a setNeedsDisplay message. Sending the NSButton a display message also has no effect. The change is not seen until the Button receives a mouse click or interestingly if the window is dragged from the main screen to a second screen.

I believe this a bug in AppKit.

Answered by Bruce D M in 736833022

I found a work around to the problem. In the example code below, in response to a color change the action message, newColor:, changes the color of the image and calls the button's setImage: message. This actually is the same NSImage object already in the Button. In this case the API ignores the setNeedsDisplay message.

If a new NSImage object is created, as in the line commented out, and sent to the Button, the Button is redrawn with the new image.

I still think it is a bug that the in the first instance the setNeedsDisplay message is ignored.

//  AppDelegate.m
//  TestUI
//
//  Created by Bruce D MacKenzie on 11/20/22.
//

#import "AppDelegate.h"

@interface AppDelegate ()
{
}

@property (strong) IBOutlet NSWindow *window;
@property (assign) IBOutlet NSButton *theButton;

@end

@implementation AppDelegate
{
    NSImage     *image;
}

@synthesize theButton;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
}


- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

-(void)awakeFromNib
{

    image = [[NSImage alloc] initWithSize: NSMakeSize( 32 , 32 )];
    [image lockFocus];
    [[NSColor redColor] drawSwatchInRect: NSMakeRect(0, 0, 32, 32)];
    [image unlockFocus];

    [[self theButton] setImage: image];
}

- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
    return YES;
}

-(IBAction)newColor: (NSColorWell *)sender
{
    NSColor     *newColor;

    newColor = [sender color];


//    image = [[NSImage alloc] initWithSize: NSMakeSize( 32 , 32 )];

    [image lockFocus];
    [newColor drawSwatchInRect: NSMakeRect( 0, 0, 32, 32)];
    [image unlockFocus];

    [[self theButton] setImage: image];
    [[self theButton] setNeedsDisplay: YES];
}
code-block

What happens if, instead of calling setNeedsDisplay, you directly set the image of the NSButton ? Does it work better ?

If so, problem could be that there is already a redraw in the queue for the button.

https://www.hackingwithswift.com/example-code/uikit/how-to-force-a-uiview-to-redraw-setneedsdisplay

Accepted Answer

I found a work around to the problem. In the example code below, in response to a color change the action message, newColor:, changes the color of the image and calls the button's setImage: message. This actually is the same NSImage object already in the Button. In this case the API ignores the setNeedsDisplay message.

If a new NSImage object is created, as in the line commented out, and sent to the Button, the Button is redrawn with the new image.

I still think it is a bug that the in the first instance the setNeedsDisplay message is ignored.

//  AppDelegate.m
//  TestUI
//
//  Created by Bruce D MacKenzie on 11/20/22.
//

#import "AppDelegate.h"

@interface AppDelegate ()
{
}

@property (strong) IBOutlet NSWindow *window;
@property (assign) IBOutlet NSButton *theButton;

@end

@implementation AppDelegate
{
    NSImage     *image;
}

@synthesize theButton;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
}


- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

-(void)awakeFromNib
{

    image = [[NSImage alloc] initWithSize: NSMakeSize( 32 , 32 )];
    [image lockFocus];
    [[NSColor redColor] drawSwatchInRect: NSMakeRect(0, 0, 32, 32)];
    [image unlockFocus];

    [[self theButton] setImage: image];
}

- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
    return YES;
}

-(IBAction)newColor: (NSColorWell *)sender
{
    NSColor     *newColor;

    newColor = [sender color];


//    image = [[NSImage alloc] initWithSize: NSMakeSize( 32 , 32 )];

    [image lockFocus];
    [newColor drawSwatchInRect: NSMakeRect( 0, 0, 32, 32)];
    [image unlockFocus];

    [[self theButton] setImage: image];
    [[self theButton] setNeedsDisplay: YES];
}
code-block

Not sure it is a bug but maybe a side effect on how the rendering engine (NSRunLoop) works, by "stacking" changes.

There was a very interesting TN by Quinn here: https://developer.apple.com/forums/thread/717392

There may be a simple way to work around the issue.

Change the color inside a dispatch asyncAfter (did not try it yet); that should give time to the system to proceed before it takes the color change into consideration:

  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1)  { 
    [[self theButton] setImage: image];
   }
NSButton Failing to Reflect a Change of its Image
 
 
Q