How should a custom view update a model object? - cocoa

This is a Cocoa n00b question - I've been programming GUI applications for years in other environments, but now I would like to understand what is "idiomatic Cocoa" for the following trivialized situation:
I have a simple custom NSView that allows the user to draw simple shapes within it. Its drawRect implementation is like this:
- (void)drawRect:(NSRect)rect
{
// Draw a white background.
[[NSColor whiteColor] set];
NSRect bounds = [self bounds];
[NSBezierPath fillRect:bounds];
[[NSColor blackColor] set];
// 'shapes' is a NSMutableArray instance variable
// whose elements are NSValues, each wrapping an NSRect.
for (NSValue *value in shapes)
{
NSRect someRect;
[value getValue:&someRect];
[self drawShapeForRect:someRect];
}
// In addition to drawing the shapes in the 'shapes'
// array, we draw the shape based on the user's
// current drag interaction.
[self drawShapeForRect:[self dragRect]];
}
You can see how simple this code is: the shapes array instance variable acts as the model that the drawRect method uses to draw the shapes. New NSRects are added to shapes every time the user performs a mouse-down/drag/mouse-up sequence, which I've also implemented in this custom view. Here's my question:
If this were a "real" Cocoa application, what would be the idiomatic way for my custom view to update its model?
In other words, how should the custom view notify the controller that another shape needs to be added to the list of shapes? Right now, the view tracks shapes in its own NSMutableArray, which is fine as an implementation detail, but I do not want to expose this array as part of my custom view's public API. Furthermore, I would want to put error-checking, save/load, and undo code in a centralized place like the controller rather than have it littered all over my custom views. In my past experience with other GUI programming environments, models are managed by an object in my controller layer, and the view doesn't generally update them directly - rather, the view communicates when something happens, by dispatching an event, or by calling a method on a controller it has a reference to, or using some similarly-decoupled approach.
My gut feeling is that idiomatic Cocoa code would expose a delegate property on my custom view, and then wire the MyDocument controller object (or another controller-layer object hanging off of the document controller) to the view, as its delegate, in the xib file. Then the view can call some methods like shapeAdded:(NSRect)shape on the delegate. But it seems like there are any number of other ways to do this, such as having the controller pass a reference to a model object (the list of shapes) directly to the custom view (feels wrong), or having the view dispatch a notification that the controller would listen to (feels unwieldy), and then the controller updates the model.

Having a delegate is a cromulent way to do this. The other way would be to expose an NSArray binding on the view, and bind it to an array controller's arrangedObjects binding, then bind the array controller's content binding to whatever owns the real array holding the model objects. You can then add other views on the same array controller, such as a list of objects in the active layer.
This being a custom view, you'll need to either create an IBPlugin to expose the binding in IB, or bind it programmatically by sending the view a bind:toObject:withKeyPath:options: message.

There is a very good example xcode project in your /Developer/Examples/AppKit/Sketch directory which is a more advanced version of what you are doing, but pertinent nonetheless. It has great examples of using bindings between controller and view that will shed light on the "right" way to do things. This example doesn't use IB Plugins so you'll get to see the manual calls to bind and the observe methods that are implemented.

there are several similarities between your code and an NSTableView, so I would look at maybe using a data source (similar to your delegate) or even perhaps bindings.

Related

How to share model object between view controllers?

