Gradient mask on UIView - ios

I have a UIView and want to have the bottom part of it fade out to 0 opacity.
May this be done to a UIView (CALayer) in order to affect an entire UIView and its content?

Yes, you can do that by setting CAGradientLayer as your view's layer mask:
#import <QuartzCore/QuartzCore.h>
...
CAGradientLayer *maskLayer = [CAGradientLayer layer];
maskLayer.frame = view.bounds;
maskLayer.colors = #[(id)[UIColor whiteColor].CGColor, (id)[UIColor clearColor].CGColor];
maskLayer.startPoint = CGPointMake(0.5, 0);
maskLayer.endPoint = CGPointMake(0.5, 1.0);
view.layer.mask = maskLayer;
P.S. You also need to link QuartzCore.framework to your app to make things work.

Using an Image as the mask to prevent Layout issues.
Although Vladimir's answer is correct, the issue is to sync the gradient layer after view changes. For example when you rotate the phone or resizing the view. So instead of a layer, you can use an image for that:
#IBDesignable
class MaskableLabel: UILabel {
var maskImageView = UIImageView()
#IBInspectable
var maskImage: UIImage? {
didSet {
maskImageView.image = maskImage
updateView()
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateView()
}
func updateView() {
if maskImageView.image != nil {
maskImageView.frame = bounds
mask = maskImageView
}
}
}
Then with a simple gradient mask like this, You can see it even right in the storyboard.
Note:
You can use this class and replace UILabel with any other view you like to subclass.
🎁 Bones:
You can use any other shaped image as the mask just like that.

Related

How to hide the CALayer's borders without causing rendering issue in iOS

I am having three UIView's one after the other horizontally, with each UiView.Layer.BorderColor set to Black and UiView.Layer.BorderWidth set to 0.25f like shown below. So this gives a border look for each of the view.
Now i have a requirement to only display the horizontal borders of the UiViews. Hence i create a mask layer and set path and frame to that mask layer to apply clip. Please refer the code snippet below to give you an idea of what am doing.
foreach (UIView UiView in myViews)
{
UiView.Layer.BorderColor = Color.Black.ToCGColor();
UiView.Layer.BorderWidth = 0.25f;
UIBezierPath maskPath = null;
CAShapeLayer maskLayer = new CAShapeLayer();
maskLayer.Frame = UiView.Bounds;
// Applying Clip to my layer
maskPath = UIBezierPath.FromRect(new CGRect(UiView.Bounds.Left + 0.5, UiView.Bounds.Top, UiView.Bounds.Width - 1, UiView.Bounds.Height));
maskLayer.Path = maskPath.CGPath;
view.Layer.Mask = maskLayer;
}
Yes i am aware that whatever the bounds i set for the maskPath is the Visible region, so given that each UiView is 60 in width , my visible region according to what i have written in code is , from 0.5 pixel upto 59.5 pixel of each UIView . Thus eliminating the borders present in the pixels from 0 to 0.5 and from 59.5 to 60. Hope this part is clear for you. Please refer the below image for a visual representation of my mask area. The Yellow borders denotes the mask bounds.
Now all is fine , but this 0.5 pixels border which is hidden causes a white space in my top and bottom borders of the UiView's put together continuously. Please refer the below image with the circled spots highlighting the void spaces in the top and bottom.
This can be easily reproduced in code too. Please suggest me on how to overcome this problem. What am i doing wrong. I know this is a very small blank space of half a pixel which is barely visible to the naked eye most of the times, but it will not be visually pleasing for my grid view application.
PS : Am developing in Xamarin.iOS , however native iOS solutions will also be helpful.
Thanks in advance
Try this for Swift.
class ViewController: UIViewController {
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
#IBOutlet weak var view3: UIView!
override func viewDidLoad() {
super.viewDidLoad()
var borderwidth = CGFloat()
borderwidth = 5
let topBorder = CALayer()
topBorder.backgroundColor = UIColor.black.cgColor
topBorder.frame = CGRect(x: self.view1.frame.origin.x, y: self.view1.frame.origin.y, width: self.view3.frame.origin.x + self.view3.frame.size.width - self.view1.frame.origin.x, height: borderwidth)
self.view.layer.addSublayer(topBorder)
let bottomBorder = CALayer()
bottomBorder.backgroundColor = UIColor.black.cgColor
bottomBorder.frame = CGRect(x: self.view1.frame.origin.x, y: self.view1.frame.origin.y + self.view1.frame.size.height - borderwidth, width: self.view3.frame.origin.x + self.view3.frame.size.width - self.view1.frame.origin.x, height: borderwidth)
self.view.layer.addSublayer(bottomBorder)
}
}
See this screenshot : 1
Masking is the problem. Try like this.
view.Layer.MasksToBounds = false;
view.Layer.AllowsEdgeAntialiasing = true;
layer.BorderColor = color.ToCGColor();
layer.BorderWidth = 0.5f;
if (layer.SuperLayer == null && layer.SuperLayer != view.Layer)
view.Layer.AddSublayer(layer);
view.Layer.BorderColor = Color.Transparent.ToCGColor();
layer.Frame = new CGRect(view.Bounds.Left, view.Bounds.Bottom - 0.5f, view.Bounds.Right, view.Bounds.Bottom);

How to draw shapes like circles, oval, ractangles dynamically in iOS?

I have a UICollectionView of showing shapes like circles, oval, ractangles. So when I clicked on Cell I need to draw same shape on UIView and after I need to move and resize that view which was drawn by clicking in Cell. How it can achieve in iOS ? Thanks in advance.
If you use a UICollectionView, your reusable cell will have the "layer" to draw on.
What to do?
1. Create a UIView subclass and place it inside your reusable cell.
2. Override drawRect(_:) method, you'll do all the drawing inside.
3. Add your shape/line drawing code to drawRect method.
For example use UIBezierPath class for lines, arcs etc. You'll be able to create all sorts of shapes.
You should read the CoreGraphics API Ref:
https://developer.apple.com/reference/coregraphics
also learn about Layers, Contexts and drawRect:
A very basic example of a circle:
class CircleView: UIView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.addEllipse(in: rect)
context.setFillColor(.red.cgColor)
context.fillPath()
}
}
and drawing lines (rects, stars etc.) with UIBezier paths:
override func drawRect(rect: CGRect) {
var path = UIBezierPath()
path.moveToPoint(CGPoint(x:<point x>, y:<point y>))
path.addLineToPoint(CGPoint(x:<next point x>, y:<next point y>))
point.closePath()
UIColor.redColor().set()
point.stroke()
point.fill()
}
study about bezier path. It is possible with this. hope it will work for you.
You can use CAShapeLayer. eg. Draw a circle.
shapeLayer = [CAShapeLayer layer];
shapeLayer.frame = aFrame;
UIBezierPath *spath = [UIBezierPath bezierPathWithArcCenter:centerPoint radius:12 startAngle:0 endAngle:(2 * M_PI) clockwise:YES];
shapeLayer.strokeColor = [UIColor colorWithRed:71/255.0 green:157/255.0 blue:252/255.0 alpha:1].CGColor;
shapeLayer.path = spath.CGPath;
shapeLayer.strokeStart = 0;
shapeLayer.strokeEnd = 0;
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 2;
[self.layer addSublayer:shapeLayer];
I hope it help you
You can use layer's properties:
cornerRadius
borderWidth
borderColor

