This question already has an answer here:
Custom view with CALayer wired effect when animating bounds change
(1 answer)
Closed 4 years ago.
I created an extension class for UIText to create a gradient layer. After animating the UIText size the gradient layer size gets smaller than the UIText itsef that is the parent element. Here's my code:
extension UITextField {
func gradientBackground(firstColor: UIColor, secondColor: UIColor){
let gradientLayer = CAGradientLayer()
gradientLayer.frame = super.bounds
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x:0.0, y:0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
layer.addSublayer(gradientLayer)
}
class gradientTeLabel: UITextField {
var once = true
override func layoutSubviews() {
super.layoutSubviews()
if once {
self.gradientBackground(firstColor: UIColor(red: 0.30, green: 0.55, blue: 1.00, alpha: 1), secondColor: UIColor(red: 0.00, green: 0.36, blue: 1.00, alpha: 1))
once = false
}
}
}
---------these were are in separate file--------
#IBAction func showMarkUp(_ sender: Any) {
if mark1.isHidden {
_ = UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut){
self.expandButton.transform = CGAffineTransform(rotationAngle: .pi)
self.asdf.layer.frame = self.asdf.bounds
}.startAnimation()
asdf.layer.frame = asdf.bounds
} else {
_ = UIViewPropertyAnimator(duration: 0.250, curve: .easeInOut){
self.mark1.isHidden = true
}.startAnimation()
}
}
---------these are in ViewController--------
asdf is the UITextField that contains the gradient layer. How can I update the gradient layer size to match the UIText size?
You have two main choices.
If possible, do not add "loose" sublayers like this. Instead, add a subview, with layout based on the superview (e.g. autoresizing or autolayout), so that when you perform view animation on the superview, the subview comes along for the ride.
If you can't do that, then when you do view animation on the view, you must also do layer animation on the layer (using Core Animation) to match it.
Related
I'm working on an iOS project that's creating all its UIs programmatically. Not storyboards.
I want to add a gradient to the background of a UIViewController's UIView.
I created the following extension method.
extension UIView {
func addGradient(colors: [UIColor]) {
let gradientLayer = CAGradientLayer()
gradientLayer.bounds = bounds
gradientLayer.colors = colors.map { $0.cgColor }
layer.insertSublayer(gradientLayer, at: 0)
}
}
And I added the gradient in the viewDidLoad of the ViewController like so.
let topColor = UIColor(red: 0.28, green: 0.521, blue: 0.696, alpha: 1)
let bottomColor = UIColor(red: 0.575, green: 0.615, blue: 0.692, alpha: 1)
view.addGradient(colors: [topColor, bottomColor])
But the gradient is not being applied to the full width and height of the screen.
I printed out the bounds of the view and it shows these values.
(0.0, 0.0, 375.0, 812.0)
So I'm not sure why the gradient is still not covering the full view.
In addGradient(colors:) method set the gradientLayer's frame instead of bounds, i.e.
func addGradient(colors: [UIColor]) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds //here.....
//rest of the code...
}
Also, as suggested move your code to viewDidLayoutSubviews() method,
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let topColor = UIColor(red: 0.28, green: 0.521, blue: 0.696, alpha: 1)
let bottomColor = UIColor(red: 0.575, green: 0.615, blue: 0.692, alpha: 1)
view.addGradient(colors: [topColor, bottomColor])
}
This is because, the gradient's frame must change everytime there are any changes in the view's layout. Example, in portrait and landscape orientations.
When we are adding view bounds to Gradient layer frame that time we have to update frame in viewDidLayoutSubviews. It works fine for me
Following worked for me
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.gradient.frame = containerView.bounds
}
containerView is the view where i am setting gradient layer.
My goal is to make radial gradient extension for UIView. Here is my code:
extension UIView {
func drawRadialGradient() {
let colors = Colors.gradientColors as CFArray
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
guard let gradientValue = gradient else{ return }
let endRadius: CGFloat? = max(frame.width, frame.height) / 2
guard let endRadiusValue = endRadius else{ return }
let bottomcenterCoordinates = CGPoint(x: frame.width / 2, y: frame.height)
let getCurrentContext = UIGraphicsGetCurrentContext()
guard let currentContext = getCurrentContext else{ return }
currentContext.drawRadialGradient(gradientValue, startCenter: bottomcenterCoordinates, startRadius: 0.0, endCenter: bottomcenterCoordinates, endRadius: endRadiusValue, options: CGGradientDrawingOptions.drawsAfterEndLocation)
let radialGradientLayer = CALayer(layer: currentContext)
radialGradientLayer.frame = bounds
radialGradientLayer.masksToBounds = true
self.layer.insertSublayer(radialGradientLayer, at: 1)
}
}
When I call this function in viewDidLoad() or viewWillAppear() the compiler contains no mistakes and no warnings, the function just does not work out. i call it as following:
override func viewDidLoad() {
super.viewDidLoad()
view.drawRadialGradient()
}
For example, I have created an extension function for drawing a Linear Gradient on the UIView and it works, I call it the same way as radial gradient function:
func drawLinearGradient() {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.frame
gradientLayer.colors = Colors.gradientColors
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.95)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.05)
self.layer.insertSublayer(gradientLayer, at: 0)
}
For colors I have created a structure:
struct Colors {
static let firstColor = colorPicker(red: 70, green: 183, blue: 0)
static let secondColor = colorPicker(red: 0, green: 170, blue: 116)
static let thirdColor = colorPicker(red: 20, green: 0, blue: 204)
static let gradientColors = [firstColor.cgColor, secondColor.cgColor, thirdColor.cgColor]
static func colorPicker(red: CGFloat, green: CGFloat, blue: CGFloat) -> UIColor {
let color = UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1.0)
return color
}
}
Please, give me a piece of advice on how to realize it as an extension.
One main thing I can see in your code, is that you try to do the drawing in viewDidLoad. Don't do that, on top of other problems, the frame size is not properly set yet at that moment. If you want the UIView to do the drawing, then derive a class from UIView, and do the drawing in the draw method of that class.
If you want the radialGradientLayer CALayer that you created to do the drawing (it currently is just empty), then derive a subclass from CALayer, and implement its drawInContext method.
What you will see in the first image, if you scroll down a little bit, is the initial screen of a fin app. Basically, every section you see on the screen is placed within a vertical Stack View. Then every Label and Cell Is placed in a horizontal Stack View and so on, so the app auto-resizes for any screen size. I was using Story Board to create the elements.
The last section has two blue UITextFields that have a gradient layer behind it. I created an extension UITextField Class in a separate file that has the gradient function constructor in it and then another Class that placed the gradient behind any UITextField that have that class attached like this:
extension UITextField {
func gradientBackground(firstColor: UIColor, secondColor: UIColor){
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x:0.0, y:0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
layer.addSublayer(gradientLayer)
}
}
class gradientToTextField: UITextField {
var once = true
override func layoutSubviews() {
super.layoutSubviews()
if once {
self.gradientBackground(firstColor: UIColor(red: 0.30, green: 0.55, blue: 1.00, alpha: 1), secondColor: UIColor(red: 0.00, green: 0.36, blue: 1.00, alpha: 1))
once = false
}
}
}
Now if you take a look at the second image on the right, the last section expands on tapping the Plus sign button. As a result, the third section(starting with Total) is hidden and a new stack view appears below within the same section. Everything resizes perfectly except the gradient layer. I have set a red background behind it which is the UITextField's Background, so the problem is highlighted. It appears that the Gradient Layer is shorter in height than the UITextField, which is its parent element.
Here's the full project on GitHub: https://github.com/silviuisidor/layerResizeProblem
How can I fix this?
tl:dr — Instead of gradient layer that is a sublayer of the text field, you use a gradient view with the text field as its subview.
More details
Instead of a gradient layer, use a gradient view. The view exists purely to host a gradient layer as its underlying layer; you arrange this by subclassing UIView and implementing layerClass to return CAGradientLayer.self.
class MyGradientView : UIView {
override class var layerClass : AnyClass { return CAGradientLayer.self }
private func config() {
let gradientLayer = self.layer as! CAGradientLayer
let firstColor = UIColor(red: 0.30, green: 0.55, blue: 1.00, alpha: 1)
let secondColor = UIColor(red: 0.00, green: 0.36, blue: 1.00, alpha: 1)
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x:0.0, y:0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
}
override init(frame: CGRect) {
super.init(frame:frame)
self.config()
}
required init?(coder: NSCoder) {
super.init(coder:coder)
self.config()
}
}
Now make the text field the subview of our gradient view. Pin it using autolayout to the center of the gradient view. The gradient view, with its text field subview, is what goes into the interface. When the animation takes place, it is the gradient view that is resized! And when that happens, the text field is automatically repositioned along with it.
Resizing the layer frame in layoutSubviews should solve your problem
extension UITextField {
func gradientBackground(firstColor: UIColor, secondColor: UIColor) -> CAGradientLayer{
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x:0.0, y:0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
layer.addSublayer(gradientLayer)
return gradientLayer
}
}
class gradientToTextField: UITextField {
var coloredLayer : CAGradientLayer! = nil
var once = true
override func layoutSubviews() {
super.layoutSubviews()
if once {
coloredLayer = self.gradientBackground(firstColor: UIColor(red: 0.30, green: 0.55, blue: 1.00, alpha: 1), secondColor: UIColor(red: 0.00, green: 0.36, blue: 1.00, alpha: 1))
once = false
}
coloredLayer.frame = self.bounds
}
}
I want to put a gradient background in all my views.
The way that I thought to do this without the need to create a IBOutlet of a view in all view controllers is to create a new class from UIView, and inside the draw() I put the code that makes a CAGradientLayer in the view.
So, in the interface builder I just need to select the background view and set its class as my custom class. It works so far.
My question is if I can do that without problems. Somebody knows is it ok?
Because the model file that inherit from UIView come with the comment:
//Only override draw() if you perform custom drawing.
And I don't know if create a layer counts. Or if draw() is only to low level drawing or something like that. Do not know nothing about the best usage of the draw() func.
This is the code:
override func draw(_ rect: CGRect) {
let layer = CAGradientLayer()
layer.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
let color1 = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
let color2 = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
layer.colors = [color1.cgColor, color2.cgColor]
self.layer.addSublayer(layer)
}
You can use #IBDesignable and #IBInspectable to configure the startColor and endColor properties from the Storyboard. Use CGGradient which specifies colors and locations instead of CAGradientLayer if you want to draw in draw(_ rect:) method. The Simple Gradient class and draw(_ rect: CGRect) function would look like this
import UIKit
#IBDesignable
class GradientView: UIView {
#IBInspectable var startColor: UIColor = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
#IBInspectable var endColor: UIColor = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
let colors = [startColor.cgColor, endColor.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace,
colors: colors as CFArray,
locations: colorLocations)!
let startPoint = CGPoint.zero
let endPoint = CGPoint(x: 0, y: bounds.height)
context.drawLinearGradient(gradient,
start: startPoint,
end: endPoint,
options: [CGGradientDrawingOptions(rawValue: 0)])
}
}
you can read more about it here and tutorial
You shouldn't be adding a CAGradientLayer inside the draw function.
If you want to add a gradient to a view then the easiest way is to do it with a layer (like you are doing) but not like this...
You should add it to your view as part of the init method and then update its frame in the layout subviews method.
Something like this...
class MyView: UIView {
let gradientLayer: CAGradientLayer = {
let l = CAGradientLayer()
let color1 = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
let color2 = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
l.colors = [color1.cgColor, color2.cgColor]
return l
}()
override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(gradientLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
I would create UIView extension and apply it when needed.
extension UIView {
func withGradienBackground(color1: UIColor, color2: UIColor, color3: UIColor, color4: UIColor) {
let layerGradient = CAGradientLayer()
layerGradient.colors = [color1.cgColor, color2.cgColor, color3.cgColor, color4.cgColor]
layerGradient.frame = bounds
layerGradient.startPoint = CGPoint(x: 1.5, y: 1.5)
layerGradient.endPoint = CGPoint(x: 0.0, y: 0.0)
layerGradient.locations = [0.0, 0.3, 0.4, 1.0]
layer.insertSublayer(layerGradient, at: 0)
}
}
Here you can change func declaration to specify startPoint, endPointand locations params as variables.
After that just call this method like
gradienView.withGradienBackground(color1: UIColor.green, color2: UIColor.red, color3: UIColor.blue, color4: UIColor.white)
P.S.
Please note that you can have as much colors as you want in colors array
I have created a progress bar to be used in a tableView by creating a gradient layer. It works perfectly.
iPhone5:
In order to use the app on multiple devices, I have created the UIView in Storyboard, tagged it and added constraints.
However, when I use the app on an iPhone 6 the CALayer don't resize.
iPhone6:
I find this extremely stupid, but never mind. I have looked around and tried to understand how to solve this for months, but I have come up short. Does ANYONE know how to make CALayers resize with the UIView? Any help would be very much appreciated ! Thank you.
progressBar = cell.contentView.viewWithTag(3) as UIView!
progressBar.layer.cornerRadius = 4
progressBar.layer.masksToBounds = true
// create gradient layer
let gradient : CAGradientLayer = CAGradientLayer()
// create color array
let arrayColors: [AnyObject] = [
UIColor (red: 255/255, green: 138/255, blue: 1/255, alpha: 1).CGColor,
UIColor (red: 110/255, green: 110/255, blue: 118/255, alpha: 1).CGColor]
// set gradient frame bounds to match progressBar bounds
gradient.frame = progressBar.bounds
// set gradient's color array
gradient.colors = arrayColors
//Set progress(progressBar)
var percentageCompleted = 0.6
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.locations = [percentageCompleted, percentageCompleted]
gradient.endPoint = CGPoint(x: 1, y: 0.5)
// replace base layer with gradient layer
progressBar.layer.insertSublayer(gradient, atIndex: 0)
The default layer of a UIView does resize with its view, but sublayers don't (as you found out). One way to make this work is to create a custom view class, move the code you have in your question to it, and override layoutSublayersOfLayer where you can set the gradient layer to be the same size as the view. Because this code is now in a custom class, I also created a property percentageCompleted (instead of a local variable), and added a willSet clause so the bar's appearance is updated any time you change the percentageCompleted property.
class RDProgressView: UIView {
private let gradient : CAGradientLayer = CAGradientLayer()
var percentageCompleted: Double = 0.0 {
willSet{
gradient.locations = [newValue, newValue]
}
}
override func awakeFromNib() {
self.layer.cornerRadius = 4
self.layer.masksToBounds = true
// create color array
let arrayColors: [AnyObject] = [
UIColor (red: 255/255, green: 138/255, blue: 1/255, alpha: 1).CGColor,
UIColor (red: 110/255, green: 110/255, blue: 118/255, alpha: 1).CGColor]
// set gradient's color array
gradient.colors = arrayColors
//Set progress(progressBar)
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.locations = [percentageCompleted, percentageCompleted]
gradient.endPoint = CGPoint(x: 1, y: 0.5)
self.layer.insertSublayer(gradient, atIndex: 0)
}
override func layoutSublayersOfLayer(layer: CALayer!) {
super.layoutSublayersOfLayer(layer)
gradient.frame = self.bounds
}
}
In IB, you would change the class of your view to RDProgressView (in my example), and in cellForRowAtIndexPath, you would only need to get a reference to the view, and set its percentageCompleted property.
progressBar = cell.contentView.viewWithTag(3) as RDProgressView!
progressBar.percentageCompleted = 0.2
As an alternative to the accepted answer, you could also change the views layer class to be CAGradientLayer. The views layer will always be resized according to layout changes. You can achieve that by subclassing UIView
class GradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
}
then set the colors
if let gradientLayer = gradientView.layer as? CAGradientLayer {
gradientLayer.colors = arrayColors
}
It's less code than adding and maintaining a sublayer, but might not suit all use cases.