Add Gradient Layer to UIImageView in IOS thru CAGradientLayer - ios

I am trying to add a Gradient in IOS like I did in Android. So I can see my label on top of the UIImageView and its not hidden.
In android I did this in a drawable `
<gradient
android:angle="90"
android:endColor="#00ffffff"
android:startColor="#aa000000"
android:centerColor="#00ffffff" />
<corners android:radius="0dp" />
`
I am trying to do this in IOS Swift 4.2 and I get the following :
let gradient: CAGradientLayer = CAGradientLayer()
gradient.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
gradient.locations = [0.0 , 1.0]
gradient.startPoint = CGPoint(x: 0.0, y: 1.0)
gradient.endPoint = CGPoint(x: 0.5, y: 0.5)
gradient.frame = CGRect(x: 0.0, y: 0.0, width: showImageView.frame.size.width, height: showImageView.frame.size.height)
showImageView.layer.addSublayer(gradient)
How do I get the Gradient to start black from the bottom and go up in 90 degrees?How do I change the opacity?Any idea?

A few thoughts:
Choose colors with alpha less than 1. Perhaps:
gradient.colors = [
UIColor.black.withAlphaComponent(0.8).cgColor,
UIColor.black.withAlphaComponent(0).cgColor
]
To have this gradient go vertically, choose start and end points that have the same x value. E.g. to cover bottom half, perhaps:
gradient.startPoint = CGPoint(x: 0.5, y: 1)
gradient.endPoint = CGPoint(x: 0.5, y: 0.5)
Be very careful about using frame. You want the layer to reference the bounds of the image view (using the image view’s coordinate system), not the frame (which specifies where the image view is in its superview’s coordinate system). If your image view happens to be at 0, 0, you might not see a difference, but if you move the image view around at all, these will start to deviate. So, assuming you’re adding this gradient to the image view, itself, you’d use the image view’s bounds:
gradient.frame = showImageView.bounds
Be aware that the frame/bounds of the image view can sometimes change. So, we will implement layoutSubviews in our UIImageView or UITableViewCell subclass, and update the gradient’s frame there, e.g.
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = bounds
}
That way it will update the gradient’s frame when the view’s layout changes.
The other solution is to define a UIView subclass, say GradientView, that renders the CAGradientLayer and define the base layerClass of that view to be a CAGradientLayer. Then add this view in between the image view and the label, define its constraints, and then you’ll have a gradient that changes dynamically as the GradientView size changes. (This layerClass approach has the advantage that it yields better animation/transitions than you’d get by just updating the frame programmatically.)
Thus, something like:
class GradientView: UIView {
override class var layerClass: AnyClass { return CAGradientLayer.self }
var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
var firstColor: UIColor = UIColor.black.withAlphaComponent(0.8) {
didSet { updateColors() }
}
var secondColor: UIColor = UIColor.black.withAlphaComponent(0) {
didSet { updateColors() }
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
}
private extension GradientView {
func configure() {
updateColors()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 1)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.5)
}
func updateColors() {
gradientLayer.colors = [firstColor, secondColor].map { $0.cgColor }
}
}
If you really want your white text to pop, in addition to adding gradient over the image, you might also add a black glow/shadow to the text. It’s subtle, but really makes the text pop. Just make its shadow color the same color as the gradient.
So you can see the effect, here are four renditions of a cell, with (a) no gradient; (b) with gradient; (c) with gradient and nice gaussian blur around text; and (d) with simple shadow around text:
The nice, gaussian blur around the text is rendered with:
customLabel.layer.shadowColor = UIColor.black.cgColor
customLabel.layer.shadowRadius = 3
customLabel.layer.shadowOpacity = 1
customLabel.layer.masksToBounds = false
customLabel.layer.shouldRasterize = true
I think that third rendition (with gradient over the image, with glow around the text) is best. It’s subtle, but the text really pops. But gaussian blurs are computationally expensive, so if you find this adversely affects your performance too much, you can use the fourth option, with the simple, non-blurred shadow:
customLabel.shadowColor = .black
// perhaps also
// customLabel.shadowOffset = CGSize(width: -1, height: -1)

Related

Swift ios cut out rounded rect from view allowing colour changes

