Applying gradient colour to storyboard view with auto layout - ios

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

Related

Gradient from UIView after orientation transition

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.

Gradient Layer Is Not Working on Portrait Mode in Xib File

i wants to set gradient layer on a UIView in Xib File but in Portrait Mode Gradient Layer it's not fill all Of UIView.
Gradient Class :
extension UIView {
#discardableResult
func applyGradient(colours: [UIColor]) -> CAGradientLayer {
return self.applyGradient(colours: colours, locations: nil)
}
#discardableResult
func applyGradient(colours: [UIColor], locations: [NSNumber]?) -> CAGradientLayer {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.locations = locations
self.layer.insertSublayer(gradient, at: 0)
return gradient
}
}
Xib File Class :
class TempView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
}
override func layoutSubviews() {
super.layoutSubviews();
self.viewMain.applyGradient(colours: [.black, .gray])
}
}
Try this:
class TempView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
}
override func layoutSubviews() {
super.layoutSubviews();
self.viewMain.layoutIfNeeded()
self.viewMain.applyGradient(colours: [.black, .gray])
}
}
i used this extension To solve This Problem :
extension UIView {
func addGradient(colors: [UIColor], locations: [NSNumber]) {
addSubview(ViewWithGradient(addTo: self, colors: colors, locations: locations))
}
}
class ViewWithGradient: UIView {
private var gradient = CAGradientLayer()
init(addTo parentView: UIView, colors: [UIColor], locations: [NSNumber]){
super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 2))
restorationIdentifier = "__ViewWithGradient"
for subView in parentView.subviews {
if let subView = subView as? ViewWithGradient {
if subView.restorationIdentifier == restorationIdentifier {
subView.removeFromSuperview()
break
}
}
}
let cgColors = colors.map { (color) -> CGColor in
return color.cgColor
}
gradient.frame = parentView.frame
gradient.colors = cgColors
gradient.locations = locations
backgroundColor = .clear
parentView.addSubview(self)
parentView.layer.insertSublayer(gradient, at: 0)
parentView.backgroundColor = .clear
autoresizingMask = [.flexibleWidth, .flexibleHeight]
clipsToBounds = true
parentView.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let parentView = superview {
gradient.frame = parentView.bounds
}
}
override func removeFromSuperview() {
super.removeFromSuperview()
gradient.removeFromSuperlayer()
}
}

how to set gradient layer width to be the same as button width?

I am new in iOS Development, I want to make gradient rounded button by using this designable class
#IBDesignable
class GradientButton: UIButton {
let gradientLayer = CAGradientLayer()
#IBInspectable
var topGradientColor: UIColor? {
didSet {
setGradient(topGradientColor: topGradientColor, bottomGradientColor: bottomGradientColor)
}
}
#IBInspectable
var bottomGradientColor: UIColor? {
didSet {
setGradient(topGradientColor: topGradientColor, bottomGradientColor: bottomGradientColor)
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
#IBInspectable var fullRounded: Bool = false {
didSet {
updateCornerRadius()
}
}
#IBInspectable var cornerRadiusOfButton : CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadiusOfButton
}
}
func updateCornerRadius() {
layer.cornerRadius = fullRounded ? (frame.size.height / 2) : cornerRadiusOfButton
}
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var borderColor: UIColor? {
set {
guard let uiColor = newValue else { return }
layer.borderColor = uiColor.cgColor
}
get {
guard let color = layer.borderColor else { return nil }
return UIColor(cgColor: color)
}
}
private func setGradient(topGradientColor: UIColor?, bottomGradientColor: UIColor?) {
if let topGradientColor = topGradientColor, let bottomGradientColor = bottomGradientColor {
gradientLayer.frame = bounds
gradientLayer.colors = [topGradientColor.cgColor, bottomGradientColor.cgColor]
gradientLayer.borderColor = layer.borderColor
gradientLayer.borderWidth = layer.borderWidth
gradientLayer.cornerRadius = layer.cornerRadius
layer.insertSublayer(gradientLayer, at: 0)
} else {
gradientLayer.removeFromSuperlayer()
}
}
}
but here is the result:
as you can see the width of the gradient layer button is incorret.
here is the constraint layout I use:
I want that gradient layer to have the same width as the original button, I suspect that the problem is in this line
gradientLayer.frame = bounds
but unfortunately I don't know how to set the gradient layer width to be the same size as the original button programmatically in #IBDesignable class
could you please help me to solve this issue ? thanks in advance
Override layoutSubviews inside GradientButton and update the gradientLayer frame as,
override func layoutSubviews() {
super.layoutSubviews()
self.gradientLayer.frame = bounds
}

