Gradient from UIView after orientation transition - ios

I have an extension for UIView to apply gradient:
extension UIView {
func applyGradient(colors: [CGColor]) {
self.backgroundColor = nil
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds // Here new gradientLayer should get actual UIView bounds
gradientLayer.cornerRadius = self.layer.cornerRadius
gradientLayer.colors = colors
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.masksToBounds = true
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
In my UIView subclass I'm creating all my view and setting up constraints:
private let btnSignIn: UIButton = {
let btnSignIn = UIButton()
btnSignIn.setTitle("Sing In", for: .normal)
btnSignIn.titleLabel?.font = UIFont(name: "Avenir Medium", size: 35)
btnSignIn.layer.cornerRadius = 30
btnSignIn.clipsToBounds = true
btnSignIn.translatesAutoresizingMaskIntoConstraints = false
return btnSignIn
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSubViews()
}
func addSubViews() {
self.addSubview(imageView)
self.addSubview(btnSignIn)
self.addSubview(signUpstackView)
self.addSubview(textFieldsStackView)
setConstraints()
}
I've overridden layoutSubviews function which is called each time when view bounds are changed(Orientation transition included), where I'm calling applyGradient.
override func layoutSubviews() {
super.layoutSubviews()
btnSignIn.applyGradient(colors: [Colors.ViewTopGradient, Colors.ViewBottomGradient])
}
The problem is that after orientation transition gradient applied wrong for some reason...
See the screenshot please
What am I missing here?

If you look at your button, you’ll see two gradients. That’s because layoutSubviews is called at least twice, first when the view was first presented and again after the orientation change. So you’ve added at least two gradient layers.
You want to change this so you only insertSublayer once (e.g. while the view is being instantiated), and because layoutSubviews can be called multiple times, it should limit itself to just adjusting existing layers, not adding any.
You can also just use the layerClass class property to make the button’s main layer a gradient, and then you don’t have to manually adjust layer frames at all:
#IBDesignable
public class RoundedGradientButton: UIButton {
static public override var layerClass: AnyClass { CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { layer as! CAGradientLayer }
#IBInspectable var startColor: UIColor = .blue { didSet { updateColors() } }
#IBInspectable var endColor: UIColor = .red { didSet { updateColors() } }
override public init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override public func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.height, bounds.width) / 2
}
}
private extension RoundedGradientButton {
func configure() {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
updateColors()
titleLabel?.font = UIFont(name: "Avenir Medium", size: 35)
}
func updateColors() {
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
}
}
This technique eliminates the need to adjust the layer’s frame manually and results in better mid-animation renditions, too.

Related

Applying gradient colour to storyboard view with auto layout

I have added a UIView in the storyboard and want to set its background with gradient colors.
This is my code
extension UIView {
func setGradientBackground(colors: [CGColor]) {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = colors
gradientLayer.locations = [0.0, 1.0]
gradientLayer.frame = self.bounds
self.layer.insertSublayer(gradientLayer, at:0)
}
}
and in the viewcontroller,
viewSocial.setGradientBackground(colors: [UIColor.gradientBottomColor.cgColor, UIColor.gradientTopColor.cgColor])
But this creates double gradient layers. see the image
I have tried adding a class for GradientLayer as mentioned here. But this is not allowing to set on a view from the storyboard. Gives the warning of the weak variable.
Here is one way to make a #IBDesignable gradient view. It's using the default top-to-bottom gradient direction:
#IBDesignable
class MyGradientView: UIView {
#IBInspectable var color1: UIColor = .red {
didSet { setNeedsDisplay() }
}
#IBInspectable var color2: UIColor = .yellow {
didSet { setNeedsDisplay() }
}
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [color1.cgColor, color2.cgColor]
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.colors = [color1.cgColor, color2.cgColor]
}
}
It has #IBInspectable vars for "color1" and "color2" ... changing them in the Attributes Inspector will be reflected in Storyboard.
No need for any additional code, such as your current approach with setGradientBackground()
Here is a code of #IBDesignable Gradient View. You can use Top to Bottom and Left to Right from Storyboard.
private var startGradientColorAssociatedKey : UIColor = .black
private var endGradientColorAssociatedKey : UIColor = .black
private var observationGradientView: NSKeyValueObservation?
extension UIView {
#IBInspectable var startGradientColor: UIColor {
get {
if let color = objc_getAssociatedObject(self, &startGradientColorAssociatedKey) as? UIColor {
return color
} else {
return .black
}
} set {
objc_setAssociatedObject(self, &startGradientColorAssociatedKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
#IBInspectable var endGradientColor: UIColor {
get {
if let color = objc_getAssociatedObject(self, &endGradientColorAssociatedKey) as? UIColor {
return color
} else {
return .black
}
} set {
objc_setAssociatedObject(self, &endGradientColorAssociatedKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
#IBInspectable
var isTopToBottomGradient: Bool {
get {
return self.isTopToBottomGradient
}
set {
DispatchQueue.main.async {
if newValue {
self.setGradientBackground(colorTop: self.startGradientColor, colorBottom: self.endGradientColor)
} else {
self.setGradientBackground(colorLeft: self.startGradientColor, colorRight: self.endGradientColor)
}
}
}
}
func setGradientBackground(colorLeft: UIColor, colorRight: UIColor) {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [colorLeft.cgColor, colorRight.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer.locations = [0, 1]
gradientLayer.frame = bounds
observationGradientView = self.observe(\.bounds, options: .new) { [weak gradientLayer] view, change in
if let value = change.newValue {
gradientLayer?.frame = value
}
}
layer.insertSublayer(gradientLayer, at: 0)
}
}
Use from Storyboard : If you want to set gradient from top to bottom then set ON isTopToBottomGradient key. Defalt value is OFF

Unable to remove a CALayer from UITextField

So, I have this custom UITextField and I have two methods to add CALayer and remove the CALayer but remove is not working.
#IBDesignable class AppTextField : UITextField {
private let bottomLine = CALayer()
override func layoutSubviews() {
self.font = .systemFont(ofSize: 20)
self.addBottomLine()
self.clearButtonMode = .unlessEditing
super.layoutSubviews()
}
func removeBttomLine() {
bottomLine.removeFromSuperlayer()
}
private func addBottomLine() {
bottomLine.frame = CGRect(origin: CGPoint(x: 0, y: self.frame.height + 4), size: CGSize(width: self.frame.width, height: 1))
bottomLine.backgroundColor = UIColor.init(hexString: "#DCCFCA")?.cgColor
self.borderStyle = .none
self.layer.addSublayer(bottomLine)
}
}
The only thing you should do in layoutSubviews() is update frames as necessary.
This will show the red line when the field is NOT being edited, and will remove it while the field IS being edited:
#IBDesignable class AppTextField : UITextField {
private let bottomLine = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.font = .systemFont(ofSize: 20)
self.backgroundColor = .white
self.clearButtonMode = .unlessEditing
self.borderStyle = .none
bottomLine.backgroundColor = UIColor.red.cgColor
addBottomLine()
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
r.origin.y = bounds.maxY
r.size.height = 4.0
bottomLine.frame = r
}
func removeBottomLine() {
bottomLine.removeFromSuperlayer()
}
private func addBottomLine() {
self.layer.addSublayer(bottomLine)
}
override func resignFirstResponder() -> Bool {
super.resignFirstResponder()
addBottomLine()
return true
}
override func becomeFirstResponder() -> Bool {
super.becomeFirstResponder()
removeBottomLine()
return true
}
}
The reason could because the layoutSubviews method gets called multiple times and the layer is getting added multiple times. Try moving the addBottomLine method to required init?(coder:) method if you're using storyboard or use init(frame:) or custom init whichever gets called just once. Here's an example:
#IBDesignable class AppTextField : UITextField {
required init?(coder: NSCoder) {
super.init(coder: coder)
addBottomLine()
}
}
You're doing 3 things in your add method:
adusting frame
setting color
adding as sublayer
And because you're calling it from layoutSubviews it's called multiple times and you're ending with multiple layers added and that's why calling remove doesn't seem to work.
To make this code work you should move adding part to init (withCoder, withFrame or both). You can join it with setting color because it can be done once. Next part is adjusting frame in layoutSubviews which is required because layers can't into autolayout. At the end you will have creation, adding as sublayer and set part called once at init, and adjusting called multiple times on layout pass. Now remove when called once - it would work with visible effect this time.

Gradient view not working programmatically with swift

Hi guys please am trying to make a view inherit from a gradient UIView with a background to it but for one or two reasons its not inheriting. Here is my code:
// The gradient class I want to be inherited from
class GradientView: UIView {
var gradient = CAGradientLayer()
override func awakeFromNib() {
super.awakeFromNib()
setupGradientView()
}
func setupGradientView(){
gradient.frame = self.bounds
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.8,1.0]
self.layer.addSublayer(gradient)
}
}
let headerHolder: GradientView = {
let view = GradientView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
//Where i set up the views
func setupViews() {
view.addSubview(headerHolder)
headerHolder.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
headerHolder.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
headerHolder.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
headerHolder.heightAnchor.constraint(equalToConstant: 80).isActive = true
}
You need to call it inside
override init(frame: CGRect)
as awakeFromNib is not called in such init cases it's called when the view loaded from xib / stroryboard
and set the frame inside
override func layoutSubviews{}
as it's the place where the view gets it's correct bounds
//
class GradientView: UIView {
var gradient = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupGradientView()
}
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = self.bounds
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupGradientView(){
gradient.colors = [UIColor.black.cgColor, UIColor.red.cgColor]
gradient.startPoint = CGPoint.zero
gradient.endPoint = CGPoint(x: 0, y: 1)
gradient.locations = [0.8,1.0]
self.layer.addSublayer(gradient)
}
}
The first problem is that you override awakeFromNib method, but it's never called, because you create your GradientView programmatically. You see, awakeFromNib is called onle the view loaded from Xib or Storyboard file. Here is quote from Apple Documentation.
The nib-loading infrastructure sends an awakeFromNib message to each object recreated from a nib archive, but only after all the objects in the archive have been loaded and initialized.
So if you want to create your view programmatically, you need to override init(frame: CGRect).
The second problem is that in setupGradientView method you're using self.bounds, but your view bounds has not been computed yet, because layout was not called. You may setup gradient layer frame at layoutSubviews method.
class GradientView: UIView {
var gradient = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupGradientView()
}
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = self.bounds
}
func setupGradientView() {
let colorFrom = UIColor.white.cgColor
let colorTo = UIColor.init(white:1.0, alpha: 0.0).cgColor
gradient.colors = [colorFrom, colorTo]
gradient.startPoint = CGPoint.zero
gradient.endPoint = CGPoint(x: 0, y: 1)
gradient.locations = [0.8, 1.0]
self.layer.addSublayer(gradient)
}
}
Hope it helps you.
The problem is that, because you created the CAGradientLayer directly, it doesn’t participate in UIKit’s layout system. You’re setting the gradient layer’s frame before the GradientView has been resized for the current device.
Instead of creating the gradient layer directly and making it a sublayer of the GradientView’s layer, tell GradientLayer that it’s own layer should be a CAGradientLayer:
class GradientView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
...
}