Delete border of UIView when app changes size class

I have a blue view with a white dashed border. As you can see from the image below, once the app is rotated the view changes its dimensions and the border doesn't adjust to the view's new width and height.
I need to find a way to
Know when the view controller changes its size - perhaps using viewWillTransitionToSize.
Delete the previously drawn border, if any.
Add a new border to the view - in the view's drawRect method.
How can I delete the border previously drawn on the view?
#IBOutlet weak var myView: UIView!
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animateAlongsideTransition(nil, completion: {
_ in
self.myView.setNeedsDisplay()
})
}
class RenderView: UIView {
override func drawRect(rect: CGRect) {
self.addDashedBorder()
}
}
extension UIView {
func addDashedBorder() {
let color = UIColor.whiteColor().CGColor
let shapeLayer:CAShapeLayer = CAShapeLayer()
let frameSize = self.frame.size
let shapeRect = CGRect(x: 0, y: 0, width: frameSize.width, height: frameSize.height)
shapeLayer.bounds = shapeRect
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height/2)
shapeLayer.fillColor = UIColor.clearColor().CGColor
shapeLayer.strokeColor = color
shapeLayer.lineWidth = 6
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [6,3]
shapeLayer.path = UIBezierPath(roundedRect: shapeRect, cornerRadius: 5).CGPath
self.layer.addSublayer(shapeLayer)
}
}
Download sample project here.
This is not the right way to do things. Each time your RenderView is redrawn, its drawRect method will be called, and addDashedBorder will add a new layer to your view.
drawRect is for drawing inside of your view using CoreGraphics or UIKit, not CoreAnimation. Inside that method, you should just draw, nothing else. If you want to use it a layer, layoutSubviews is a better place to add it, and to update it to match the view.
Here are two ways to solve your problem. Both update the border correctly, and animate the border smoothly when the device is rotated.
Alternative 1: Just draw the border in drawRect, rather than using a separate shape layer. Also, set your view's contentMode so it automatically redraws when its size changes.
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.myView.contentMode = .Redraw
}
}
class RenderView: UIView {
override func drawRect(rect: CGRect) {
let path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 5)
path.lineWidth = 6
let pattern: [CGFloat] = [6.0, 3.0]
path.setLineDash(pattern, count: 2, phase: 0)
UIColor.whiteColor().setStroke()
path.stroke()
}
}
Alternative 2: Continue to use CAShapeLayer, but only create a single one, by using a lazy stored property. Update the layer in an override of layoutSubviews, and if necessary, animate it alongside any changes in the view's bounds.
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
}
class RenderView: UIView {
// Create the borderLayer, and add it to our view's layer, on demand, only once.
lazy var borderLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.whiteColor().CGColor
shapeLayer.lineWidth = 6
shapeLayer.lineDashPattern = [6,3]
self.layer.addSublayer(shapeLayer)
return shapeLayer
}()
override func layoutSubviews() {
super.layoutSubviews()
// We will update the borderLayer's path to match the view's current bounds.
let newPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 5).CGPath
// We may be animating from the old bounds to the new bounds.
// If so, we want the borderLayer to animate alongside that.
// (UIView does not do this automatically, since it does not know
// anything about our borderLayer; it's at the the CoreAnimation level,
// below UIKit.)
//
// We want an animation that uses the same properties as the existing
// animation, but applies to a different value: the borderLayer's path.
// We'll find the existing animation on the view's bounds.size,
// and if it exists, add our own animation based on it that will
// apply the path change.
if let viewBoundsAnimation = self.layer.animationForKey("bounds.size") {
let pathAnimation = CABasicAnimation()
pathAnimation.beginTime = viewBoundsAnimation.beginTime
pathAnimation.duration = viewBoundsAnimation.duration
pathAnimation.speed = viewBoundsAnimation.speed
pathAnimation.timeOffset = viewBoundsAnimation.timeOffset
pathAnimation.timingFunction = viewBoundsAnimation.timingFunction
pathAnimation.keyPath = "path"
pathAnimation.fromValue = borderLayer.path
pathAnimation.toValue = newPath
borderLayer.addAnimation(pathAnimation, forKey: "path")
}
// Finally, whether we are animating or not, make the border layer show the new path.
// If we are animating, this will appear when the animation is finished.
// If we are not animating, this will appear immediately.
self.borderLayer.path = newPath
}
}
Note that neither of these alternatives require overriding viewWillTransitionToSize or traitCollectionDidChange. Those are higher-level UIViewController concepts, that may get called during device rotation, but won't happen if some other code changes your view's size. It's better to use the simple UIView-level drawRect or layoutSubviews methods, because they will always work.
Call set needs display when the view transitions.You can detect it by using this function.It works perfectly to detect orientation change
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
self.myView.setNeedsDisplay()
}

