I struggle to understand what I'm doing wrong here.
In my document-based app the document's model is modified in a notification. The model is correctly updated with the new content and I just want to tag the view as needing update.
To my surprise, nothing happened until I manually resized the window which led me to the following workaround-wart (a dummy change to the window size):
NSWindow *window = document.window;
#if false
window.contentView.needsDisplay = YES;
#else
NSSize sz = document.model.canvasSize;
[window setContentSize:NSMakeSize(0,0)];
[window setContentSize:sz];
#endif
[window makeKeyAndOrderFront:self];
Obviously, that's not how it's supposed to be done, but I'm at my wits end here.
The inital model renders perfectly, but any changes to the model are ignored.
I tried dispatching needsDisplay
on the main thread using performSelectorOnMainThread
but that didn't seem to matter.
Any clues to what goes on here would be greatly appreciated.
Update
I've created a minimal(?) example showing the same behaviour available on github:
Most of the functionality is in AppDelegate.m:
@interface AppDelegate ()
{
Document *_documentRef;
}
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[[NSDistributedNotificationCenter defaultCenter] addObserver:self
selector:@selector(notificationHandler:)
name:@"foo"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(notificationHandler:)
name:@"foo"
object:nil];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
[[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (Document *)document
{
Document *doc = _documentRef;
NSLog(@"_documentRef: %@", _documentRef);
if (doc == nil) {
Document *doc = [[Document alloc] init];
NSLog(@"Created document: %@", doc);
_documentRef = doc;
NSDocumentController *docController = NSDocumentController.sharedDocumentController;
[docController addDocument:doc];
[doc makeWindowControllers];
}
return _documentRef;
}
- (void)notificationHandler:(NSNotification *)notification {
NSString *newModel = notification.userInfo[@"newModel"];
NSData *newModelData = [newModel dataUsingEncoding:NSUTF8StringEncoding];
Document *document = [self document];
NSLog(@"document: %@", document);
NSLog(@"Document before update: %@", document.model);
Boolean success = [document readFromData:newModelData ofType:@"foo" error:nil];
NSLog(@"Document after update: %@", document.model);
NSWindow *window = document.window;
window.contentView.needsDisplay = YES;
[window makeKeyAndOrderFront:self];
}
- (IBAction)localNotification:(id)sender {
NSString *dateString = [NSDateFormatter localizedStringFromDate:[NSDate date]
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterShortStyle];
NSString *newModel = [NSString stringWithFormat:@"Local: %@", dateString];
[[NSNotificationCenter defaultCenter] postNotificationName:@"foo"
object:@"bar"
userInfo:@{@"newModel": newModel}];
}
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
return NO;
}
- (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender {
return NO;
}
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)hasVisibleWindows {
return NO;
}
@end
Document.m is as small as they get:
@implementation Document
- (instancetype)init {
self = [super init];
if (self) {
_model = @"Initial value";
}
return self;
}
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError {
self.model = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return (self.model != nil);
}
- (NSWindow *)window {
return [self.windowControllers[0] window];
}
@end
and finally the view (MyView.m):
@implementation MyView
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSString *model = [[[self.window windowController] document] model];
[model drawAtPoint:NSMakePoint(50.0, self.bounds.size.height/2.0) withAttributes:nil];
}
@end
To send a local notification I hooked up an action from Help -> Notify to localNotification:
, and to create a distributed notification there is a (non-sandboxed) "Sender" target in Xcode creating a simple tool:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *dateString = [NSDateFormatter localizedStringFromDate:[NSDate date]
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterShortStyle];
NSString *newModel = [NSString stringWithFormat:@"Remote: %@", dateString];
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"foo"
object:@"bar"
userInfo:@{@"newModel": newModel}
deliverImmediately:YES];
}
return 0;
}
Running the app and sending some notifications yields the following log output:
_documentRef: (null)
Created document: <Document: 0x600003436450>
document: <Document: 0x600003436450>
Document before update: Initial value
Document after update: Local: 2025-03-14, 11:22
_documentRef: <Document: 0x600003436450>
document: <Document: 0x600003436450>
Document before update: Local: 2025-03-14, 11:22
Document after update: Remote: 2025-03-14, 11:22
_documentRef: <Document: 0x600003436450>
document: <Document: 0x600003436450>
Document before update: Remote: 2025-03-14, 11:22
Document after update: Local: 2025-03-14, 11:22
but only the first notification cause the view to update.
FWIW, resizing the window doesn't invoke the view's drawRect:
method (the y-position of the text should be in the middle of the view)
I'm pretty sure I'm missing something basic here...