Resize a CALayer with animation in Swift - ios

I have added a CALayer to an image doing the following :
var menulayer = CALayer()
menulayer.frame = CGRectMake(0.0, 0.0, menuimage.frame.size.width, menuimage.frame.size.height);
menulayer.backgroundColor = bartint.CGColor
menulayer.opacity = 1
menuimage.layer.addSublayer(menulayer)
I want to animate the layer so that it reveals the image from left to right. I have tried this:
let width = CGFloat(0)
CATransaction.begin()
CATransaction.setAnimationDuration(2.0)
self.menulayer.bounds = CGRectMake(0, 0, width , 50)
CATransaction.commit()
But the animation starts at the center of the image, how can I make it start from the left edge of the image ?

So animate the origin of the layer to shift it to the right until your image view is fully revealed. You will need to set menuImage.layer.masksToBounds = true so the cover layer isn't visible as it scrolls out of the frame of the image view.

Related

UIKit: How to mix several CALayers into one view layer?

I would like to mix several sublayers into only one layer.
If I simply assign my modifications to the view layer, it works fine:
testView.layer.cornerRadius = 15.0
testView.layer.shadowColor = UIColor.yellow.withAlphaComponent(1.0).cgColor
testView.layer.shadowOpacity = 1.0
testView.layer.shadowRadius = 24.0
testView.layer.shadowOffset = .zero
testView.backgroundColor = .white
Then, I tried this:
testView.layer.cornerRadius = 15.0
// sl1
let layer1 = CALayer()
layer1.shadowColor = UIColor.yellow.withAlphaComponent(1.0).cgColor
layer1.shadowOpacity = 1.0
layer1.shadowRadius = 24.0
layer1.shadowOffset = .zero
// sl2
let layer2 = CALayer()
layer2.shadowColor = UIColor.red.withAlphaComponent(1.0).cgColor
layer2.shadowOpacity = 1.0
layer2.shadowRadius = 24.0
layer2.shadowOffset = .zero
// sublayers
let layer = CALayer()
testView.layer.sublayers = [layer1, layer2]
testView.backgroundColor = .white
But now here is the result I get
There is now no shadow surrounding the white view. Why is this happening?
Thank you for your help
The chief problem with your code is that none of your layers have any size. Thus for example you see no shadow, because the layer itself is of zero size in the top left corner so there is nothing there to cast any shadow.
It is your job to give a layer a frame immediately after creating it! Typically this will be the same as the bounds of the superlayer. Keep in mind, however, that unlike views, when a view or superlayer is resized, sublayers are not. Thus the sublayer can cease to "fit" its superlayer properly if you don't take measures to correct that.

Corners of button not staying rounded when using constraints constraints

