Subviews are not displaying for CAGradientLayer added view [duplicate] - ios

This question already has answers here:
How to Apply Gradient to background view of iOS Swift App
(30 answers)
Closed 4 years ago.
used below code for adding gradient layer for a view. After adding gradient layer to a view if i tried to add new subviews inside my gradient view the new view are not getting displayed
func setGradientBackground(_ view: UIView ,colorStart:CGColor ,colorEnd:CGColor,cornerRadius:CGFloat) {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [colorStart, colorEnd]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5);
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5);
gradientLayer.frame = view.bounds
gradientLayer.cornerRadius = cornerRadius
view.layer.addSublayer(gradientLayer)
}
if i extend a class with UIView how can i set gradient color directly from storyboard attributes inspector. I have seen this in some libraries (cosmos) where we can directly set rating color

Instead of adding gradient layer, create subclass of uiview and override layer property with gradientLayer
Gradient View:
#IBDesignable class VerticalGradientView: UIView {
#IBInspectable var topColor: UIColor = UIColor.red {
didSet {
setGradient()
}
}
#IBInspectable var bottomColor: UIColor = UIColor.blue {
didSet {
setGradient()
}
}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
setGradient()
}
private func setGradient() {
(layer as! CAGradientLayer).colors = [topColor.cgColor, bottomColor.cgColor]
(layer as! CAGradientLayer).startPoint = CGPoint(x: 0.5, y: 0)
(layer as! CAGradientLayer).endPoint = CGPoint(x: 0.5, y: 1)
}
}
Use:
let gradientView = VerticalGradientView()
gradientView.frame = CGRect(0,0,100,100)
gradientView.topColor = .black
let label = UILabel()
gradientView.addSubview(label)

It looks like you are using the layer directly and adding it to the view's backing layer. I recommend a simple UIView subclass that encapsulates the gradient behavior. The implementation is quite simple and the the largest benefit is the control you get over where it is rendered in the z dimension. Then you can just use the gradient view as a parent view for your content or if that isn't reasonable in your specific scenario, you could just insert the gradient view as a subview at index 0 using the UIView.insert(_:,at:) method of UIView instances. This would make your gradient view appear below the other views you add later.
I recently began using gradients in one of my apps. Here is the code I used to define my custom gradient type.
import UIKit
public class AxialGradientView: UIView {
public var colors: [UIColor] = [] {
didSet {
gradientLayer.colors = colors.map { $0.cgColor }
}
}
/// Expressed in a unit coordinate system.
public var startPoint: CGPoint = CGPoint(x: 0.5, y: 0) {
didSet {
gradientLayer.startPoint = startPoint
}
}
/// Expressed in a unit coordinate system.
public var endPoint: CGPoint = CGPoint(x: 0.5, y: 1) {
didSet {
gradientLayer.endPoint = endPoint
}
}
override public class var layerClass: Swift.AnyClass {
get {
return CAGradientLayer.self
}
}
private var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
}

Related

Diagonal Gradient background on UIView and Screen Rotation [duplicate]

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
}
}

Fade Bottom of TableView - Swift