Animate setFillColor color change in custom UIView

I have a custom UIView called CircleView which is essentially a colored ellipse. The color property I'm using to color the ellipse is rendered using setFillColor on the graphics context. I was wondering if there was a way to animate the color change, because when I run through the animate / transition the color changes immediately instead of being animated.
Example Setup
let c = CircleView()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
UIView.transition(with: c, duration: 5, options: .transitionCrossDissolve, animations: {
c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
c.color = UIColor.yellow // Not animated
}
Circle View
class CircleView : UIView {
var color = UIColor.blue {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setFillColor(color.cgColor)
context.fillPath()
}
}
You can use the built in animation support for the layer's backgroundColor.
While the easiest way to make a circle is to make your view a square (using aspect ratio constraints, for instance) and then set the cornerRadius to half the width or height, I assume you want something a bit more advanced, and that is why you used a path.
My solution to this would be something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// Setup the view, by setting a mask and setting the initial color
private func setup(){
layer.mask = shape
layer.backgroundColor = color.cgColor
}
// Change the path in case our view changes it's size
override func layoutSubviews() {
super.layoutSubviews()
let path = CGMutablePath()
// add an elipse, or what ever path/shapes you want
path.addEllipse(in: bounds)
// Created an inverted path to use as a mask on the view's layer
shape.path = UIBezierPath(cgPath: path).reversing().cgPath
}
// this is our shape
private var shape = CAShapeLayer()
}
Or if you really need a simple circle, just something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup(){
clipsToBounds = true
layer.backgroundColor = color.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
}
Either way, this will animate nicely:
UIView.animate(withDuration: 5) {
self.circle.color = .red
}
Strange things happens!
Your code is ok, you just need to call your animation in another method and asyncronusly
As you can see, with
let c = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
changeColor()
}
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self.c, duration: 5, options: .transitionCrossDissolve, animations: {
self.c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
self.c.color = UIColor.yellow // Not animated
}
}
}
Work as charm.
Even if you add a button that trigger the color change, when you press the button the animation will work.
I encourage you to set this method in the definition of the CircleView
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self, duration: 5, options: .transitionCrossDissolve, animations: {
self.color = UIColor.red
})
UIView.animate(withDuration: 5) {
self.color = UIColor.yellow
}
}
}
and call it where you want in your ViewController, simply with
c.changeColor()