I'm using this approach to cut out a rounded rect "window" from a background view:
override func draw(_ rect: CGRect) {
guard let rectsArray = rectsArray else {
return
}
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
if let context = UIGraphicsGetCurrentContext() {
let roundedWindow = UIBezierPath(roundedRect: holeRect, cornerRadius: 15.0)
if holeRectIntersection.intersects(rect) {
context.addPath(roundedWindow.cgPath)
context.clip()
context.clear(holeRectIntersection)
context.setFillColor(UIColor.clear.cgColor)
context.fill(holeRectIntersection)
}
}
}
}
In layoutSubviews() I update the background colour add my "window frame" rect:
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = self.baseMoodColour
isOpaque = false
self.rectsArray?.removeAll()
self.rectsArray = [dragAreaView.frame]
}
I'm adding the rect here because layoutSubviews() updates the size of the "window frame" (i.e., the rect changes after layoutSubviews() runs).
The basic mechanism works as expected, however, if I change the background colour, the cutout window fills with black. So I'm wondering how I can animate a background colour change with this kind of setup? That is, I want to animate the colour of the area outside the cutout window (the window remains clear).
I've tried updating backgroundColor directly, and also using didSet in the accessor of a custom colour variable in my UIView subclass, but both cause the same filling-in of the "window".
var baseMoodColour: UIColor {
didSet {
self.backgroundColor = baseMoodColour
self.setNeedsDisplay()
}
}
Try to use UIView.animate, you can check it here
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveEaseOut], animations: {
self.backgroundColor = someNewColour
//Generally
//myView.backgroundColor = someNewColor
}, nil)
The problem in the short run is that that is simply what clear does if the background color is opaque. Just give your background color some transparency — even a tiny bit of transparency, so tiny that the human eye cannot perceive it — and now clear will cut a hole in the view.
For example, your code works fine if you set the view's background color to UIColor.green.withAlphaComponent(0.99).
By the way, you should delete the lines about UIColor.clear; that's a red herring. You should also cut the lines about the backgroundColor; you should not be repainting the background color into your context. They are two different things.
The problem in the long run is that what you're doing is not how to punch a hole in a view. You should be using a mask instead. That's the only way you're going to get the animation while maintaining the hole.
Answering my own question, based on #matt's suggestion (and linked example), I did it with a CAShapeLayer. There was an extra "hitch" in my requirements, since I have a couple of views on top of the one I needed to mask out. So, I did the masking like this:
func cutOutWindow() {
// maskedBackgroundView is an additional view, inserted ONLY for the mask
let r = self.maskedBackgroundView.bounds
// Adjust frame for dragAreaView's border
var dragSize = self.dragAreaView.frame.size
var dragPosition = self.dragAreaView.frame.origin
dragSize.width -= 6.0
dragSize.height -= 6.0
dragPosition.x += 3.0
dragPosition.y += 3.0
let r2 = CGRect(x: dragPosition.x, y: dragPosition.y, width: dragSize.width, height: dragSize.height)
let roundedWindow = UIBezierPath(roundedRect: r2, cornerRadius: 15.0)
let mask = CAShapeLayer()
let path = CGMutablePath()
path.addPath(roundedWindow.cgPath)
path.addRect(r)
mask.path = path
mask.fillRule = kCAFillRuleEvenOdd
self.maskedBackgroundView.layer.mask = mask
}
Then I had to apply the colour change to maskedBackgroundView.layer.backgroundColor (i.e., to the layer, not the view). With that in place, I get the cutout I need, with animatable colour changes. Thanks #matt for pointing me in the right direction.

How to make a CAGradientLayer follow a CGPath

