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
}
...
}
Related
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.
This question already has answers here:
How to Apply Gradient to background view of iOS Swift App
(30 answers)
Closed 3 years ago.
I am trying to set the background on my UIView to a gradient. I have the following in my UIViewController's viewDidLoad method:
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.blue70.cgColor, UIColor.blue60.cgColor, UIColor.blue70.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1 )
gradientLayer.frame = self.view.bounds
self.view.layer.insertSublayer(gradientLayer, at: 0)
The gradient renders as expected. However, when I rotate the device the gradient does not redraw and is no longer rendered properly. Rotating from portrait->landscape leaves me with a blank section on the right or left. Rotating from landscape->portrait leaves me with a blank section at the bottom. I tried moving this code to viewDidLayoutSubviews, but that didn't solve it. Any recommendations on how to accomplish this?
What i would recommend here is to add the gradient in the viewWillLayoutSubviews() method
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.view.addGradiant()
}
But now we would have to remove the old layer When adding the new one with the new frames(In case the Phone rotates)
We can then first remove the layer and add the new one like in this extension method
extension UIView {
func addGradiant() {
let GradientLayerName = "gradientLayer"
if let oldlayer = self.layer.sublayers?.filter({$0.name == GradientLayerName}).first {
oldlayer.removeFromSuperlayer()
}
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.blue.cgColor, UIColor.red.cgColor, UIColor.green.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1 )
gradientLayer.frame = self.bounds
gradientLayer.name = GradientLayerName
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
Make gradientLayer view controller's property.
let gradientLayer = CAGradientLayer()
Add gradientLayer in the viewDidLoad in the same way you did.
Set the frame in viewWillLayoutSubviews
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
gradientLayer.frame = self.view.bounds
}
While you can do as the other answers suggest and update your gradient layer bounds in viewDidLayoutSubviews, the more reusable way to do things is to make a gradient view subclass who's layerClass is a CAGradientLayer. Such a view will have its layer automatically resized to fit the view (because the gradient layer is now the backing layer for the view and not some subLayer) and you can use this view anywhere just by changing the class of your view in your nib file or storyboard:
class GradientView: UIView {
var colors: [UIColor] = [.red, .white] {
didSet {
setup()
}
}
var locations: [CGFloat] = [0,1] {
didSet {
setup()
}
}
var startPoint: CGPoint = .zero {
didSet {
setup()
}
}
var endPoint: CGPoint = CGPoint(x: 1, y: 1) {
didSet {
setup()
}
}
override class var layerClass : AnyClass { return CAGradientLayer.self }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func awakeFromNib() {
super.awakeFromNib()
setup()
}
private func setup() {
guard let layer = self.layer as? CAGradientLayer else {
return
}
layer.colors = colors.map { $0.cgColor }
layer.locations = locations.map { NSNumber(value: Double($0)) }
layer.startPoint = startPoint
layer.endPoint = endPoint
}
}
Currently I have a UIView container situated above a UITableView. Using the inheritance from UIScrollView, I created an IBOutlet of the container view's leading constraint, and adjust the constraint similar to as follows:
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
if scrollView.contentOffset.y < 0
{
...
containerViewLeftConstraint.constant -= abs(scrollView.contentOffset.y) * 2
}
else
{
...
containerViewLeftConstraint.constant += abs(scrollView.contentOffset.y) * 2
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
resetContainerViewSize()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
{
resetContainerViewSize()
}
func resetContainerViewSize()
{
containerViewLeftConstraint.constant = 0.0
UIView.animate(withDuration: 0.4,
delay: 0.0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: .curveEaseInOut,
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
For demonstration, I've set the container view's backgroundColor to red to visually see what's happening:
I've declared a var gradientLayer = CAGradientLayer() and added it as as a sublayer to the container view like so:
func createGradientOverlay()
{
gradientLayer.frame = containerView.frame
let colors = [UIColor.black.withAlphaComponent(0.5).cgColor,
UIColor.white.cgColor]
gradientLayer.colors = colors
gradientLayer.startPoint = CGPoint(x:0.0,
y:0.5)
gradientLayer.endPoint = CGPoint(x:1.0, y:0.5);
containerView.layer.addSublayer(gradientLayer)
}
However, the result I'm getting is as shown:
The gradient layer does not stick to the bounds of the container view and appears to be floating.
I've looked at several similar questions:
CALayers didn't get resized on its UIView's bounds change. Why?
Auto Layout constraint on CALayer IOS
How to get CAShapeLayer to work with constraints with swift?
How to add a CAGradientLayer to a UIView when using programmatic constraints
All suggests using viewDidLayoutSubviews like so:
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
gradientLayer.frame = nameContainerView.frame
}
However the gradient layer still appears to be floating and not sticking to the constraints of the container view.
I've also tried the following to no avail:
gradientLayer.frame = nameContainerView.bounds
gradientLayer.bounds = nameContainerView.bounds
gradientLayer.frame = nameContainerView.layer.bounds
gradientLayer.frame = nameContainerView.layer.frame
How can I resolve this issue?
Pangu - If someone wants additional information in an effort to try to help you then you should probably provide the information so that they can help you. Now to your question. You can accomplish this two ways cleanly and if I could see your function probably 3.
The first would be to create a view that uses a CAGradientLayer as the backing layer and then you could apply autolayout to the Gradient View. Here is a class you could use that took me a few minutes.
import UIKit
class GradientBackedLayer: UIView {
var colors : [UIColor] = [UIColor.black.withAlphaComponent(0.5),UIColor.white]{
didSet{
setUpGradient()
}
}
var startPoint : CGPoint = CGPoint(x:0.0,y:0.5){
didSet{
setUpGradient()
}
}
var endPoint : CGPoint = CGPoint(x:1.0, y:0.5){
didSet{
setUpGradient()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setUpGradient()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUpGradient()
}
func setUpGradient(){
self.backgroundColor = .clear
let cgColors = colors.map({$0.cgColor})
if let gradientLayer = self.layer as? CAGradientLayer{
gradientLayer.colors = cgColors
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
}
}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
The second is to use a transform instead of changing the constraints on the container and everything will scale proportionately. I will leave this for you to work it out if that is the way you would like to go.
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)
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:)") }
}