Fading UILabel in from left to right

I want to animate a UILabel to fade text in from left to right. My code below fades the entire UILabel in at once, but I would like to go letter by letter or word by word. Also, words can be added to this text, and when that happens I only want to fade in the new words, not the entire UILabel again.
__weak ViewController *weakSelf = self;
weakSelf.label.alpha = 0;
[UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveEaseIn
animations:^{ weakSelf.label.alpha = 1;}
completion:nil];
I have done this once before with decent results. You will probably have to tweak it to your preference.
First step is to create a gradient image to match the way you want the text to fade in. If your text color is black you can try this.
- (UIImage *)labelTextGradientImage
{
CAGradientLayer *l = [CAGradientLayer layer];
l.frame = self.label.bounds;
l.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithRed:0 green:0 blue:0 alpha:0] CGColor], (id)[[UIColor colorWithRed:0 green:0 blue:0 alpha:1] CGColor], nil];
l.endPoint = CGPointMake(0.0, 0.0f);
l.startPoint = CGPointMake(1.0f, 0.0f);
UIGraphicsBeginImageContext(self.label.frame.size);
[l renderInContext:UIGraphicsGetCurrentContext()];
UIImage *textGradientImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return textGradientImage;
}
That code creates an black image that should fade from left to right.
Next, when you create you label, you need to set its text color based from a pattern made from the image we just generated.
self.label.alpha = 0;
self.label.textColor = [UIColor colorWithPatternImage:[self labelTextGradientImage]];
Assuming that works, the last step is to animate the label into view and change its text color while that happens.
[UIView transitionWithView:self.label duration:0.2 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
self.label.textColor = [UIColor blackColor];
self.label.alpha = 1;
} completion:^(BOOL finished) {
}];
The animation code is a little funky because the UILabel's textColor property doesn't seem to be animatable with a UIViewAnimation.
Well, I know how I would do it. I would make a mask containing a gradient, and animate the mask across the front of the label.
Here's the result when I do that (don't know whether it's exactly what you have in mind, but perhaps it's a start):
I managed to achieve this and created a function within a UIView extension:
#discardableResult public func addSlidingGradientLayer(withDuration duration: Double = 3.0, animationDelegate delegate: CAAnimationDelegate? = nil) -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor, UIColor.clear.cgColor]
gradientLayer.startPoint = CGPoint(x: -1.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
let gradientAnimation = CABasicAnimation(keyPath: "startPoint")
gradientAnimation.fromValue = gradientLayer.startPoint
gradientAnimation.toValue = gradientLayer.endPoint
gradientAnimation.duration = duration
gradientAnimation.delegate = delegate
gradientLayer.add(gradientAnimation, forKey: "startPoint")
layer.mask = gradientLayer
return gradientLayer
}
To begin with we create a CAGradientLayer instance that we use as a mask for a view (in this case a UILabel).
We then create a CABasicAnimation instance, which we use to animate the startPoint of the CAGradientLayer. This has the effect of sliding the startPoint from left to right with some supplied duration value.
Finally, we an also the delegate of the CABasicAnimation. Then, if we adhere to the CAAnimationDelegate protocol, we can execute some code after the animation completes.
This is how you might use this extension:
class ViewController: UIViewController {
#IBOutlet weak var someLabel: UILabel!
private func configureSomeLabel() {
someLabel.text = "Sliding into existence!"
someLabel.sizeToFit()
someLabel.addSlidingGradientLayer(withDuration: 3.0, animationDelegate: self)
}
override func viewDidLoad() {
super.viewDidLoad()
configureSomeLabel()
}
}
Where someLabel has been hooked up to your storyboard. And as we've assigned this view controller as the animation delegate we need to add:
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
someLabel.layer.mask = nil
}
}
where I have chosen to remove the masking layer.
Hope this helps someone!

