I want to create a UIView subclass that masks itself so that any child view's I add to it are cropped by a circle. I want this view and it's child views to be defined in IB, so that I can easily define layout constraints to the children. So far I have the following...
#interface BubbleView ()
// eg: this is an example of a child view that would be "under" a mask
#property(weak,nonatomic) IBOutlet UIImageView *imageView;
#end
#implementation BubbleView
// not really sure if this kind of init is the right pattern, but it seems to work and
// I don't think this is my current problem??
+ (instancetype)bubbleViewFromNib {
BubbleView *view = [[NSBundle mainBundle] loadNibNamed:#"BubbleView" owner:nil options:nil][0];
UIImage *_maskingImage = [UIImage imageNamed:#"circlemask"];
CALayer *maskingLayer = [CALayer layer];
[view.layer addSublayer:maskingLayer];
maskingLayer.contents = (__bridge id _Nullable)(_maskingImage.CGImage);
view.layer.mask = maskingLayer;
return view;
}
- (void)layoutSubviews {
self.layer.mask.frame = self.bounds;
}
(note: I give the view a purple color in IB so I can see what's going on)
This almost works, but when the owning view controller resizes this view, like this...
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
BubbleView *b = (BubbleView *)[self.view viewWithTag:222];
//b.transform = CGAffineTransformScale(b.transform, 1.2, 1.2);
b.frame = CGRectInset(b.frame, -30,-30);
}
The mask does something weird: It stays small "jumps" to the upper left corner of the view, then very quickly animates to the correct size (bigger by 30,30). Why would it animate?
Before...
Fast animation like this...
Placing NSLog in layoutSubviews, I notice it gets called twice, which is strange, but still not enough times to explain the quick animation.
Also, when I change the transform instead of the frame, it resizes perfectly, with no animation. But I need to do both frame and transform changes.
Can someone tell me where I've gone wrong?
When setting an animatable property of a layer, unless that layer is a UIView's primary layer, implicit animation is the default. Moreover, the frame property is merely a facade for the bounds and position properties. For this reason, you should never set a layer's frame directly; always set the bounds and position yourself. If you don't want animation, you'll need to turn off implicit animation for these properties; the simplest way is to turn it off entirely for the current CATransaction (setDisableActions to true), but there are more subtle and precise ways to accomplish the same thing.
Related
I have a class that I've used for a long time that draws a border around a UIView (or anything that inherits from UIView) and give that view rounded corners. I was doing some testing today (after upgrading to Xcode 7 and compiling on iOS 8.3 for the first time) and noticed that the right edge of the UIView is being truncated when I run on iPhone 6/6+ on the simulator (I don't have the actual devices, but I assume the results would be the same).
Here is a simple example. Notice how I've given the superview a red background to make this jump out. The subview is a UIView that has a fixed height and is vertically aligned to the center of the view. That works. The leading and trailing edges are supposed to be pinned to the edge of the superview, as you can see in the constraints in this image. Notice how the inner UILabel and UIButton are centered as they should be, but the UIView container is getting truncated on the right, even though the border is being drawn.
Here are the storyboard settings. The UIView that has the borders is of a fixed height, centered vertically, with leading and trailing edges pinned to the superview:
And finally, here is the code. In the UIViewController, I ask for borders like this. If I comment this code out, the view looks just fine, other than I don't have the borders that I want, of course.
BorderMaker *borderMaker = [[BorderMaker alloc] init];
[borderMaker makeBorderWithFourRoundCorners:_doneUpdatingView borderColor:[SharedVisualElements primaryFontColor] radius:8.0f];
And the BorderMaker class:
#implementation BorderMaker
- (void) makeBorderWithFourRoundCorners : (UIView *) view
borderColor : (UIColor *) borderColor
radius : (CGFloat) radius
{
UIRectCorner corners = UIRectCornerAllCorners;
CGSize radii = CGSizeMake(radius, radius);
[self drawBorder : corners
borderColor : borderColor
view : view
radii : radii];
}
- (void) drawBorder : (UIRectCorner) corners
borderColor : (UIColor *) borderColor
view : (UIView *) view
radii : (CGSize) radii
{
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:view.bounds
byRoundingCorners:corners
cornerRadii:radii];
// Mask the container view’s layer to round the corners.
CAShapeLayer *cornerMaskLayer = [CAShapeLayer layer];
[cornerMaskLayer setPath:path.CGPath];
view.layer.mask = cornerMaskLayer;
// Make a transparent, stroked layer which will dispay the stroke.
CAShapeLayer *strokeLayer = [CAShapeLayer layer];
strokeLayer.path = path.CGPath;
strokeLayer.fillColor = [UIColor clearColor].CGColor;
strokeLayer.strokeColor = borderColor.CGColor;
strokeLayer.lineWidth = 1.5; // the stroke splits the width evenly inside and outside,
// but the outside part will be clipped by the containerView’s mask.
// Transparent view that will contain the stroke layer
UIView *strokeView = [[UIView alloc] initWithFrame:view.bounds];
strokeView.userInteractionEnabled = NO; // in case your container view contains controls
[strokeView.layer addSublayer:strokeLayer];
// configure and add any subviews to the container view
// stroke view goes in last, above all the subviews
[view addSubview:strokeView];
}
Somewhere in that class, it seems that the views bounds are not reflecting the fact that AutoLayout has stretched the view to fill the larger iPhone 6/6+ screen width. Just a guess since I am out of ideas. Any help is appreciated. Thanks!
BorderMaker creates various layers and views based on the current size of the input view. How do those layers and views get resized when the input view changes size? Answer: they don't.
You could add code to update the size in various ways, but I wouldn't recommend it. Since you're rounding all four corners anyway, you can solve this better by just using the existing CALayer support for drawing a border, rounding the corners, and masking its contents.
Here's a simple BorderView class:
BorderView.h
#import <UIKit/UIKit.h>
IB_DESIGNABLE
#interface BorderView : UIView
#property (nonatomic, strong) IBInspectable UIColor *borderColor;
#property (nonatomic) IBInspectable CGFloat borderWidth;
#property (nonatomic) IBInspectable CGFloat cornerRadius;
#end
BorderView.m
#import "BorderView.h"
#implementation BorderView
- (void)setBorderColor:(UIColor *)borderColor {
self.layer.borderColor = borderColor.CGColor;
}
- (UIColor *)borderColor {
CGColorRef cgColor = self.layer.borderColor;
return cgColor ? [UIColor colorWithCGColor:self.layer.borderColor] : nil;
}
- (void)setBorderWidth:(CGFloat)borderWidth {
self.layer.borderWidth = borderWidth;
}
- (CGFloat)borderWidth {
return self.layer.borderWidth;
}
- (void)setCornerRadius:(CGFloat)cornerRadius {
self.layer.cornerRadius = cornerRadius;
}
- (CGFloat)cornerRadius {
return self.layer.cornerRadius;
}
#end
Now, if you create a view in your storyboard and set its custom class to BorderView, you can set up its border right in the storyboard:
Note that I set “Clip Subviews” in the storyboard, so it'll clip subviews if they happen to go outside the rounded bounds of the BorderView.
If you set up constraints on the BorderView, they'll keep everything sized and positioned:
I solved this. The problem is that I was calling these BorderMaker methods from within the viewDidLoad method of the UIViewController. All I had to do was to move this to viewDidAppear. Presumably, as Rob Mayoff suggested, the autolayout wasn't finished by the time that the view was passed to the BorderMaker class, so it was getting a frame that hadn't considered the size of the screen, but rather was just using the width defined in the IB.
After some trial and error, it seems that viewDidAppear is the earliest life cycle method that I can use where autolayout is done with its work.
I am trying to implement iOS7 parallax effect. For this purpose I am using standard UIInterpolatingMotionEffect class.
Here is what I am trying to achieve:
Place photo inside UIView. Photo is larger than View's frame (on each side)
Set View's size, and CornerRadius (with masksToBounds = YES)
Move phone and watch parallax effect. Something similar as you are peaking inside hole :)
Almost every tutorial on web is simply moving whole view (by setting center.x) , but I don't know how to move content only (and clip it in same time). I have tried something, but obviously is not working:
Inside viewDidLoad I am doing next:
- (void)viewDidLoad
{
[super viewDidLoad];
_myView.layer.contents = (id) [UIImage imageNamed:#"dog"].CGImage;
_myView.layer.contentsGravity = kCAGravityCenter;
_myView.layer.masksToBounds = YES;
UIInterpolatingMotionEffect *horizontalMotionEffect = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:#"layer.position.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
horizontalMotionEffect.minimumRelativeValue = #(-80);
horizontalMotionEffect.maximumRelativeValue = #(80);
[_myView addMotionEffect:horizontalMotionEffect];
}
CALayer don't have addMotionEffect so I am accessing view.
Maybe my approach is not good from start, so if you have some other solution - it will help.
Thank you for any help.
I think a better approach here might just be to use 2 separate views.
The wrapping UIView you have now, sized to what you want with a corner radius and clipsToBounds set to YES.
a UIImageView with the image you want, added as a subview of the UIView.
Then, just apply the motion effect to the image view. This will keep the containing view static and achieve the keyhole effect you are looking for.
I have a CAShapeLayer (which is the layer of a UIView subclass) whose path should update whenever the view's bounds size changes. For this, I have overridden the layer's setBounds: method to reset the path:
#interface CustomShapeLayer : CAShapeLayer
#end
#implementation CustomShapeLayer
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
self.path = [[self shapeForBounds:bounds] CGPath];
}
This works fine until I animate the bounds change. I would like to have the path animate alongside any animated bounds change (in a UIView animation block) so that the shape layer always adopts to its size.
Since the path property does not animate by default, I came up with this:
Override the layer's addAnimation:forKey: method. In it, figure out if a bounds animation is being added to the layer. If so, create a second explicit animation for animating the path alongside the bounds by copying all the properties from the bounds animation that gets passed to the method. In code:
- (void)addAnimation:(CAAnimation *)animation forKey:(NSString *)key
{
[super addAnimation:animation forKey:key];
if ([animation isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *basicAnimation = (CABasicAnimation *)animation;
if ([basicAnimation.keyPath isEqualToString:#"bounds.size"]) {
CABasicAnimation *pathAnimation = [basicAnimation copy];
pathAnimation.keyPath = #"path";
// The path property has not been updated to the new value yet
pathAnimation.fromValue = (id)self.path;
// Compute the new value for path
pathAnimation.toValue = (id)[[self shapeForBounds:self.bounds] CGPath];
[self addAnimation:pathAnimation forKey:#"path"];
}
}
}
I got this idea from reading David Rönnqvist's View-Layer Synergy article. (This code is for iOS 8. On iOS 7, it seems that you have to check the animation's keyPath against #"bounds" and not `#"bounds.size".)
The calling code that triggers the view's animated bounds change would look like this:
[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
self.customShapeView.bounds = newBounds;
} completion:nil];
Questions
This mostly works, but I am having two problems with it:
Occasionally, I get a crash on (EXC_BAD_ACCESS) in CGPathApply() when triggering this animation while a previous animation is still in progress. I am not sure whether this has anything to do with my particular implementation. Edit: never mind. I forgot to convert UIBezierPath to CGPathRef. Thanks #antonioviero!
When using a standard UIView spring animation, the animation of the view's bounds and the layer's path is slightly out of sync. That is, the path animation also performs in a springy way, but it does not follow the view's bounds exactly.
More generally, is this the best approach? It seems that having a shape layer whose path is dependent on its bounds size and which should animate in sync with any bounds changes is something that should just work™ but I'm having a hard time. I feel there must be a better way.
Other things I have tried
Override the layer's actionForKey: or the view's actionForLayer:forKey: in order to return a custom animation object for the path property. I think this would be the preferred way but I did not find a way to get at the transaction properties that should be set by the enclosing animation block. Even if called from inside an animation block, [CATransaction animationDuration] etc. always return the default values.
Is there a way to (a) determine that you are currently inside an animation block, and (b) to get the animation properties (duration, animation curve etc.) that have been set in that block?
Project
Here's the animation: The orange triangle is the path of the shape layer. The black outline is the frame of the view hosting the shape layer.
Have a look at the sample project on GitHub. (This project is for iOS 8 and requires Xcode 6 to run, sorry.)
Update
Jose Luis Piedrahita pointed me to this article by Nick Lockwood, which suggests the following approach:
Override the view's actionForLayer:forKey: or the layer's actionForKey: method and check if the key passed to this method is the one you want to animate (#"path").
If so, calling super on one of the layer's "normal" animatable properties (such as #"bounds") will implicitly tell you if you are inside an animation block. If the view (the layer's delegate) returns a valid object (and not nil or NSNull), we are.
Set the parameters (duration, timing function, etc.) for the path animation from the animation returned from [super actionForKey:] and return the path animation.
This does indeed work great under iOS 7.x. However, under iOS 8, the object returned from actionForLayer:forKey: is not a standard (and documented) CA...Animation but an instance of the private _UIViewAdditiveAnimationAction class (an NSObject subclass). Since the properties of this class are not documented, I can't use them easily to create the path animation.
_Edit: Or it might just work after all. As Nick mentioned in his article, some properties like backgroundColor still return a standard CA...Animation on iOS 8. I'll have to try it out.`
I know this is an old question but I can provide you a solution which appears similar to Mr Lockwoods approach. Sadly the source code here is swift so you will need to convert it to ObjC.
As mentioned before if the layer is a backing layer for a view you can intercept the CAAction's in the view itself. This however isn't convenient for example if the backing layer is used in more then one view.
The good news is actionForLayer:forKey: actually calls actionForKey: in the backing layer.
It's in the actionForKey: in the backing layer where we can intercept these calls and provide an animation for when the path is changed.
An example layer written in swift is as follows:
class AnimatedBackingLayer: CAShapeLayer
{
override var bounds: CGRect
{
didSet
{
if !CGRectIsEmpty(bounds)
{
path = UIBezierPath(roundedRect: CGRectInset(bounds, 10, 10), cornerRadius: 5).CGPath
}
}
}
override func actionForKey(event: String) -> CAAction?
{
if event == "path"
{
if let action = super.actionForKey("backgroundColor") as? CABasicAnimation
{
let animation = CABasicAnimation(keyPath: event)
animation.fromValue = path
// Copy values from existing action
animation.autoreverses = action.autoreverses
animation.beginTime = action.beginTime
animation.delegate = action.delegate
animation.duration = action.duration
animation.fillMode = action.fillMode
animation.repeatCount = action.repeatCount
animation.repeatDuration = action.repeatDuration
animation.speed = action.speed
animation.timingFunction = action.timingFunction
animation.timeOffset = action.timeOffset
return animation
}
}
return super.actionForKey(event)
}
}
I think you have problems because you play with the frame of a layer and it's path at the same time.
I would just go with CustomView that has custom drawRect: that draws what you need, and then just do
[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
self.customView.bounds = newBounds;
} completion:nil];
Sor the is no need to use pathes at all
Here is what i've got using this approach
https://dl.dropboxusercontent.com/u/73912254/triangle.mov
Found solution for animating path of CAShapeLayer while bounds is animated:
typedef UIBezierPath *(^PathGeneratorBlock)();
#interface AnimatedPathShapeLayer : CAShapeLayer
#property (copy, nonatomic) PathGeneratorBlock pathGenerator;
#end
#implementation AnimatedPathShapeLayer
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
if ([key rangeOfString:#"bounds.size"].location == 0) {
CAShapeLayer *presentationLayer = self.presentationLayer;
CABasicAnimation *pathAnim = [anim copy];
pathAnim.keyPath = #"path";
pathAnim.fromValue = (id)[presentationLayer path];
pathAnim.toValue = (id)self.pathGenerator().CGPath;
self.path = [presentationLayer path];
[super addAnimation:pathAnim forKey:#"path"];
}
[super addAnimation:anim forKey:key];
}
- (void)removeAnimationForKey:(NSString *)key {
if ([key rangeOfString:#"bounds.size"].location == 0) {
[super removeAnimationForKey:#"path"];
}
[super removeAnimationForKey:key];
}
#end
//
#interface ShapeLayeredView : UIView
#property (strong, nonatomic) AnimatedPathShapeLayer *layer;
#end
#implementation ShapeLayeredView
#dynamic layer;
+ (Class)layerClass {
return [AnimatedPathShapeLayer class];
}
- (instancetype)initWithGenerator:(PathGeneratorBlock)pathGenerator {
self = [super init];
if (self) {
self.layer.pathGenerator = pathGenerator;
}
return self;
}
#end
I think it is out of sync between bounds and path animation is because different timing function between UIVIew spring and CABasicAnimation.
Maybe you can try animate transform instead, it should also transform the path (untested), after it finished animating, you can then set the bound.
One more possible way is take snapshot the path, set it as content of the layer, then animate the bound, the content should follow the animation then.
I'm using [UIView animationWithDuration..] it works great, smooth.. but when I add this effect to UIView, it freezes...
_menuView.layer.masksToBounds = NO;
_menuView.layer.shadowOffset = CGSizeMake(-8, 10);
_menuView.layer.shadowRadius = 5;
_menuView.layer.shadowOpacity = 0.5;
Any ideas? How to fix that?
Thanks
You need to specify shadowPath, otherwise layer with recalculate the shadow each frame, which causes great performance overhead.
A common problem with shadows is that if you do not set the shadowPath, the performance will be very poor. Try adding
_menuView.layer.shadowPath = [UIBezierPath bezierPathWithRect:_menuView.layer.bounds].CGPath;
Assuming, of course, that the view you are animating is indeed rectangular (which is the optimization that I make in this case).
First of all, it's incorrect to animate layer's properties in the animation block, they are not animatable. Please, read the documentation.
https://developer.apple.com/library/ios/documentation/uikit/reference/uiview_class/UIView/UIView.html
Following UIView properties are animatable:
#property frame
#property bounds
#property center
#property transform
#property alpha
#property backgroundColor
#property contentStretch
You can use Core Animation and the view’s underlying layer to create the animation. Because view and layer objects are intricately linked together, changes to a view’s layer affect the view itself.
Take a look at this question, it describes how to animate cornerRadius:
Changing cornerRadius using Core Animation
I have to draw a custom border for a UIView.
I already have code to do that but it was written before we started using autolayout.
The code basically adds a sublayer of width/height=1.0f
-(void)BordeIzquierdo:(UIColor *)pcColor cfloatTamanio:(CGFloat )pcfloatTamanio
{
CALayer* clElemento = [self layer];
CALayer *clDerecho = [CALayer layer];
clDerecho.borderColor = [UIColor darkGrayColor].CGColor;
clDerecho.borderWidth = pcfloatTamanio;
clDerecho.frame = CGRectMake(0, 1, pcfloatTamanio, self.frame.size.height);
[clDerecho setBorderColor:pcColor.CGColor];
[clElemento addSublayer:clDerecho];
}
Now the problem is that with autolayout, this happens before layoutSubViews. So UIView.layer.frame is (0,0;0,0) and so the sublayer added is not shown because it has width/height=0.0f
Now how can I make this code without transferring this custom drawing to another method executed after viewDidLoad such as didLayoutSubviews.
I would like this styling to be correctly applied before viewDidLoad is called.
Is there anything like autolayout constraints that I can use with CALayers?
Any other ideas?
There is no CALayer autoresizing or constraints in iOS. (There is on Mac OS X, but no in iOS.)
Your requirements here are unreasonable. You say that you refuse to move the call to BordeIzquierdo:. But you can't give clDerecho a correct size until you know the size of self (and therefore self.layer). Half the art of Cocoa touch programming is putting your code in the right place. You must find the right place to put the call to BordeIzquierdo: so that the values you need have meaning at the moment it is called.
What I would do is put it in layoutSubviews along with a BOOL flag so that you only do it once.