I have two buttons in a stack view. I have used an extension of UIButton to round the outside corners. This works on the 7Plus which I designed for in storyboard but as soon as I run on a smaller device size in the simulator it stops working and I can only round corners on the left side of either button and not the right. Any ideas?
On a 7Plus
On a 7
These are the extensions I'm using
extension CGSize{
init(_ width:CGFloat,_ height:CGFloat) {
self.init(width:width,height:height)
}
}
extension UIButton{
func roundOneSide(topCorner: UIRectCorner, bottomCorner: UIRectCorner){
let maskPAth1 = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [topCorner , bottomCorner],
cornerRadii:CGSize(6.0, 6.0))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = self.bounds
maskLayer1.path = maskPAth1.cgPath
self.layer.mask = maskLayer1
}
}
I have also tried the following code to no avail. It can only successfully round corners on the left when constraints come into play.
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 6, height: 6)).cgPath
facebookBtn.layer.mask = maskLayer
facebookBtn.layer.masksToBounds = true
Use you use segment Control as jerky said or
Try this below code:
// For login button
UIBezierPath *cornersPathLeft = [UIBezierPath bezierPathWithRoundedRect:buttonLogin.bounds byRoundingCorners:(UIRectCornerBottomLeft|
UIRectCornerTopLeft) cornerRadii:CGSizeMake(5, 5)];
//Create a new layer to use as a mask
CAShapeLayer *maskLayerLeft = [CAShapeLayer layer];
// Set the path of the layer
maskLayerLeft.path = cornersPathLeft.CGPath;
buttonLogin.layer.mask = maskLayerLeft;
// For FB button
UIBezierPath *cornersPathRight = [UIBezierPath bezierPathWithRoundedRect:buttonFB.bounds byRoundingCorners:(UIRectCornerTopRight|
UIRectCornerBottomRight) cornerRadii:CGSizeMake(5, 5)];
CAShapeLayer *maskLayerRight = [CAShapeLayer layer];
maskLayerRight.path = cornersPathRight.CGPath;
buttonFB.layer.mask = maskLayerRight;
NOTE:
do maskToBounds = true it allows to give effect on the layers.
-
Difference Between MaskToBounds and ClipsToBounds
MaskToBounds
Any sublayers of the layer that extend outside its boundaries will be clipped to those boundaries. Think of the layer, in that case, as a window onto its sublayers; anything outside the edges of the window will not be visible. When masksToBounds = NO, no clipping occurs.
When the value of this property is true, Core Animation creates an implicit clipping mask that matches the bounds of the layer and includes any corner radius effects. If a value for the mask property is also specified, the two masks are multiplied to get the final mask value.
ClipsToBounds
The use case for clipsToBounds is more for subviews which are partially outside the main view.
For example, I have a (circular) subview on the edge of its parent (rectangular) UIView. If you set clipsToBounds to YES, only half the circle/subview will be shown. If set to NO, the whole circle will show up. Just encountered this so wanted to share
Conclusion
MaskToBounds are applied for the sublayer of any view. Like here OP added layer over button but it does not give effects. I mean layer is not bounds properly.
ClipToBounds are applied on the subVies of any view. Assume you have you have a view( says, viewBG ) and and now you added another view (says, upperView), now you dont wanted to see view upper to look outside the viewBG. ViewUpper always bounded inside its superview. so in this case you have to true the clipstobounds.
Practical experience
Try this below code in swift
let viewBG : UIView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
viewBG.backgroundColor = UIColor.lightGray
self.view.addSubview(viewBG)
let viewUpper : UIView = UIView(frame: CGRect(x: -50, y: -50, width: 100, height: 100))
viewUpper.backgroundColor = UIColor.blue
viewBG.addSubview(viewUpper)
Output
1. When i done viewBG.clipsToBounds = false
When i done viewBG.clipsToBounds = true
You need to enable clipToBound property of button in storyboard;
You could try a different, very simple method of curving the corners on a UIButton or Label.
Click on button
Click on identity inspector
Then add an attribute in the identity section
Key path is layer.cornerRadius
Set as Number
Finally give a value, the higher the value, the more rounded the corners are.
Well that was an easy fix. I just needed to round the corners in viewWillLayoutSubviews.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
loginBtn.roundOneSide(topCorner: .topLeft, bottomCorner: .bottomLeft)
facebookBtn.roundOneSide(topCorner: .topRight, bottomCorner: .bottomRight)
}

Using a UILabel Sublayer to Cut Off Corners Overlaying an Image

