Getting IB_DESIGNABLE to work properly


Graham Cox
 

Hi all,

I’m working on a small custom view project (Mac), and have it working generally quite well.

I decided to make it IB_DESIGNABLE, and that too mostly works - I can see the view in IB rendered with the correct background and border colours, I see all the inspectable properties, and I have established that the internal subviews it creates are constructed. This isn’t easy to determine with IB, because it doesn’t display anything I log; I have to rely on NSBeep() to tell me if a given bit of code ran and so on - this is very unsatisfactory, because I can’t debug the view in the IB case, only in the normal runtime case, where it works perfectly.

The problem I have is that the subviews I create as part of my view exist, but never draw. That means the appearance in IB is blank.

I’m out of ideas on what I need to do to get this to work. 

If anyone’s got a few minutes to take a quick look at the project and see if they can spot the problem, I’d be very grateful: http://s3.amazonaws.com/Mapdiva/Source/Test%20Projects/GCDigitArrayControl.zip

This shows the difference of the views in IB and at runtime:

Each digit in the display is a separate subview rendering one character, created programmatically when the view is instantiated.

—Graham



Quincey Morris
 

On Mar 3, 2018, at 20:29 , Graham Cox <graham@...> wrote:

If anyone’s got a few minutes to take a quick look at the project and see if they can spot the problem

AFAICT it doesn’t work like that. The canvas isn’t showing the view hierarchy, it’s showing the objects in the design hierarchy that you can reveal on the left. Of course, that represents the view hierarchy that will be created by nib loading, but that’s not happening at design/editing time.

I think your best choice is to eliminate the subviews, and do all the work in the parent view, using an internal “selected digit” mechanism. Your subviews aren’t so terribly complex that this is hard, just tedious.


Graham Cox
 

On 4 Mar 2018, at 4:14 pm, Quincey Morris <quinceymorris@...> wrote:

AFAICT it doesn’t work like that. The canvas isn’t showing the view hierarchy, it’s showing the objects in the design hierarchy that you can reveal on the left. Of course, that represents the view hierarchy that will be created by nib loading, but that’s not happening at design/editing time.
Thanks for having a look,

I see what you’re saying, but IB has to instantiate the view (at least once) to capture what it renders. It must also redo this capture whenever any of the properties change. I can prove that by adding NSBeep() to the -drawRect method - it beeps on every property change. By the same approach I know that my subviews are created - but a beep in their -drawRect: method doesn’t occur.

If I have this code in my view’s -drawRect:, it also beeps:

if( self.subviews.count > 8 )
NSBeep();

This demonstrates that the subviews are there, so it’s not that IB is forcing the subviews to be empty after instantiating the view, it’s just somehow preventing them rendering - I wondered maybe by invoking -drawRect: instead of -display. I can’t think why they’d do this, it makes the feature a lot less useful than it could be. However, even if I call -display on all the subviews myself as part of -drawRect:, they don’t draw, so there’s something tricky going on.



I think your best choice is to eliminate the subviews, and do all the work in the parent view, using an internal “selected digit” mechanism. Your subviews aren’t so terribly complex that this is hard, just tedious.
Not worth the rewrite for this extra nicety, TBH.

—Graham


Jeremy Hughes
 

Hi Graham,

Are you setting needsDisplay for each of the subviews?

Jeremy


Graham Cox
 

On 4 Mar 2018, at 10:43 pm, Jeremy Hughes via Groups.Io <moon.rabbit@...> wrote:

Hi Graham,

Are you setting needsDisplay for each of the subviews?

Jeremy

Yes - the view works properly in the normal runtime case.

I’ve determined that the reason is that the IB ‘agent’ for previewing an IB_DESIGNABLE view directly calls -drawRect: on the view - it does not use the proper hierarchical view rendering process. So any subviews are just not drawn. It may be doing this for some good reason - performance, or security perhaps (since it’s running ‘foreign’ code that it can’t vet) - but this minimises the usefulness of the IB_DESIGNABLE feature. I’m considering whether to report this as a bug/enhancement request, since it means no view that uses subviews it creates programmatically will render properly.

As Quincey suggested, in this case I could reorganise my view to be flat and get around this, but it would be a better solution for IB to render the view using -display instead of -drawRect: - it would make IB better. Since they keep taking away stuff from us (e.g. the support for IB plug-ins) it would be nice if they could occasionally give something back.


—Graham


Quincey Morris
 

On Mar 4, 2018, at 14:10 , Graham Cox <graham@...> wrote:

the IB ‘agent’ for previewing an IB_DESIGNABLE view directly calls -drawRect: on the view - it does not use the proper hierarchical view rendering process