How to make a UIView shadow stretch when device is rotated?

In a custom tableview cell, I'm drawing a simple rectangle with a shadow like this:
photoBorder = [[[UIView alloc] initWithFrame:CGRectMake(4, 4, self.frame.size.width-8, 190)] autorelease];
photoBorder.autoresizingMask = UIViewAutoresizingFlexibleWidth;
photoBorder.backgroundColor = [UIColor whiteColor];
photoBorder.layer.masksToBounds = NO;
photoBorder.layer.shadowOffset = CGSizeMake(0, 1);
photoBorder.layer.shadowRadius = 4;
photoBorder.layer.shadowOpacity = 1.0;
photoBorder.layer.shadowColor = [UIColor darkGrayColor].CGColor;
photoBorder.layer.shouldRasterize = YES;
photoBorder.layer.shadowPath = [UIBezierPath bezierPathWithRect:photoBorder.bounds].CGPath; // this line seems to be causing the problem
This works fine when the view first loads. However, when you rotate the device, the shadow stays the same size. I'd really like it to stretch to the new width of "photoBorder".
I can get it to work by removing the shadowPath, but the tableview takes a noticeable performance hit.
Anyone have any tips on making a shadow, on a UIView, that can stretch, without losing performance?
After searching for a few hours and not finding anything, I posted this. Then found an answer a few minutes later.
The simple solution for me appears to be simply moving the shadowPath into layoutSubviews.
- (void)layoutSubviews{
photoBorder.layer.shadowPath = [UIBezierPath bezierPathWithRect:photoBorder.bounds].CGPath;
}
You need to create a subclass of UIView so that you can get the new bounds in the layoutSubviews() method.
Note: If you try to add this code in a ViewController that owns the subview, the bounds will remain static as you rotate, which results in the wrong shadowPath.
import UIKit
class BackgroundView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
updateShadow(on: self)
}
func updateShadow(on background: UIView) {
let layer = background.layer
layer.shadowPath = UIBezierPath(rect: background.bounds).cgPath
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 2)
layer.shadowRadius = 4
layer.shadowOpacity = 0.22
}
}
Make sure you call super.layoutSubviews() to handle any Auto Layout constraints.
You can set a custom class in a Storyboard file.
for performance enhancement you can draw an inner shadow using Core Graphics.
Inner shadow effect on UIView layer

Resources