Why gradient doesn't cover the whole width of the view

I'm trying to apply a gradient to a view which is constraint to the top, left and right of the main screen but for some reason the gradient doesn't cover the whole width of the view that is applied to (see the yellow in the picture).
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let gradient = CAGradientLayer()
gradient.colors = [UIColor.blue.cgColor, UIColor.white.cgColor]
gradient.startPoint = CGPoint(x:00, y:00)
gradient.endPoint = CGPoint(x:0, y:0.6)
gradient.frame = myView.bounds
myView.clipsToBounds = true
myView.layer.insertSublayer(gradient, at: 0)
}
}
What am I doing wrong?
The problem is likely that you are adding the gradient layer in viewDidLoad(). A view doesn't get set to it's final size until after viewDidLoad().
Your view controller may be set up in your XIB/Storyboard for a different screen-size than you're running it on. (Say you have it set to iPhone SE size, but you're running it on a 6. The 6's screen is a little wider, so the layer will get set to the width of the iPhone SE, when the view is first loaded. Then the view will be resized, but the layer's size will never be adjusted.)
You should implement the UIViewController method viewDidLayoutSubviews(), and in that method, adjust the layer's frame:
override func viewDidLayoutSubviews() {
gradientLayer.frame = self.view.bounds
}
That way if the view gets resized (due to auto-rotation, for example) the gradient layer will be automatically adjusted accordingly.
EDIT:
As pointed out by Sulthan, changes to a layer's frame are animated by default. You should probably wrap the frame change in a CATransaction.begin/CATransaction.end and disable actions, like below:
override func viewDidLayoutSubviews() {
CATransaction.begin()
CATransaction.setDisableActions(true)
gradientLayer.frame = self.view.bounds
CATransaction.commit()
}
You can turn it to a UIView. So it will resize automatically and can be seen directly in the Storyboard:
#IBDesignable
final class GradientView: UIView {
#IBInspectable var firstColor: UIColor = .clear { didSet { updateView() } }
#IBInspectable var secondColor: UIColor = .clear { didSet { updateView() } }
#IBInspectable var startPoint: CGPoint = CGPoint(x: 0, y: 0) { didSet { updateView() } }
#IBInspectable var endPoint: CGPoint = CGPoint(x: 1, y: 1) { didSet { updateView() } }
override class var layerClass: AnyClass { get { CAGradientLayer.self } }
override func layoutSubviews() {
super.layoutSubviews()
updateView()
layer.frame = bounds
}
private func updateView() {
let layer = self.layer as! CAGradientLayer
layer.colors = [firstColor, secondColor].map {$0.cgColor}
layer.startPoint = startPoint
layer.endPoint = endPoint
}
}
You needn't set the start and end point, given your goal is to have the gradient span the entire view. You're already setting that with `
gradientLayer.frame = self.view.bounds
`
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
var gradientLayer: CAGradientLayer!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
createGradientLayer()
}
func createGradientLayer() {
gradientLayer = CAGradientLayer()
gradientLayer.frame = self.view.bounds
gradientLayer.colors = [UIColor.blueColor().CGColor, UIColor.whiteColor().CGColor]
self.view.layer.addSublayer(gradientLayer)
}
}
simply put you gredient layer in main thred like this
DispatchQueue.main.async {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.viewGredient.bounds
gradientLayer.colors = [UIColor.black.cgColor, UIColor.white.cgColor]
gradientLayer.locations = [0.0, 1.0]
self.viewGredient.layer.addSublayer(gradientLayer)
}
You can simplify it by CustomClass with one line of code
class GradientView: UIView {
override class var layerClass: Swift.AnyClass {
return CAGradientLayer.self
}
}
// MARK: Usage
#IBOutlet weak var myView: GradientView!
guard let gradientLayer = myView.layer as? CAGradientLayer else { return }
gradient.colors = [UIColor.blue.cgColor, UIColor.white.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.0)
gradient.endPoint = CGPoint(x:0, y:0.6)

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