iPhone: Resizing in drawRect: - iphone

I have a UIView based control which I need to resize depending on various criteria and properties of the control. I'm not sure if the way I'm doing it is the best way but so far it's the only one I've found that seems to work. Because the size of the control is dependant on various properties, I cannot set the size in a constructor.
I have a method called setupControl: which contains all the code to finish the setup based on the properties set. I don't want setupControl: called manually, so in drawRect I detect if I need to call it and then queue a selector like this:
[self performSelector:#selector(setupControl)withObject:self afterDelay:0];
return;
At the bottom of setupControl: I then do:
[self setNeedsDisplay];
self.hidden = NO;
I've also overridden the initWithFrame: and initWithDecoder: constructors so that they set the UIView to be hidden to start with until the setup code is executed. The idea being to eliminate any "flashes" on the display as the control resizes.
As I said this works fine, the controls can be drawn ay size is the xib file and then set themselves to the correct size at run time.
My question is whether this method of queuing a selector, exiting the drawRect: and then using a setNeedsDisplay is the only way to do this? O is there some method I haven't found that I can override? or something else?

What you probably want to override is layoutSubviews rather than drawRect as you're changing the layout, not implementing custom drawing.
You may also have to use a custom setter for any properties that change the size of the view and call [self setNeedsLayout] in the setter to make sure your layoutSubviews method is called before your view size is computed.

Related

Adding UIView from class without frame

Is there a way to add a UIView from a function (in this case, within a statically built library) to a view without passing a frame for the view itself.
Paypal's library achieves this, and I was wondering how the implementation would go.
I don't want to change the root controller, I would just like to add my view over the current controller.
in the style of
[something addSubview:myNewView];
Where something is a view that I don't have access too.
Yes. You simply use the default init method instead of initWithFrame::
MyView *mySubview = [[MyView alloc] init];
[otherView addSubview:mySubview];
Note that this will result in a frame at origin (0/0) with zero size for mySubview and though it would be invisible. You could either specify the frame later, once you know it, or MyView can override the init method and pass a default frame to [super initWithFrame:defaultFrame].
While DrummberB's answer is correct, the view would not show up anywhere because a view initialized without a frame defaults to a frame of CGRectZero. Sooner or later you will need to assign a frame to the view.

On iOS, if a subclass of CALayer is used with an empty display method, then app seems to go into infinite loop?

If a Single View app is created, with a FooView that subclasses UIView, and do a
NSLog(#"hello");
in drawRect, then it is printed.
And if I create a subclass of CALayer called CoolLayer, and add this method to FooView.m:
+(Class) layerClass {
return [CoolLayer class];
}
and at the end of FooView.m's drawRect, do a
NSLog(#"layer's class is %#", self.layer.class);
then CoolLayer is printed. So now the view's underlaying layer is CoolLayer.
But when the following is added to CoolLayer.m:
-(void) display {
}
which is the method that is automatically called to redraw the layer (similar to drawRect), then no NSLog whatsoever was printed. It might be that the app went into an infinite loop. (even my touchesBegan that prints out NSLog messages is not printing). If a breakpoint is set at display, it will stop there once but when I continue the program, it will never arrive at display again. What is wrong with this and how can it be fixed?
The layer's display method will not be called again unless the layer is dirty, i.e. set to need a redisplay. This is usually a good thing, and is why you don't see the method being called more than once.
Also, the normal implementation of display will call the drawInContext: method. Since you override this in your subclass, the drawRect: method of the view is never called. You need to either replicate the standard behavior of CALayer, or call the superclass' display method in your own implementation.
That does not sound like an infinite loop. If you were in an infinite loop your app would freeze, and after a few seconds the springboard app would kill it for being unresponsive.
Call setNeedsDisplay or setNeedsDisplayInRect on your layer to make it "dirty" and require drawing again. Note that you don't want to call setNeedsDisplay any more than you have to, because it takes a lot of work to re-render the layer and push it's contents onto the screen. Only display when something has changed
You're not seeing an infinite loop here. If you were, as Duncan points out, you'd eventually crash either due to the watchdog timer or from an infinite recursion that would be immediately obvious in the stack trace you'd see in the debugger.
If you put an NSLog into your UIView's -drawRect: method, then override its default layer class with your own custom CALayer that does its own drawing, your UIView's -drawRect: would no longer be called. Drawing would now be handled by your custom backing layer.
As described in the "Providing CALayer Content by Subclassing" subsection of the Core Animation Programming Guide, you typically override -display in your CALayer if you want to somehow customize the contents CGImageRef for your layer. Normally, you'll be overriding -drawInContext: if you want to render custom Quartz drawing within your CALayer. Making your overridden -display method totally blank, and not writing anything to the contents property, is not standard behavior, so I'm not surprised that you're seeing odd results from doing that.
Based on your series of recent questions, I highly recommend you stop and spend some time reading the Core Animation Programming Guide and looking at some sample code involving CALayers before proceeding further.

why might layoutSubviews NOT automatically be called on a view when its frame is set?

I have a view. It is part of a large and complex project. When the frame of my view is set, layoutSubviews is not automatically called.
Playing around with this, I tried putting the following method into my view:
-(void) setFrame: (CGRect) frame {
NSLog (#"setting frame");
[super setFrame: frame];
[self setNeedsLayout];
}
At this point, layout was redone when the frame was set, as expected. And if I commented out the setNeedsLayout line, layout was not done. Furthermore, when I click on "step into" at the line where super is called, the debugger does not step into anything. So as far as I can tell, super's setFrame method is not being overridden.
FWIW, I also did a small test project in which I changed the frame of a view. In this case, layoutSubviews was called when I did that. [setNeedsLayout was not called in my test project.] So something in my large project is changing the behavior. But I don't know what that something is.
Can anyone explain what might be going on?
EDIT: to clarify, my frame change is changing the view's height. The width is not changed.

How to detect when a UIView has changed size?

I have a UIViewController that is initialised with a correct frame, however somewhere in my code the frame gets mangled and I'm having difficulty finding out where.
In situations like this it is usually handy to watch a variable in the debugger, however I have no way of accessing the controller->view->frame property in my variable view, since it isn't a variable, it's a property (surprisingly enough)
Drilling into the UIView in the variables display shows a few things but nothing I can relate to the frame, I thought perhaps that would be in layer but it isn't.
Is there any way to watch for changes in a private API? I guess not, since the variables are essentially 'hidden' and so you can't specify exactly what to watch.
Alternatively, what other approach could I use? I already tried subclassing UIView, setting my UIViewController's view to point to this subclass and breaking on the setFrame method but it didn't seem to work.
EDIT: the subclassing UIView method DID work, I just had to set the view to point to my test subclass in viewDidLoad and not the init method. Leaving this question open as I'm not sure if this is the best way of approaching this kind of problem...
Subclass your the view you want to track and rewrite the setFrame method:
#implementation MyTableView
- (void)setFrame:(CGRect)frame;
{
NSLog(#"%#", frame);
[super setFrame:frame];
}
#end
Then use the debugger to add a breakpoint to it and check when it gets called. Eventually, you'll see when the frame gets changed and where does the change comes from.
I discovered this can be done using key value observers.
http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/KeyValueObserving/KeyValueObserving.html
You could create an ivar, view2, and just assigned it to your view in your loadView method. That should enable you to watch it like a normal variable.

Calling setNeedsDisplay on a UIView from another class

I have a UIView subclass that I manipulate a lot of graphics on based on touches. All the [self setNeedsDisplay] calls seem to be fine.
However, I used an instance variable pointer to my same UIView subclass instance, and then tried manipulating it and then calling [UIViewSubClass setNeedsDisplay] from another UIView class, and DrawRect is never being called. Are there restrictions to where you can call setNeedsDisplay from?
(This method is called when a button is clicked on another UIView subclass. The method is being called, but not DrawRect)
-(IBAction)loadGrid2;
{
tempSoundArray = musicGridView1.soundArray;
[musicGridView1.soundArray setButtonArrayToNull];
[musicGridView1 setNeedsDisplay];
musicGridView1.soundArray = tempSoundArray;
NSLog(#"loadGrid2 was called");
}
drawRect: will only be called when it makes sense; ie, the view must be visible, onscreen, and dirty. Is your drawRect: ever called? It should be called when the view is first brought onscreen as well.
To add to Ben:
This most likely means that you have problems elsewhere. Your pointer may not be nil or otherwise invalid or the view may not be added to the hierarchy properly.
You may want to consider not handling this type behavior within the view and instead in the view controller. Control behavior and save presentation state in the view controller and don't subclass the view classes. It will simplify your code with less "pointer passing". This will also make it easier to debug this type of problem.
If you feel your view controller is getting bloated, consider splitting the responsibilities up among multiple view controllers.