Given a CAShapeLayer defining a path as in the picture below, I want to add a CAGradientLayer that follows the path of the shape layer.
For example, given a gradient from black->red:
the top right rounded piece would be black,
and if the slider was at 100, the top left would be red,
and if the slider was at 50, then half the slider would be black (as below), and the visible gradient would go from black (top right) to a redish-black at the bottom
Every previous post I've found does not actually answer this question.
For example, because I can only add axial CAGradientLayers, I can kind-of do this (pic below), but you can see it's not correct (the top left ends up becoming black again). How do I make the gradient actually follow the path/mask
For the simple circular shape path, the new conic gradient is great. I was able to get this immediately (this is a conic gradient layer masked by a circle shape layer):
I used red and green so as to show the gradient clearly. This doesn't seem to be identically what you're after, but I can't believe it will be very difficult to achieve your goals now that .conic exists.
class MyGradientView : UIView {
override class var layerClass : AnyClass { return CAGradientLayer.self }
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
let lay = self.layer as! CAGradientLayer
lay.type = .conic
lay.startPoint = CGPoint(x:0.5,y:0.5)
lay.endPoint = CGPoint(x:0.5,y:0)
lay.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
}
override func layoutSubviews() {
super.layoutSubviews()
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
let b = UIBezierPath(ovalIn: shape.frame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)))
shape.path = b.cgPath
shape.lineWidth = 10
shape.lineCap = .round
shape.strokeStart = 0.1
shape.strokeEnd = 0.9
shape.anchorPoint = CGPoint(x: 0.5, y: 0.5)
shape.transform = CATransform3DMakeRotation(-.pi/2, 0, 0, 1)
self.layer.mask = shape
}
}
Anybody on iOS 12 should go with Matt's suggestion or maybe use SFProgressCircle as DonMag suggested.
My solution:
I personally ended up adding two CAShapeLayers, each with a corresponding CAGradientLayer.
By programmatically splitting the slider in half as in the pic below, I was able to apply a top-to-bottom gradient on each side, which gives the effect I'm looking for. It's invisible to the user.

CAGradientLayer not filling width

I am relatively new to Swift. I'm just messing around trying to design a taxi app.
I discovered CGGradientLayer and thought it would add a nice effect to the top bar on the app.
I've added it and for some reason, it's not fully stretching to the views width.
This is my subclass:
import UIKit
class GradientView: UIView {
let gradient = CAGradientLayer()
override func awakeFromNib() {
setupGradientView()
}
func setupGradientView() {
gradient.frame = self.frame
gradient.colors = [UIColor.white.cgColor, UIColor.init(white: 1.0, alpha: 0.0).cgColor]
gradient.startPoint = CGPoint.zero
gradient.endPoint = CGPoint(x: 0, y: 1)
gradient.locations = [0.9, 1.0]
self.layer.addSublayer(gradient)
}
}
And this is the result.
The green background is actually the width of the UIView which the sub class is applied too.
You are setting the frame of the gradient too early. You should set it up in an override of layoutSubviews:
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = self.bounds
}
Notes:
You should be setting the gradient frame to self.bounds. That is the coordinate system used inside of the view. The gradient is placed relative to the GradientView and doesn't change if the GradientView is placed in another location on the screen (because the bounds do not change in that case, but the frame does).
At the time your view is first created, Auto Layout has not established the size of your view for your current device (or orientation). By setting the frame in layoutSubviews you always get the latest value for the size of the view.

Adding CAShapeLayer to UIButton but it appears under the border line

I want to add a red dot to the border of a UIButton. My current code for adding a dot is like so:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
override func setUp() {
super.setUp()
layer.borderWidth = borderWidth
layer.borderColor = normalBorderColor.cgColor
let redDotLayer = CAShapeLayer()
redDotLayer.path = CGPath(ellipseIn: CGRect(x: 30, y: -3.5, width: 8, height: 8), transform: nil)
redDotLayer.fillColor = UIColor.red.cgColor
layer.addSublayer(redDotLayer)
}
However when I add the red dot it appears under the border line. I need it to be on top of the border line.
What am I doing wrong here?
Nothing wrong with your approach.
For example just consider adding subview to parent UIView, subview always stays inside the parent it can't be added above the parent.
Same scenario applies to CALayer . You can't addSublayer above parent layer.
As an exception,
Unlike views, a superlayer does not automatically clip the contents of sublayers that lie outside its bounds rectangle. Instead, the superlayer allows its sublayers to be displayed in their entirety by default. However, you can reenable clipping by setting the masksToBounds property of the layer to YES.
as per apple documentation sublayer can go beyond parent visible region, but not above the parent.
Even below methods won't help us.
- insertSublayer:atIndex:
- insertSublayer:above:
- insertSublayer:below:
addSublayer:
Appends the layer to the layer’s list of sublayers.
Solution
Draw another CAShapeLayer around the button.
CALayers may just draw the border around itself last. I think the easiest solution for you is just be to draw the border in a separate layer so that you can control the ordering.
override func setUp() {
super.setUp()
let borderLayer = CALayer()
borderLayer.borderWidth = borderWidth
borderLayer.borderColor = normalBorderColor.cgColor
layer.addSublayer(borderLayer)
let redDotLayer = CAShapeLayer()
redDotLayer.path = CGPath(ellipseIn: CGRect(x: 30, y: -3.5, width: 8, height: 8), transform: nil)
redDotLayer.fillColor = UIColor.red.cgColor
layer.addSublayer(redDotLayer)
}
In my code, I was unable to use another layer as a border so I've used another UIView as a border.
An important part is to set the border UIView isUserInteractionEnabled to false so the responder chain would pass it to the UIButton:
let borderView = UIView(frame: .zero)
borderView.backgroundColor = .clear
borderView.layer.borderWidth = 0.5
borderView.layer.borderColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.18).cgColor
borderView.layer.cornerRadius = 19
// Dont't forget to set it to false or else the button won't work
borderView.isUserInteractionEnabled = false