I currently have a tableView populated with the user's friends. The tableView is part of TableViewController which operates correctly. I want the tableview to be permanently faded at the very bottom similar to the following picture:
I'm aware that you can use gradients, but I'm not sure how. Somebody please help.
You could achieve this appearance by adding a -white background- UIView on top the the table view and set its alpha to the desired value.
At Storyboard, it would be similar to:
Note that at the document outline - views hierarchy the view is underneath the tableview, which means it would be on top of it.
Remark: it might be hard to add the view by dragging it directly to the view controller since it would be as subview in the table view; You could drag it in the document outline instead.
Then you could setup the desired constraints to it (I just added 0 leading, 0 trailing, 0 bottom and 70 height). Finally change its color opacity:
As you can see, I just edited the opacity to be 50% (alpha = 0.5).
Furthermore:
you could also let the bottom view to has gradient of white and clear colors, it could even make it nicer! You might want to check:
How to Apply Gradient to background view of iOS Swift App
You can do this by adding a gradient layer behind the button and above the table view.
For this, Create a UIView subclass, Say GradientView:
import UIKit
#IBDesignable
class GradientView: UIView {
#IBInspectable var startColor: UIColor = .black { didSet { updateColors() }}
#IBInspectable var middleColor: UIColor = .white { didSet { updateColors() }}
#IBInspectable var endColor: UIColor = .white { didSet { updateColors() }}
#IBInspectable var startLocation: Double = 0.05 { didSet { updateLocations() }}
#IBInspectable var middleLocation: Double = 0.05 { didSet { updateLocations() }}
#IBInspectable var endLocation: Double = 0.95 { didSet { updateLocations() }}
#IBInspectable var horizontalMode: Bool = false { didSet { updatePoints() }}
#IBInspectable var diagonalMode: Bool = false { didSet { updatePoints() }}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return (layer as? CAGradientLayer)!
}
func updatePoints() {
if horizontalMode {
gradientLayer.startPoint = diagonalMode ? CGPoint(x: 1, y: 0): CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = diagonalMode ? CGPoint(x: 0, y: 1): CGPoint(x: 1, y: 0.5)
} else {
gradientLayer.startPoint = diagonalMode ? CGPoint(x: 0, y: 0): CGPoint(x: 0.5, y: 0)
gradientLayer.endPoint = diagonalMode ? CGPoint(x: 1, y: 1): CGPoint(x: 0.5, y: 1)
}
}
func updateLocations() {
gradientLayer.locations = [startLocation as NSNumber, middleLocation as NSNumber, endLocation as NSNumber]
}
func updateColors() {
gradientLayer.colors = [startColor.cgColor, middleColor.cgColor, endColor.cgColor]
}
override func layoutSubviews() {
super.layoutSubviews()
updatePoints()
updateLocations()
updateColors()
}
}
And, you can assign this view in storyboard as well as it is #IBDesignable:
You can configure the colour from here. In you case, you should take white colour:

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:)") }
}

Border in circle UIView using Swift

I'm trying to make an UIView with border but I only make it appear in UIView not only on the circle.
In the brown circle I want a black border, but it has to be in the circle not in the UIView.
I have a class called Ball where I draw the circle.
class Ball: UIView {
var desiredColour = UIColor.blueColor()
struct mine {
static var p = UIBezierPath(ovalInRect: CGRectMake(0,0,118,117))
}
override func drawRect(rect: CGRect) {
// Drawing code
desiredColour.setFill()
mine.p.fill()
}
func colour() {
var randColor: UIColor = Colors.randomColor()
Colors.ballColor = randColor
Colors.colorPosition = find(Colors.arrayColors, randColor)!
desiredColour = randColor
self.setNeedsDisplay()
}
}
I used the code:
override func drawRect(rect: CGRect) {
// Drawing code
desiredColour.setFill()
let desiredBorderColor = UIColor.blackColor()
desiredBorderColor.setStroke()
self.layer.borderWidth = 3.0
self.layer.cornerRadius = self.frame.size.width/2.0
mine.p.fill()
mine.p.stroke()
}
but i get a border with a little cuts:
Try calling this function by passing you view as a parameter
func drawBlackBorder(view: UIView) {
view.layer.borderColor = UIColor.blackColor
view.layer.borderWidth = 1.0
view.layer.cornerRadius = view.frame.size.width/2.0
view.backgroundColor = UIColor.brownColor
}
override func drawRect(rect: CGRect) {
// Drawing code
desiredColour.setFill()
let desiredBorderColor = UIColor.blackColor()
desiredBorderColor.setStroke()
mine.p.lineWidth = 2.0 //set to whatever you want
mine.p.fill()
mine.p.stroke()
}
But note that for border to work you need to have some space around you circle.
Do the following. Declare myBorderWidth (of type CGFloat) property and then change
static var p = UIBezierPath(ovalInRect: CGRectMake(0,0,118,117))
to
static var p = UIBezierPath(ovalInRect: CGRectMake(myBorderWidth/2,myBorderWidth/2,118-myBorderWidth/2,117-myBorderWidth/2))
You can also remove repetitions of myBorderWidth/2 by declaring it as a property.
No need to modify layer, just set up UIBezierPath with proper insets depending on border thickness and set it's width (tested on Swift 4.2):
var fillColor: UIColor //circle color
var strokeColor: UIColor //border color
var borderWidth: CGFloat //border width
override func draw(_ rect: CGRect) {
fillColor.setFill()
strokeColor.setStroke()
let circlePath = UIBezierPath(ovalIn: CGRect(x: borderWidth, y: borderWidth, width: rect.width - borderWidth*2, height: rect.height - borderWidth*2))
circlePath.lineWidth = borderWidth
circlePath.stroke()
circlePath.fill()
}

Resources