Custom UIView drawRect is called even with UIViewContentModeScaleAspectFill - ios

I'm having the following issue.
Suppose I have my own image view class (to make it easy I've trimmed all the code and just included only the code that is causing the issue). That class inherits from UIView and overrides drawRect. Here is how my drawRect looks (the code is in Xamarin, but it's easy to understand)
public override void Draw(RectangleF rect)
{
base.Draw(rect);
CGContext context = UIGraphics.GetCurrentContext();
if (Image != null)
{
var imageRect = getAspectFillRect(rect, Image.Size);
context.DrawImage(imageRect, Image.CGImage);
}
else
{
this.BackgroundColor.SetFill();
context.FillRect(this.Bounds);
}
}
This code simply draws the image set to Image property in a way which simulates UIImageView's aspect fill option by calculating the rect which will display the image in aspect fill mode using this line of code getAspectFillRect(rect, Image.Size);
I also have UICollectionView with custom layout and custom cells. Each UICollectionViewCell UI is defined in an xib file, there I have all the necessary constraints set and I there I also have my custom view for displaying images. For that view in interface builder I've set content mode to "Aspect Fill". The custom UICollectionViewLayout is made in a way that cell expands (you'll see the example shortly). So the image inside the cell should also scale, and as I've set "Aspect Fill" option, it shouldn't call drawRect of my custom view, rather it should just scale already drawn content. But that is not the case!
LOOK HERE to see the video which demonstrates what happens.
You may see that the image inside is not growing together with the cell. The problem is that before ever the cell expanding animation begins drawRect of my custom UIView is called with the rect which will eventually be established after the animation. So I get a jerky animation. In my custom layout code I just call LayoutIfNeeded after updating the constraints of the cell. I don't call setNeedsDisplay, so I don't get why my drawRect is called.
For experiment I replaced my custom image view with UIImageView, I set it's content mode to "Aspect Fill", and LOOK HERE what happened. YEAH! It worked. So I suppose the issue is not in the custom UICollectionView layout, rather in my custom image drawing class.
What I should take into account?? How I should handle this case? Any Ideas?
Thanks!

DrawRect will be called whenever the system "feels" it should call it. Regardless if you call SetNeedsDisplay or not.
Now, setting ContentMode to ScaleAspectFill does not mean that DrawRect will not be called. In fact, from Apple docs here:
Instead of redrawing the contents of the view every time, you can use
this property to specify that you want to scale the contents (either
with or without distortion) or pin them to a particular spot on the
view.
This means that, if you don't want to go through the hassle of drawing the contents yourself, just set the ContentMode property. In your example, you are using DrawRect to draw.
Now, the fact that DrawRect is called before the animation starts, but with the target value for Bounds (or Frame?) means that the target value of these properties is set before the change finishes. During an animation, these properties do not change. Note during. A Bounds' or Frame's value will not change through all the values of an ongoing transition. It only knows "start" and "end".
What would happen in your app if DrawRect was not called before the animation start, but was called after animation end? Well, the cell would resize along with its subview, but your image would remain small throughout the animation and snap to the large size after the animation finished. So it's one way or the other.
Solutions:
Use the UIImageView.
If you do need to use a custom view, use a UIImageView on that to display your image.
Draw your image on a custom CALayer and add that layer on your custom view's Layer.

Related

When does a UIView know how big it is?

I have a subclass of UIView that draws a picture onto itself using CAShapeLayers. I want the drawing to adapt to whatever size the UIView is given by AutoLayout. I gave it a drawGraph() function that gets its current size from .bounds.size and then draws a picture to fit.
I started by calling my drawing function in the container UIViewController's viewDidLoad(), but at that point the layout hasn't happened yet so it has whatever bounds it had in the storyboard.
I tried calling it from the view controller's viewDidLayoutSubviews(), but that isn't totally satisfactory either. It gets called twice: first with the bounds from the storyboard, then again with the updated (correct) bounds. There's a pause in between, so the draw and redraw is visible to the user.
How can I tell if a UIView has had its layout constraints applied and is ready to give me accurate dimensions?
You should draw it in UIView's drawRect(rect: CGRect). Once view needs to draw on screen, this function will be called.
In my subclasses of UIView I usually do the code related to layout by overriding the layoutSubview() method. If I remember correctly it's more lightweight than drawRect(rect: CGRect).
But if you're really doing only drawing then you should probably do it in drawRect(rect: CGRect), as that's what Xcode suggests you when you create a new UIView subclass.

UILabel redraws the view. How to avoid this behavior?