I created a basic OS X application with a storyboard to just draw circles in a custom view. The main window contains a NSSplitViewControllercontaining two sub-views (content and side bar like Apple Pages or Numbers have). The content view is a custom subclass of NSView for drawing circles while the side bar view contains standard controls. Both should be bound to a model object which holds the properties like number of circles, diameter and so on.
As I understand both subviews have their own controllers in any case. How do have a data model object (let's call it Circles) which both controllers reference so I can hook up key-value observation for redrawing my custom view on changing the controls' values?
My idea would be to create the model object in the common parent controller and pass it on to the children, but how to set that up in Interface Builder in Xcode 7.2?
Working off my comment. You can use the representedObject property of NSViewController to pass any object along to other view controllers. The one downside to this though is that the property is of type AnyObject? so making that work with Swift can be awkward. In your NSViewController subclass you can make a new property to store the data and give it the correct type, or you could make a protocol to define the computed property that serves as a wrapper around representedObject property.

Cocoa Programming: Adding a Rectangle to a Custom View (NSView)

Is there a simple way to add a simple rectangle to a Custom View without using a custom NSView subclass for it? Something along the lines of:
Assign an IBOutlet (let's call it colorWheelView) of NSView type to the CustomView
In my NSViewController's initWithNibName use it to change draw the rectangle:
// pseudocode
self.colorWheelView.addRectangle(myRectangle);
self.redraw()
The only way I've seen it done (on this site, and in my book Cocoa Programming for Mac OSX, pp. 241) is by making a custom class for the Custom View and modifying its drawRect method... Is this really the only way to accomplish this?
Edit: not sure why formatting is not being rendered correctly. I'm trying to fix it.
It really isn't all that hard to roll your own..
Just add an NSArray property to your NSView subclass, then in your drawRect method draw them either manually or using one of the NSRectFillList* methods provided by AppKit already.
(Beware: those take a plain C array, not an NSArray).
You wouldn't want to manually trigger the redraw from outside the view as in your sample code, though. To keep things consistent your addRectangle would trigger a redraw of the view itself e.g. by calling setNeedsDisplay:.

Two customs views being drawn instead of one

I'm developing a little app with two custom views in Cocoa. The custom views are controlled by two separated classes. I've a NSTimer set up in my AppDelegate that asks one of the views to draw it self, but the the problem is that both views are being drawn.
My method looks like this:
- (void)timerMethod:(NSTimer *) theTimer
{
[theOtherView setNeedsDisplay:YES]; // The View that needs to be drawn
[theBackGroundView setNeedsDisplay:NO]; // Doesn't really do anything, thought it might though.
}
I hope someone can point me in a direction to how I should draw my view instead.

Calling setNeedsDisplay:YES on layer-hosting view does not redraw the view

I have a layer-hosting view set up like this in a custom NSView subclass:
[self setLayer:rootLayer];
[self setWantsLayer:YES];
I add all the sublayers to the layer tree after I called setNeedsDisplay on each sublayer. Each layer's content is provided by a drawLayer:inContext method of my layer's delegate.
Here is my problem:
After initializing my view the view gets draw correctly. However, when the model has changed and I call [myCustomView setNeedsDisplay:YES]; from my view controller the drawLayer:inContext is not called.
I am confused now how to update the view:
Do I have to call the setNeedsDisplay method on each CALayer in the layer tree?
Should not the call of setNeedsDisplay:YES on the layer-hosting view itself trigger the redraw of the whole layer tree?
Thanks for your help.
Edit
I have found something in the NSView Class reference
A layer-backed view is a view that is backed by a Core Animation layer. Any drawing done by the view is the cached in the backing layer. You configured a layer-backed view by simply invoking setWantsLayer: with a value of YES. The view class will automatically create the a backing layer for you, and you use the view class’s drawing mechanisms. When using layer-backed views you should never interact directly with the layer.
A layer-hosting view is a view that contains a Core Animation layer that you intend to manipulate directly. You create a layer-hosting view by instantiating an instance of a Core Animation layer class and setting that layer using the view’s setLayer: method. After doing so, you then invoke setWantsLayer: with a value of YES. When using a layer-hosting view you should not rely on the view for drawing, nor should you add subviews to the layer-hosting view.
link to documentation
In my case I have a layer-hosting view. So does that indeed mean that I have to trigger the redraw manually? Should I implement a pseudo drawRect method in the custom NSView to call the appropriate setNeedsDisplay on the CALayers that changed?
After further research in Apple's sample code of a kiosk-style menu I found out that if you are using a layer-hosting view, you have to take care of the screen updates which are neccessary due to model changes yourself. Calling setNeedsDisplay:YES on the NSView will not do anything.
So what one has to do if one has to update a view one should write a method like reloadData and in it one should call setNeedsDisplayon each CALayer that needs a refresh. I am still not sure if a call to this method on the root layer will propagate through all the children layers but I do not think so.
I solved the problem now by calling setNeedsDisplay on the individual CALayers that needed recaching. It works without problems.
There is also an oft-used practice of having an empty "drawrect", a la -(void) drawRect:(NSRect)dirtyRect {} to help coerce things into drawing, i believe via good ole view.needsDisplay = YES;.
and it should be noted.. that what is indeed happening is that - by saying your NSView *view; is layer.delegate = view; causes the layer to be drawn when [layer setNeedsDisplay]; is called.... via - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {...}..
along the same vein... when saying layer.layoutManager = view... subsequent demands that [layer setNeedsLayout]; will be fulfilled only when the - (void) layoutSublayersOfLayer:(CALayer *)layer {..} method is implemented..
These vital concepts are glossed over / strewn about in Apple's docs... and they are really so pivotal to making absolutely anything work at all.
You can automatically delegate the setNeedsDispay: by changing the redraw policy of the view. You have to assign NSViewLayerContentsRedrawOnSetNeedsDisplay to the property layerContentsRedrawPolicy (see https://developer.apple.com/documentation/appkit/nsview/1483514-layercontentsredrawpolicy). This will trigger redrawing of the layer when you send setNeedsDisplay: to the view:
[self setLayer:rootLayer];
[self setWantsLayer:YES];
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
or in Swift:
layer = rootLayer
wantsLayer = true
layerContentsRedrawPolicy = .onSetNeedsDisplay

How do I save state with CALayers?

I have a simple iphone application that paints blocks using a subclass of CALayer and am trying to find the best way to save state or persist the currently created layers.
I tried using Brad Larson's answer from this previous question on storing custom objects in the NSUserDefaults, which worked for persisting my subclass of CALayer, but not it's basic state like geometry and background color, so they were there but did not render on relaunch.
I made my declared instance variables conform to the NSCoding protocol but do not know how to make CALayer's properties do the same without re-declaring all of it's properties. Or is this not the correct/best approach altogether?
Here is the code I'm using to archive the array of layers:
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:viewController.view.layer.sublayers] forKey:#"savedArray"];
And here is the code I'm using to reload my layers in -viewDidLoad:
NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *dataRepresentingSavedArray = [currentDefaults objectForKey:#"savedArray"];
if (dataRepresentingSavedArray != nil) {
[self restoreStateWithData:dataRepresentingSavedArray];
And in -restoreStateWithData:
NSArray *savedLayers = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if (savedLayers != nil) {
for(layer in savedLayers) {
[self.view.layer addSublayer:layer];
}
[spaceView.layer layoutSublayers];
}
Just to be precise, according to the Apple docs, the CALayer is actually the model and not the view in the MVC pattern.
"CALayer is the model class for layer-tree objects. It encapsulates the position, size, and transform of a layer, which defines its coordinate system."
The view behind the layer is actually the view part of the pattern. A layer cannot be displayed without a backing view.
It seems to me that it should be a perfectly legitimate candidate for data serialization. Take a look at the KVC Extensions for CALayer. Particularly look at:
- (BOOL)shouldArchiveValueForKey:(NSString *)key
Cocoa (and Cocoa Touch) are mostly based on a model-view-controller organization. CALayers are in the view tier. This leads to two questions:
What part of your model does your layer present to the user?
How do you make that part of the model persist?
The answers to those questions are your solution.
Nowadays, for a new app, the simplest (certainly most extensible) path to a persistent model is probably Core Data.

Resources