I've encountered a problem with code I'd written to cut off the corners of a UILabel (or, indeed, any UIView-derived object to which you can add sublayers) -- I do have to thank Kurt Revis for his answer to Use a CALayer to add a diagonal banner/badge to the corner of a UITableViewCell that pointed me in this direction.
I don't have a problem if the corner overlays a solid color -- it's simple enough to make the cut-off corner match that color. But if the corner overlays an image, how would you let the image show through?
I've searched SO for anything similar to this problem, but most of those answers have to do with cells in tables and all I'm doing here is putting a label on a screen's view.
Here's the code I use:
-(void)returnChoppedCorners:(UIView *)viewObject
{
NSLog(#"Object Width = %f", viewObject.layer.frame.size.width);
NSLog(#"Object Height = %f", viewObject.layer.frame.size.height);
CALayer* bannerLeftTop = [CALayer layer];
bannerLeftTop.backgroundColor = [UIColor blackColor].CGColor;
// or whatever color the background is
bannerLeftTop.bounds = CGRectMake(0, 0, 25, 25);
bannerLeftTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerLeftTop.position = CGPointMake(10, 10);
bannerLeftTop.affineTransform = CGAffineTransformMakeRotation(-45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerLeftTop];
CALayer* bannerRightTop = [CALayer layer];
bannerRightTop.backgroundColor = [UIColor blackColor].CGColor;
bannerRightTop.bounds = CGRectMake(0, 0, 25, 25);
bannerRightTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerRightTop.position = CGPointMake(viewObject.layer.frame.size.width - 10.0, 10.0);
bannerRightTop.affineTransform = CGAffineTransformMakeRotation(45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerRightTop];
}
I'll be adding similar code to do the BottomLeft and BottomRight corners, but, right now, those are corners that overlay an image. The bannerLeftTop and bannerRightTop are actually squares that are rotated over the corner against a black background. Making them clear only lets the underlying UILabel background color appear, not the image. Same for using the z property. Is masking the answer? Oo should I be working with the underlying image instead?
I'm also encountering a problem with the Height and Width being passed to this method -- they don't match the constrained Height and Width of the object. But we'll save that for another question.
What you need to do, instead of drawing an opaque corner triangle over the label, is mask the label so its corners aren't drawn onto the screen.
Since iOS 8.0, UIView has a maskView property, so we don't actually need to drop to the Core Animation level to do this. We can draw an image to use as a mask, with the appropriate corners clipped. Then we'll create an image view to hold the mask image, and set it as the maskView of the label (or whatever).
The only problem is that (in my testing) UIKit won't resize the mask view automatically, either with constraints or autoresizing. We have to update the mask view's frame “manually” if the masked view is resized.
I realize your question is tagged objective-c, but I developed my answer in a Swift playground for convenience. It shouldn't be hard to translate this to Objective-C. I didn't do anything particularly “Swifty”.
So... here's a function that takes an array of corners (specified as UIViewContentMode cases, because that enum includes cases for the corners), a view, and a “depth”, which is how many points each corner triangle should measure along its square sides:
func maskCorners(corners: [UIViewContentMode], ofView view: UIView, toDepth depth: CGFloat) {
In Objective-C, for the corners argument, you could use a bitmask (e.g. (1 << UIViewContentModeTopLeft) | (1 << UIViewContentModeBottomRight)), or you could use an NSArray of NSNumbers (e.g. #[ #(UIViewContentModeTopLeft), #(UIViewContentModeBottomRight) ]).
Anyway, I'm going to create a square, 9-slice resizable image. The image will need one point in the middle for stretching, and since each corner might need to be clipped, the corners need to be depth by depth points. Thus the image will have sides of length 1 + 2 * depth points:
let s = 1 + 2 * depth
Now I'm going to create a path that outlines the mask, with the corners clipped.
let path = UIBezierPath()
So, if the top left corner is clipped, I need the path to avoid the top left point of the square (which is at 0, 0). Otherwise, the path includes the top left point of the square.
if corners.contains(.TopLeft) {
path.moveToPoint(CGPoint(x: 0, y: 0 + depth))
path.addLineToPoint(CGPoint(x: 0 + depth, y: 0))
} else {
path.moveToPoint(CGPoint(x: 0, y: 0))
}
Do the same for each corner in turn, going around the square:
if corners.contains(.TopRight) {
path.addLineToPoint(CGPoint(x: s - depth, y: 0))
path.addLineToPoint(CGPoint(x: s, y: 0 + depth))
} else {
path.addLineToPoint(CGPoint(x: s, y: 0))
}
if corners.contains(.BottomRight) {
path.addLineToPoint(CGPoint(x: s, y: s - depth))
path.addLineToPoint(CGPoint(x: s - depth, y: s))
} else {
path.addLineToPoint(CGPoint(x: s, y: s))
}
if corners.contains(.BottomLeft) {
path.addLineToPoint(CGPoint(x: 0 + depth, y: s))
path.addLineToPoint(CGPoint(x: 0, y: s - depth))
} else {
path.addLineToPoint(CGPoint(x: 0, y: s))
}
Finally, close the path so I can fill it:
path.closePath()
Now I need to create the mask image. I'll do this using an alpha-only bitmap:
let colorSpace = CGColorSpaceCreateDeviceGray()
let scale = UIScreen.mainScreen().scale
let gc = CGBitmapContextCreate(nil, Int(s * scale), Int(s * scale), 8, 0, colorSpace, CGImageAlphaInfo.Only.rawValue)!
I need to adjust the coordinate system of the context to match UIKit:
CGContextScaleCTM(gc, scale, -scale)
CGContextTranslateCTM(gc, 0, -s)
Now I can fill the path in the context. The use of white here is arbitrary; any color with an alpha of 1.0 would work:
CGContextSetFillColorWithColor(gc, UIColor.whiteColor().CGColor)
CGContextAddPath(gc, path.CGPath)
CGContextFillPath(gc)
Next I create a UIImage from the bitmap:
let image = UIImage(CGImage: CGBitmapContextCreateImage(gc)!, scale: scale, orientation: .Up)
If this were in Objective-C, you'd want to release the bitmap context at this point, with CGContextRelease(gc), but Swift takes care of it for me.
Anyway, I convert the non-resizable image to a 9-slice resizable image:
let maskImage = image.resizableImageWithCapInsets(UIEdgeInsets(top: depth, left: depth, bottom: depth, right: depth))
Finally, I set up the mask view. I might already have a mask view, because you might have clipped the view with different settings already, so I'll reuse an existing mask view if it is an image view:
let maskView = view.maskView as? UIImageView ?? UIImageView()
maskView.image = maskImage
Finally, if I had to create the mask view, I need to set it as view.maskView and set its frame:
if view.maskView != maskView {
view.maskView = maskView
maskView.frame = view.bounds
}
}
OK, how do I use this function? To demonstrate, I'll make a purple background view, and put an image on top of it:
let view = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
view.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
let backgroundView = UIView(frame: view.frame)
backgroundView.backgroundColor = UIColor.purpleColor()
backgroundView.addSubview(view)
XCPlaygroundPage.currentPage.liveView = backgroundView
Then I'll mask some corners of the image view. Presumably you would do this in, say, viewDidLoad:
maskCorners([.TopLeft, .BottomRight], ofView: view, toDepth: 50)
Here's the result:
You can see the purple background showing through the clipped corners.
If I were to resize the view, I'd need to update the mask view's frame. For example, I might do this in my view controller:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.cornerClippedView.maskView?.frame = self.cornerClippedView.bounds
}
Here's a gist of all the code, so you can copy and paste it into a playground to try out. You'll have to supply your own adorable test image.
UPDATE
Here's code to create a label with a white background, and overlay it (inset by 20 points on each side) on the background image:
let backgroundView = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
let label = UILabel(frame: backgroundView.bounds.insetBy(dx: 20, dy: 20))
label.backgroundColor = UIColor.whiteColor()
label.font = UIFont.systemFontOfSize(50)
label.text = "This is the label"
label.lineBreakMode = .ByWordWrapping
label.numberOfLines = 0
label.textAlignment = .Center
label.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
backgroundView.addSubview(label)
XCPlaygroundPage.currentPage.liveView = backgroundView
maskCorners([.TopLeft, .BottomRight], ofView: label, toDepth: 50)
Result:

Clip top side of UIImage in ellipse shape

I've got the following screen design :
I want to render MKMapView in UIImage, than apply elipsis UIBezierPath and clip top part of UIImage. How can i achieve this? Thanks in advance.
Here is a simple implementation that you can follow to have the similar effect using CAShapeLayer.
Create sufficiently ellipse path to fit height of your image view, but width can be adjusted to control curve.
Create rectangular path to fit the size of the imageView, width and height should be match the size of imageView.
Transform the circle in such a way that rectangular path is exactly at the middle of the circle.
Now, if you look at the image above, the rectangle has the same size as your imageView. If you somehow manage to remove the portion of shapes which are not intersected, you will have your desired effect.
And this will be the portion of the image that you will be masking,
This can be achieved quite easily using CAShapeLayer.
Here is a simple implementation that you can use,
let image = UIImage(named: "image.jpg")
let imageSize = image!.size
let imageView = UIImageView(frame: CGRect(origin: CGPoint.zero, size: imageSize))
imageView.clipsToBounds = true
imageView.image = image
let curveRadius: CGFloat = imageSize.width * 0.005
let invertedRadius: CGFloat = 1.0 / curveRadius
// draw ellipse in rect with big width, but same height
let ellipticalPath = UIBezierPath(ovalInRect: CGRectMake(0, 0, imageSize.width + 2 * invertedRadius * imageSize.width, imageSize.height))
// transform it to center of imageView
ellipticalPath.applyTransform(CGAffineTransformMakeTranslation(-imageSize.width * invertedRadius, 0))
// create rectangle path exactly similar to imageView
let rectanglePath = UIBezierPath(rect: imageView.bounds)
// translate it by 0.5 ratio in order to create intersection between circle and rectangle
rectanglePath.applyTransform(CGAffineTransformMakeTranslation(0, -imageSize.height * CGFloat(0.5)))
// append rectangle to elliptical path
ellipticalPath.appendPath(rectanglePath)
// create mask
let maskLayer = CAShapeLayer()
maskLayer.frame = imageView.bounds
maskLayer.path = ellipticalPath.CGPath
imageView.layer.mask = maskLayer
And here is how it looks,
You can adjust the value of curveRadius to suit your need.
Note: That the shape layer intersection is possible due to something called fillRule property on CAShapeLayer, which has a default value of kCAFillRuleNonZero. Read more about it here, https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAShapeLayer_class/#//apple_ref/occ/instp/CAShapeLayer/fillRule

Setting the view's frame as old but its position changed

I built a demo app to check the relationship between layer's anchorpoint, position and frame.
The initial look of the view looks like following:
In the code, I change that red view's anchor point, it will looks like this, which I could understand since change of anchor point will affect that view's frame.
to maintain the view's frame as original one, I used the following code:
We could see from the console's printout the frame has already remained the same.
However the view's final look looks like following, which still changes its position, how could this happen?
All the code looks like this:
Code are as following:
// Reserve original frame
let oldFrame = self.controlledView.frame
// Here I changed the anchorPoint which will cause that view's frame change
self.controlledView.layer.anchorPoint = CGPoint(x: 0, y: 0)
// to avoid the change of frame, set it back
self.controlledView.frame = oldFrame
// From the console's log the frame doesn't change, the red view's final
// location should be the same with the first image. However it is aligned to the right,
// which I could not understand.
From the CALayer Class Reference on Apple Documentation
You specify the value for this property using the unit coordinate space. The default value of this property is (0.5, 0.5), which represents the center of the layer’s bounds rectangle. All geometric manipulations to the view occur about the specified point. For example, applying a rotation transform to a layer with the default anchor point causes the layer to rotate around its center. Changing the anchor point to a different location would cause the layer to rotate around that new point.
From the UIView Class Reference on Apple Documentation
This rectangle defines the size and position of the view in its superview’s coordinate system. You use this rectangle during layout operations to size and position the view. Setting this property changes the point specified by the center property and the size in the bounds rectangle accordingly. The coordinates of the frame rectangle are always specified in points
So from my point of view, once the view is inside another view, when you change the frame you are changing it's center and size relative to his superview and not with itself.
To test my theory, i perform a small example
import UIKit
class ViewController: UIViewController {
var insideView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Setup Inside view
self.insideView = UIView()
var frame: CGRect = CGRectZero
frame.size.height = 40.0
frame.size.width = 40.0
frame.origin.x = self.view.frame.size.width / 2 - 20.0
frame.origin.y = self.view.frame.size.height / 2 - 20.0
self.insideView.frame = frame
self.insideView.backgroundColor = UIColor.redColor()
self.view.addSubview(self.insideView)
NSLog("One layer -> InsideView size: %#", NSStringFromCGRect(self.insideView.frame))
// Output: 2015-05-22 20:13:11.342 test[42680:12030822] One layer -> InsideView size: {{140, 264}, {40, 40}}
// Setup Another layer
var insideLayer: CALayer = CALayer()
insideLayer.backgroundColor = UIColor.blueColor().CGColor
var insideLayerFrame: CGRect = self.insideView.layer.frame;
insideLayerFrame.origin.x = 0.0
insideLayerFrame.origin.y = 0.0
insideLayer.frame = insideLayerFrame
insideLayer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
self.insideView.layer.addSublayer(insideLayer)
NSLog("Two layers -> InsideView size: %#", NSStringFromCGRect(self.insideView.frame))
// Output: 2015-05-22 20:13:11.342 test[42680:12030822] Two layers -> InsideView size: {{140, 264}, {40, 40}}
}
}
So i leave the layer of the view in it's position and add a new that i can manipulate.
And the result is:
Hope this can help :)

Resources