Masking CAGradientLayer over CALayers

In my scene I have 2 views: first holds CALayer instances (bars), another hold CAGradientLayer and placed over first one. Picture below describes current state.
But I need this gradient to be applied only to bars (CALayer) of the first view.
I haven't found any relevant information to my problem. Any help appreciated.
You have to apply a mask to the gradient. There are various ways you could approach this problem.
You could create a CAShapeLayer, set the shape layer's path to the shape of the bars, and set the gradient layer's mask to that shape layer.
Or you could get rid of the bar layer and instead use two gradient layers, one for the orange bars and the other for the gray bars. Put both gradient layers in a subview, side-by-side, and set the superview's layer mask to the shape layer. Here's how to do that.
You'll need two gradient layers and a shape layer:
#IBDesignable
class BarGraphView : UIView {
private let orangeGradientLayer = CAGradientLayer()
private let grayGradientLayer = CAGradientLayer()
private let maskLayer = CAShapeLayer()
You'll also need the bar width:
private let barWidth = CGFloat(9)
At initialization time, set up the gradients and add all the sublayers:
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
initGradientLayer(orangeGradientLayer, with: .orange)
initGradientLayer(grayGradientLayer, with: .gray)
maskLayer.strokeColor = nil
maskLayer.fillColor = UIColor.white.cgColor
layer.mask = maskLayer
}
private func initGradientLayer(_ gradientLayer: CAGradientLayer, with color: UIColor) {
gradientLayer.colors = [ color, color, color.withAlphaComponent(0.6), color ].map({ $0.cgColor })
gradientLayer.locations = [ 0.0, 0.5, 0.5, 1.0 ]
layer.addSublayer(gradientLayer)
}
At layout time, set the frames of the gradient layers and set the mask layer's path. This requires a little work because you don't want a bar to be half orange and half gray.
override func layoutSubviews() {
super.layoutSubviews()
let barCount = ceil(bounds.size.width / barWidth)
let orangeBarCount = floor(barCount / 2)
let grayBarCount = barCount - orangeBarCount
var grayFrame = bounds
grayFrame.size.width = grayBarCount * barWidth
grayFrame.origin.x = frame.maxX - grayFrame.size.width
grayGradientLayer.frame = grayFrame
var orangeFrame = bounds
orangeFrame.size.width -= grayFrame.size.width
orangeGradientLayer.frame = orangeFrame
maskLayer.frame = bounds
maskLayer.path = barPath()
}
private func barPath() -> CGPath {
var columnBounds = self.bounds
columnBounds.origin.x = columnBounds.maxX
columnBounds.size.width = barWidth
let path = CGMutablePath()
for datum in barData.reversed() {
columnBounds.origin.x -= barWidth
let barHeight = CGFloat(datum) * columnBounds.size.height
let barRect = columnBounds.insetBy(dx: 1, dy: (columnBounds.size.height - barHeight) / 2)
path.addRoundedRect(in: barRect, cornerWidth: 2, cornerHeight: 2)
}
return path
}
let barData: [Double] = {
let count = 100
return (0 ..< count).map({ 0.5 + (1 + sin(8.0 * .pi * Double($0) / Double(count))) / 4 })
}()
}
Result:
The BarGraphView is transparent wherever there are no bars. If you want it on a dark background, put a dark view behind it, or make it a subview of a dark view:

Resources