An interesting observation. I went back to your test project and made the digit-array drawRect call drawRect on the subview, and they draw in IB!

Unfortunately, they draw in the wrong place. If you can figure out why their frame is not what you originally set (or compensate somehow), you presumably could get this to draw right.

This assumes that your digit-array view knows it’s drawing for IB, and it *can* know that. There’s a “prepareForInterfaceBuilder” method that’s supposedly called to let you know you’re in that environment.


Quincey Morris
 

On Mar 4, 2018, at 14:29 , Quincey Morris <quinceymorris@...> wrote:

Unfortunately, they draw in the wrong place

Oh, of course, by calling their drawRect, it makes them draw in parent bounds coordinates, not their own bounds coordinates, but you can compensate for that.


Quincey Morris
 

On Mar 4, 2018, at 14:31 , Quincey Morris <quinceymorris@...> wrote:

you can compensate for that

Works, too!

- (void) prepareForInterfaceBuilder
{
mIsInterfaceBuilder = YES;
}

- (void) drawRect:(NSRect) dirtyRect 
{

if (mIsInterfaceBuilder)
{
for (GCDigitView* subview in self.subviews)
{
if (subview.isHidden || ![subview isKindOfClass: [GCDigitView class]])
continue;

[subview drawRect: dirtyRect offset: YES];
}
}
}

- (void) drawRect:(NSRect) dirtyRect
{
[self drawRect: dirtyRect offset: NO];
}