I have a viewController with three subviews.
Also I use AutoLayout and size classes.
These views are animated and change location and size.
After the animations I update a label but the whole view is redrawn so each view is in their initial position and size. This shouldn't happen.
As in Apple developer reference that says:
"The default content mode of the UILabel class is
UIViewContentModeRedraw. This mode causes the view to redraw its
contents every time its bounding rectangle changes. You can change
this mode by modifying the inherited contentMode property of the
class."
It doesn't seem clear to me how to modify the -contentMode- in order to update that label and leave the view -as is-. Can anyone give me a clue?
Thanks in advance.
It sounds like you may be using autolayout to lay out your view (e.g., via constraints in IB), but then you're manipulating your views' frames directly for your animations - is this the case? If so, you should instead be animating the constant values of your constraints, or possibly the transforms of your subviews.
If you manipulate frames directly in a view which uses autolayout, your changes will be over-written the next time the system lays out your view (e.g. after a label's text changes).
You have 3 options to overcome your issue -
Stop using AutoLayout in your Storyboard/Xib.
Not a great solution
Animate changes to the transform property of your subviews. e.g. myView.transform = CGAffineTransformMakeScale(2.0,2.0);
Useful for presentation and dismissal animations, but mixing AutoLayout and transforms has some issues pre-iOS 8.0
Add IBOutlets for the constraints you need to change in your animations. Animate changes to the constant values of those constraints.
Most robust approach but can lead to a lot of properties and code for complex animations
Try contentMode = UIViewContentModeCenter. This should prevent the redraw you're seeing. Although that may not be the contentMode you want.
Option 2 is to use CATextLayer to draw the label. It will resize very nicely with animation, but it's a lot more work to set up.

What method is called on UIView when it's resized by autolayout?

I have an image view that I'm implementing rounded corners on by overriding the following in my subclass:
-(void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self.layout setCornerRadius:frame.size.width/10.0];
}
This works great for the initial display of the image, but if the size of the image changes due to device rotation or some other mechanism, this method does not get called to implement the resize.
I'm using autolayout, and I want to know what method of UIView (and thus UIImageView) is being called when my constraints resize the view so that I can recalculate my corner radius whenever this resize takes place. My (apparently false) assumption was that the autolayout system called setFrame: to move / resize views as needed.
From the docs on updateConstraints:
Custom views that set up constraints themselves should do so by overriding this method
....
Before layout is performed, your implementation of updateConstraints will be invoked, allowing you to verify that all necessary constraints for your content are in place at a time when your custom view’s properties are not changing.
Or from the docs on layoutSubviews:
You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly.
but if you need to do this on rotational changes, check out willTransitionToTraitCollection:withTransitionCoordinator:
Implementors of this method can use it to adapt the interface based on the values in the newCollection parameter. A common use of this method is to make changes to the high-level presentation style when the current size class changes.

Notification when visible area of CALayer changes?

I have a CALayer for which I provide content for only the visible area (somewhat similar to CATiledLayer). The problem is there does not seem to be a way to receive notification when the visible area of the CALayer changes so that displayLayer is called. I currently subclass and hook setPosition, setBounds, and setTransform, but this doesn't catch the cases where a superview/layer changes (for example, UIScrollView scrolls by changing the scroll views origin ). I'm left hooking parent views and sprinkling setNeedsDisplay all over the code.
Is there a better way?
The currently visible rect is [CALayer visibleRect]. This is set by the scroll view (layer) and is what you're expected to base drawing on in scroll views.
You probably want to override -needsDisplayOnBoundsChange to return YES. That's typically how you handle most of what you're describing.
If you want things like position to force a redraw (that's unusual, but possible), then you can override +needsDisplayForKey: to return YES for any key changes that you want to force a redraw.
If you want to make sure you're only drawing what you need to draw, then you should be checking your clipping box using CGContextGetClipBoundingBox() during your drawing code.

Adjusting UIButton's imageView properties

I'm having trouble figuring out how much I can adjust the imageView property of a UIButton. In the docs, it says:
Although this property is read-only, its own properties are read/write. Use these properties to configure the appearance and behavior of the button’s view.
I'm hoping to perform a transition where I shrink the size of the button in an animation, and I'd like the button's image to also shrink accordingly. However, I find that I have no control over the size of that image. If I modify either the frame or the bounds of UIButton's imageView property, nothing seems to happen. The image seems to want to retain it's standard dimensions. Changing autolayout properties on the imageView or contentMode properties on the button don't seem to help either.
In fact, the only thing that seems to work at all is to use UIEdgeInsetsMake to "squish" the image from all sides, but that isn't animatable.
I'd prefer not to use the backgroundImage property either, since I'm already using that to style the button in the first place.
You could add an own UIImageView as subview to the UIButton instead of using the "embedded" imageView.
You can set the frame of "your" image view as usually relative to the surrounding UIButton. You will have full control on it's position and you will also be able to animate it.

Resources