Applying gradient background for UIView using auto layout

I extended UIView to add a addGradientWithColor() method to get the gradient background:
extension UIView {
func addGradientWithColor() {
let gradient = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = [gradientEndColor.CGColor, gradientStartColor.CGColor]
gradient.startPoint = CGPointMake(1,0)
gradient.endPoint = CGPointMake(0.2,1)
self.layer.insertSublayer(gradient, atIndex: 0)
} }
My issue is when I run landscape mode, the UIView is not stretched
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.view.addGradientWithColor() }
I tried to calling viewDidLayoutSubviews() but its not working properly
Here is the screen shot:
after removing viewDidLayoutSubviews()
You can subclass the UIView and override drawRect method where you add your gradient.
Updated to Swift 4
class GradientView: UIView {
private let gradient : CAGradientLayer = CAGradientLayer()
private let gradientStartColor: UIColor
private let gradientEndColor: UIColor
init(gradientStartColor: UIColor, gradientEndColor: UIColor) {
self.gradientStartColor = gradientStartColor
self.gradientEndColor = gradientEndColor
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
gradient.frame = self.bounds
}
override public func draw(_ rect: CGRect) {
gradient.frame = self.bounds
gradient.colors = [gradientEndColor.cgColor, gradientStartColor.cgColor]
gradient.startPoint = CGPoint(x: 1, y: 0)
gradient.endPoint = CGPoint(x: 0.2, y: 1)
if gradient.superlayer == nil {
layer.insertSublayer(gradient, at: 0)
}
}
}
After you create your UIView you just need to add your constraints to that view.
you could save a reference to the layer, and adjust it's frame in the views layoutSublayersOfLayer method. this could be outsourced in an UIView subclass:
class GradientView: UIView {
private let gradient : CAGradientLayer = CAGradientLayer()
/**
displays a gradient on the view
*/
func addGradient() {
self.gradient.frame = self.bounds
self.gradient.colors = [gradientEndColor.CGColor, gradientStartColor.CGColor]
gradient.startPoint = CGPointMake(1,0)
gradient.endPoint = CGPointMake(0.2,1)
self.layer.insertSublayer(self.gradient, atIndex: 0)
}
/**
resizes the gradient with the view size
*/
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
self.gradient.frame = self.bounds
}
}
Layers do not autoresize them self. To fix this issue you should change layer frame. This is one way how it is possible to implement:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//Update you're layer based on the new frame
self.view.addGradientWithColor()
}
Inspired from solutions here, I've created the following class.
Usage examples:
// Simple usage. From clear to black.
let gradientView1 = GradientView(colors: [.clear, .black])
// Tweak locations. Here the gradient from red to green will take 30% of the view.
let gradientView2 = GradientView(colors: [.red, .green, .blue], locations: [0, 0.3, 1])
// Create your own gradient.
let gradient = CAGradientLayer()
gradient.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
let gradientView3 = GradientView(gradient: gradient)
The class:
import UIKit
/**
* View with a gradient layer.
*/
class GradientView: UIView {
let gradient : CAGradientLayer
init(gradient: CAGradientLayer) {
self.gradient = gradient
super.init(frame: .zero)
self.gradient.frame = self.bounds
self.layer.insertSublayer(self.gradient, at: 0)
}
convenience init(colors: [UIColor], locations:[Float] = [0.0, 1.0]) {
let gradient = CAGradientLayer()
gradient.colors = colors.map { $0.cgColor }
gradient.locations = locations.map { NSNumber(value: $0) }
self.init(gradient: gradient)
}
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
self.gradient.frame = self.bounds
}
required init?(coder: NSCoder) { fatalError("no init(coder:)") }
}

Resources