- (void) drawRect:(NSRect) dirtyRect offset: (BOOL) isOffset
{
NSRect dbr = [self highlightRectForMask:kGCBothHalvesHighlight];
if (isOffset)
dbr = NSOffsetRect (dbr, self.frame.origin.x, self.frame.origin.y);


Gary L. Wade
 

Looking at the code, it appears you’re missing a call to [super drawRect:dirtyRect] in your own drawRect: method.

On Mar 3, 2018, at 8:29 PM, Graham Cox <graham@...> wrote:

Hi all,

I’m working on a small custom view project (Mac), and have it working generally quite well.

I decided to make it IB_DESIGNABLE, and that too mostly works - I can see the view in IB rendered with the correct background and border colours, I see all the inspectable properties, and I have established that the internal subviews it creates are constructed. This isn’t easy to determine with IB, because it doesn’t display anything I log; I have to rely on NSBeep() to tell me if a given bit of code ran and so on - this is very unsatisfactory, because I can’t debug the view in the IB case, only in the normal runtime case, where it works perfectly.

The problem I have is that the subviews I create as part of my view exist, but never draw. That means the appearance in IB is blank.

I’m out of ideas on what I need to do to get this to work. 

If anyone’s got a few minutes to take a quick look at the project and see if they can spot the problem, I’d be very grateful: http://s3.amazonaws.com/Mapdiva/Source/Test%20Projects/GCDigitArrayControl.zip

This shows the difference of the views in IB and at runtime:

<Screen Shot 2018-03-04 at 3.23.08 pm.png><Screen Shot 2018-03-04 at 3.26.35 pm.png>
Each digit in the display is a separate subview rendering one character, created programmatically when the view is instantiated.

—Graham




Graham Cox
 

On 5 Mar 2018, at 10:46 am, Gary L. Wade <garywade@...> wrote:

Looking at the code, it appears you’re missing a call to [super drawRect:dirtyRect] in your own drawRect: method.
I’ve never called super, nor understand why it would be needed. I know the boilerplate calls it, but it appears to do nothing.

If there has been a change in the recommendation for this since 10.2 or so, then I missed it.

—Graham


Graham Cox
 

On 5 Mar 2018, at 9:57 am, Quincey Morris <quinceymorris@...> wrote:

On Mar 4, 2018, at 14:31 , Quincey Morris <quinceymorris@...> wrote:

you can compensate for that
Works, too!

- (void) prepareForInterfaceBuilder
{
mIsInterfaceBuilder = YES;
}

- (void) drawRect:(NSRect) dirtyRect
{


if (mIsInterfaceBuilder)
{
for (GCDigitView* subview in self.subviews)
{
if (subview.isHidden || ![subview isKindOfClass: [GCDigitView class]])
continue;

[subview drawRect: dirtyRect offset: YES];
}
}
}
- (void) drawRect:(NSRect) dirtyRect
{
[self drawRect: dirtyRect offset: NO];
}

- (void) drawRect:(NSRect) dirtyRect offset: (BOOL) isOffset
{
NSRect dbr = [self highlightRectForMask:kGCBothHalvesHighlight];
if (isOffset)
dbr = NSOffsetRect (dbr, self.frame.origin.x, self.frame.origin.y);
Cool - I’ll give that a shot. Thanks!

—Graham


Graham Cox
 

On 5 Mar 2018, at 11:12 am, Graham Cox <graham@...> wrote:



On 5 Mar 2018, at 9:57 am, Quincey Morris <quinceymorris@...> wrote:

On Mar 4, 2018, at 14:31 , Quincey Morris <quinceymorris@...> wrote:

you can compensate for that
Works, too!

- (void) prepareForInterfaceBuilder
{
mIsInterfaceBuilder = YES;
}
I’ve gone for something slightly different that’s a little more generic (it’ll work for any view, not relying on it being a specific class). It still wouldn’t work for a layer backed or hosting view though, but it’s good for this case. Thanks for the idea.


//… -drawRect: ….


if( mIsInterfaceBuilder )
{
for( NSView* subView in self.subviews )
{
if( !subView.isHidden )
{
NSRect svf = subView.frame;

if( NSIntersectsRect( svf, dirtyRect ))
{
NSAffineTransform* tfm = [NSAffineTransform transform];
NSRect dr = [self convertRect:dirtyRect toView:subView];

[tfm translateXBy:svf.origin.x yBy:svf.origin.y];
[NSGraphicsContext saveGraphicsState];
[tfm concat];
[subView drawRect:dr];
[NSGraphicsContext restoreGraphicsState];
}
}
}
}


I’m still inclined to report this as an enhancement/bug against IB though, I feel it *should* work without this workaround.

—Graham


Gary L. Wade
 

You do want your subviews drawn, right?

On Mar 4, 2018, at 4:12 PM, Graham Cox <graham@...> wrote:



On 5 Mar 2018, at 10:46 am, Gary L. Wade <garywade@...> wrote:

Looking at the code, it appears you’re missing a call to [super drawRect:dirtyRect] in your own drawRect: method.


I’ve never called super, nor understand why it would be needed. I know the boilerplate calls it, but it appears to do nothing.

If there has been a change in the recommendation for this since 10.2 or so, then I missed it.

—Graham


Graham Cox
 

On 5 Mar 2018, at 11:33 am, Gary L. Wade <garywade@...> wrote:

You do want your subviews drawn, right?

Of course, but they’re not drawn by -drawRect:, they’re drawn by an internal private method that calls -drawRect: Normally, drawRect is concerned only with the drawing pertaining to itself and nothing else, not even its subviews. If you set a breakpoint in -drawRect:, you can see the call stack:

#0 0x000000010000987c in -[GCDigitView drawRect:] at /Users/grahamcox/Projects/GCDigitArrayControl/GCDigitArrayControl/GCDigitArrayControl.m:1190
#1 0x00007fff409d8c21 in _NSViewDrawRect ()
#2 0x00007fff409e5eb9 in -[NSView(NSInternal) _recursive:displayRectIgnoringOpacity:inGraphicsContext:shouldChangeFontReferenceColor:] ()
#3 0x00007fff409e5889 in -[NSView(NSInternal) _recursive:displayRectIgnoringOpacity:inContext:shouldChangeFontReferenceColor:] ()
#4 0x00007fff4027746c in __46-[NSView(NSLayerKitGlue) drawLayer:inContext:]_block_invoke ()
#5 0x00007fff40277000 in -[NSView(NSLayerKitGlue) _drawViewBackingLayer:inContext:drawingHandler:] ()
#6 0x00007fff402769d1 in -[NSView(NSLayerKitGlue) drawLayer:inContext:] ()
#7 0x00007fff4dc85ad5 in CABackingStoreUpdate_ ()
#8 0x00007fff4dc859b4 in ___ZN2CA5Layer8display_Ev_block_invoke ()
#9 0x00007fff4dc851b7 in -[CALayer _display] ()
#10 0x00007fff40275996 in _NSBackingLayerDisplay ()
#11 0x00007fff4026a80d in -[_NSViewBackingLayer display] ()
#12 0x00007fff4dc76d3b in CA::Layer::display_if_needed(CA::Transaction*) ()
#13 0x00007fff4dc767f9 in CA::Layer::layout_and_display_if_needed(CA::Transaction*) ()
#14 0x00007fff4dc75894 in CA::Context::commit_transaction(CA::Transaction*) ()
#15 0x00007fff4dc7543d in CA::Transaction::commit() ()
#16 0x00007fff40a21658 in __65+[CATransaction(NSCATransaction) NS_setFlushesWithDisplayRefresh]_block_invoke ()
#17 0x00007fff42b66127 in __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ ()


This is for my subview. As you can see, there are a lot of internal things going on, but the superView’s -drawRect: is not one of them.

In any case, calling [super drawRect:] in my outer view makes no difference to whether the subviews are drawn or not.

—Graham