drawRect depends on Views -> On Startup Custom View uses wrong frames - ios

My view hierachy looks like this: (I use Autolayout)
ContentView
CustomView (implements drawRect)
Button
TableView
In the drawRect of the CustomView I draw a UIBezierPath from the top to the bottom through all CustomView.SuperView.subViews where the line stops if a Frame is in the way and begins to draw further till the next frame and so on.
On Startup when the DrawRect is called the Views are not finished with layouting I guess, because the frames I get from the views are not correct.
The Problem is solved if I call
customView.setNeedsLayout()
customView.layoutIfNeeded()
in the ViewDidAppear method. But this impacts performance cause it gets called twice.
What's the right way to do this?

Your approach is a violation of the principle tell, don't ask. You should be telling your view where to draw; it shouldn't be asking.
Inside CustomView:
Get rid of any calls to self.superview.
Add a property that represents the information you need to draw (like #property NSArray *framesToSkip)
In drawRect: look in your property for the information you need.
In your view controller:
In viewWillLayoutSubviews:, update your custom view's framesToSkip property with the appropriate views.
If needed, call setNeedsDisplay on your custom view.
A few general rules that, if broken, indicate you might be violating tell, don't ask:
Never access a view's superview.
Never access a view controller's parentViewController or presentingViewController.
Never import a view controller class unless it's your child / presented view controller.

Related

When to add CAAnimation in a custom UIVIew?

The problem: my custom UIView animation shows only if I add the view in viewWillAppear or later.
The context: in my custom UIView I create and add the animation to the layer in my custom init function.
override init(color: UIColor) {
backgroundPulsatingDotView = RouteIndicatorDotView(color: color)
super.init(colour: color)
configureBackgroundPulsatingDot()
addPulsatingAnimation()
}
This custom UIView is deep inside the view hierarchy and ideally my view controller should not care to trigger the animation on its own or trigger the adding of view in viewWillAppear just because of this animation [besides other bugs which appear if using viewWillAppear or later stages for adding subviews]
Is there a way to still keep the logic of adding and starting the animation inside my custom view without needing a trigger from an external party which would say "hey now the view is visible, please animate yourself"?
Or where should I add the adding and starting of the animation inside my custom view lifecycle to make it work correctly with view controller life cycle?
Is there a way to still keep the logic of adding and starting the animation inside my custom view without needing a trigger from an external party which would say "hey now the view is visible, please animate yourself"?
There is nothing to animate until the view is part of the app's view hierarchy and has been initially drawn. You can't animate until then. That is why you are sent events marking the view's life stages, so that you can put your code in the right method which will run at the the right time in the view’s like cycle. init is the wrong method. It is too early.
Try overriding another lifetime method — perhaps something like layoutSubviews (though beware: this can be called many times, so you'll need to use a Bool flag so that you start the animation only the first time).
Also note that even then you can't add the dot and start animating it all in one move. After adding it, you must wait for the redraw moment to pass before you start animating it. What I usually do is step out to main thread with a very short delay so that my animation code runs after the current CATransaction has committed.

UIViewController subviews existance invariant

At what point in the UIViewController lifecycle is the subview property of self.view guaranteed to be populated with all the correct views? Note: I don't care about if they're laid out or not, just that they exist in the subview array. WWDC videos say that loadView, viewDidLoad, and init all don't come with that guarantee but viewWillLayoutSubviews is also late in the game.
The task I'm trying to perform in this instance is localization. In a common view controller class, I want to loop through all the subviews, see if they have a custom attribute set that identifies what localized string key is attached to that view, and then recurse through all that views subviews until the bottom of the view hierarchy is reached. Layout isn't important in this instance, just that the subviews are populated in the view controller.
At what point in the UIViewController lifecycle is the subview property of self.view guaranteed to be populated with all the correct views
The earliest point implemented by most apps is viewDidLoad. At that point you are guaranteed that self.view exists along with all the subviews from the storyboard, and that any outlets hooked up to this view controller from the storyboard have been populated.
I don't care about if they're laid out or not, just that they exist in the subview array
Exactly so. self.view and its nib-loaded subviews exist at this point, but their layout has not yet taken place and their frame is not necessarily correct. You don't care, so viewDidLoad is fine for your purposes.

MVC with view built in code

I've been trying to interpret the lessons from CS193P, and have a few questions.
I'm building views in code, the way I do it is I have a UIView subclass where I put all the views in place in the init method. This class is initialized by the ViewController.
The question is then, what is the right approach from here - say i want to animate a button I placed at 0,0 to 100,100. I'd like to animate it from the ViewController, but i don't like the fact that i set the 0,0 position in the UIView class (in the initializer) and now i am setting a new position in the ViewController. I'd prefer there would be just one place knowing about the actual (x,y) positions of my views.
How am i supposed to go about this?
Move the positions in the initializer to the ViewController
Put a method in my UIView "-(void)AnimateToSecondPosition" where the actual "second position" is then up to the view?
Just let it go. It seems like this would be the right approach if i had placed the button in interface builder - i consider interface builder to be the view then...
Or maybe even a fourth option?
Please help me understand it better and not just give me the right answer ;)
I'd like to be able to compare my approach in some way to how you would do it using interface builder, so each of my views are public and accessable from the controller - this way i believe i could easily start using interface builder instead if i wanted, without changing the controller code, just hooking up the outlets.
I'm guessing the case would be the same for disabling, hiding and doing other things with the views.
Thanks in advance.
If you want to create a new View programmatically you should generally instantiate it in your View Controller using its designated initialiser:
UIView *testView = [[UIView alloc]initWithFrame:myFrame];
If you create a custom view it's totally fine and correct to put some configuration code in the init method, but it's your ViewController that should be in charge of deciding what to do with this view - it is his job! Using the MVC the View and the Model should never communicate directly (as you definitely learned in the first lesson of CS193P).
Therefore the same apply to the animations. You should animate the Views within your ViewController and not implement the animation in the View itself.
Therefore in my opinion you "second position" should be setup by the VC - if this has to be done when something happens to the view (e.g. someone pressing a UIButton) you should set a target/action to your VC and handle this within your VC.
ADDED:
Regarding building UIViews in the Interface Builder I don't know what you mean by "and let them go". Interface builder will create the views and add them to the specific superviews at runtime - as you can see in the example below you control the view hierarchy graphically on the left. For instance in this case there is a UIView (which I coloured green for clarity) and two buttons. One is a subview of the main view while the other is a subview of the green UIView.
Once your ViewController is loaded the view hierarchy is automatically loaded to self.view - in fact if you run the following code in your VC when it is loaded you will see the list of self.views subviews in your console.
for (UIView *view in self.view.subviews){
NSLog("%#", [view description]);
}
If you know already that you need to change some attributes of a specific UIView you setup via Interface Builder (e.g. we know we want to change programmatically the color of the green UIView in the example above) you should create an outlet which allows you to have a reference to that view in your code. You do it by crtl-drag from the storyboard to your ViewController code - see the example below.
When you have done that you can refer in code to this as any other property, with the difference that it has been created by Interface Builder.
Hope this helps.
You can add an -setButtonFrameToSecondPosition to the view subclass, which simply updates the frame of the button, and then call that from the view controller via one of the +[UIView animate:...] methods.

Why can't I assign a UIView to self.view?

This might be a stupid question, but I'll shoot.
I made a little test project to test out a concept I had for a sliding view controller type of thing. I naively assumed I could create a UIView (let's call it peekView) with an outlet in a controller, and call something like [slidingControllerSlideFrom:self.view] from any visible view controller, the implementation of such being:
- (void)slidingControllerSlideFrom(UIView*)controllersMainView
{
// push side controller to top of navigation stack
self.peekView = controllersMainView;
// sliding animation
}
But there is no effect. No crash, no warning, no change of view in the pushed controller.
Of course, the pushed controller crashes when trying to add self's view as a subview, but assigning it to a predefined UIView just results in nothing.
So, why? And if a mere 'why' is not enough of a question- what happens when I try to assign one controller's view another controller's subview, and what was the reason for designing UIKit where you cannot set views from self.view?
To do that you have two options:
1 - If the controller in the peekView is always the same one in a given scene, use a "Container View". Those are explained here. Basically, they allow you to add a view in your scene that is managed by another controller.
2 - If the controller in the peekView depends on different conditions, you will have to create something similar to a custom tabbarcontroller. That means that you instantiate the controller that you need, add it's view as a subview of peekView (not assign the controller's view to the peekView itself) and then use didmovetoparentviewcontroller to notify the child controller. This question might help.
UPDATE:
Just saw your comment, so let me answer what you actually asked: The peekview property is actually just a reference to the real UIView you placed in the screen. When you do this:
self.peekView = controllersMainView;
You are changing the reference, but no the view object itself. That's why you are not seeing any changes. There are ways of adding a new view to the controller from code, but it is much simpler to simply use addSubview to add your controllers view to a UIView that is already in the controller.
Check out the discussion here: subView Slide up from bottom of the screen
and here: SubView slide in animation, iphone
Hopefully that gives you a bit of framework on how to approach this task!

ios what is opposite of layoutSubviews

I have a view that exits in its own class, with its own xib.
This view is initialized and added as a subview to my viewController view.
When the view is initialized, the method layoutSubviews is called where i customize some stuff in the view.
BUT which method is called when the view is removed from the superview (if any)?
For example, for a ViewController, viewWill/DidDisappear is called. Is there a similar method to a UIView (opposite to the layoutSubviews)?
Thanks in advance
---EDIT---
I just found a method that is called both on adding and removing a subView:
- (void)willMoveToSuperview:(UIView *)newSuperview
AND if newSuperview == 0, you can customize the removing of the subview.
Am i right or is it a tacky way to handle the situation?
BUT which method is called when the view is removed from the superview (if any)?
-removeFromSuperview is called, so you can override that if you need to do some housekeeping when the view is removed. Just remember to call super's version, too.
-layoutSubviews isn't necessarily only called when the view is added to a superview -- it's called whenever layout is needed. For example, it might be called when the orientation changes, or when the superview lays itself out again, or when the view's frame changes. There's really not an inverse of -layoutSubviews because none is needed. (What would it be called? -messupSubviews? ;-))

Resources