I'm writing an app that generates random gradients and displays them on the screen using a custom UIView Class. Because I have more planned functions for the app, I chose to give the gradient its own class. The code for generating the gradient is here:
import UIKit
class Gradient {
var topColor = UIColor()
var bottomColor = UIColor()
var gradient: CGGradientRef?
func generateGradient() -> CGGradientRef {
var red = CGFloat(arc4random_uniform(256))
var green = CGFloat(arc4random_uniform(256))
var blue = CGFloat(arc4random_uniform(256))
topColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0)
red = CGFloat(arc4random_uniform(256))
green = CGFloat(arc4random_uniform(256))
blue = CGFloat(arc4random_uniform(256))
bottomColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0)
let gradientColors = [topColor.CGColor, bottomColor.CGColor]
let gradientStops:[CGFloat] = [0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let generatedGradient = CGGradientCreateWithColors(colorSpace, gradientColors, gradientStops)
return generatedGradient
}
init() {
gradient = self.generateGradient()
}
}
The gradient viewer class that displays the gradient reads:
import UIKit
#IBDesignable class Gradient_Viewer: UIView {
override func drawRect(rect: CGRect) {
let background = Gradient()
let context = UIGraphicsGetCurrentContext()
let startPoint = CGPoint.zeroPoint
let endPoint = CGPoint(x: 0, y: self.bounds.height)
CGContextDrawLinearGradient(context, background.gradient, startPoint, endPoint, 0)
}
}
Is there something wrong with the gradient generation, or am I not accessing the gradient properly?
#IBDesignable class Gradient_Viewer: UIView {
var random8bit: CGFloat {
return CGFloat(arc4random_uniform(256))/255.0
}
var gradient: CGGradientRef {
return CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), [UIColor(red: random8bit, green: random8bit, blue: random8bit, alpha: 1.0).CGColor, UIColor(red: random8bit, green: random8bit, blue: random8bit, alpha: 1.0).CGColor], [0, 1.0])
}
override func drawRect(rect: CGRect) {
CGContextDrawLinearGradient(UIGraphicsGetCurrentContext(), gradient, CGPoint.zeroPoint, CGPoint(x: 0, y: self.bounds.height), 0)
}
}
Related
On iPhone 8 :
On iPhone X :
The issue lies with status bar. The is nothing exclusive I am doing here. It is just that the color is a gradient one, but it should not matter.
ViewController's attributes :
Function for setting gradient :
func setNavigationBarAppearence() {
let gradient = CAGradientLayer()
let sizeLength = UIScreen.main.bounds.size.height * 2
let defaultNavigationBarFrame = CGRect(x: 0, y: 0, width: sizeLength, height: self.navigationController.navigationBar.frame.size.height)
gradient.frame = defaultNavigationBarFrame
gradient.colors = [UIColor(red: 30/255, green: 234/255, blue: 191/255, alpha: 1).cgColor, UIColor(red: 12/255, green: 198/255, blue: 183/255, alpha: 1).cgColor]
UINavigationBar.appearance().setBackgroundImage(self.image(fromLayer: gradient), for: .default)
UINavigationBar.appearance().tintColor = UIColor.white
}
func image(fromLayer layer: CALayer) -> UIImage {
UIGraphicsBeginImageContext(layer.frame.size)
layer.render(in: UIGraphicsGetCurrentContext()!)
let outputImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return outputImage!
}
Try this approch. May this solve your problem
/// Applies a background gradient with the given colors
func apply(gradient colors : [CGColor]) {
var frameAndStatusBar: CGRect = self.bounds
frameAndStatusBar.size.height += UIApplication.shared.statusBarFrame.height
let gradient = CAGradientLayer()
gradient.frame = frameAndStatusBar
gradient.colors = colors
// gradient.locations = [0.0,1.0]
gradient.startPoint = CGPoint.init(x: 0.0, y: 0.0)
gradient.endPoint = CGPoint.init(x: 0.0, y: 1.0)
if let img = self.image(fromLayer: gradient)
{
setBackgroundImage(img, for: .default)
}
}
/// Creates a gradient image with the given settings
func image(fromLayer layer: CALayer) -> UIImage? {
UIGraphicsBeginImageContext(layer.frame.size)
if let context = UIGraphicsGetCurrentContext()
{
layer.render(in:context)
let outputImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return outputImage
}
return nil
}
Here is how I used this:
extension CAGradientLayer {
convenience init(frame: CGRect, colors: [UIColor]) {
self.init()
self.frame = frame
self.colors = []
for color in colors {
self.colors?.append(color.cgColor)
}
startPoint = CGPoint(x: 0, y: 0)
endPoint = CGPoint(x: 0, y: 1)
}
func createGradientImage() -> UIImage? {
var image: UIImage? = nil
UIGraphicsBeginImageContext(bounds.size)
if let context = UIGraphicsGetCurrentContext() {
render(in: context)
image = UIGraphicsGetImageFromCurrentImageContext()
}
UIGraphicsEndImageContext()
return image
}
}
extension UINavigationBar {
func setGradientBackground(colors: [UIColor]) {
var updatedFrame = bounds
updatedFrame.size.height += self.frame.origin.y
// above adjustment is important, otherwise the frame is considered without
// the status bar height which in turns causes the gradient layer to be calculated wrong
let gradientLayer = CAGradientLayer(frame: updatedFrame, colors: colors)
setBackgroundImage(gradientLayer.createGradientImage(), for: .default)
}
}
Usage:
override func viewDidLoad() {
...
let colors = [UIColor(red: 30/255, green: 234/255, blue: 191/255, alpha: 1), UIColor(red: 12/255, green: 198/255, blue: 183/255, alpha: 1)]
navigationController?.navigationBar.setGradientBackground(colors: colors)
...
}
This was related to the total height of navigation bar + status bar. I fixed it as follows :
func setNavigationBarAppearence() {
let gradient = CAGradientLayer()
let sizeLength = UIScreen.main.bounds.size.height * 2
var defaultNavigationBarFrame = CGRect(x: 0, y: 0, width: sizeLength, height: self.navigationController.navigationBar.frame.size.height)
if UIDevice().userInterfaceIdiom == .phone {
if UIScreen.main.nativeBounds.height == 2436{
defaultNavigationBarFrame.size.height += 44
} else {
defaultNavigationBarFrame.size.height += 20
}
}
gradient.frame = defaultNavigationBarFrame
gradient.colors = [UIColor(red: 30/255, green: 234/255, blue: 191/255, alpha: 1).cgColor, UIColor(red: 12/255, green: 198/255, blue: 183/255, alpha: 1).cgColor]
UINavigationBar.appearance().setBackgroundImage(self.image(fromLayer: gradient), for: .default)
UINavigationBar.appearance().tintColor = UIColor.white
}
func image(fromLayer layer: CALayer) -> UIImage {
UIGraphicsBeginImageContext(layer.frame.size)
layer.render(in: UIGraphicsGetCurrentContext()!)
let outputImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return outputImage!
}
Not the perfect fix I would say, but a working alternative.
I try to make a standard gradient top-bottom with long UIView. But it's not full. The nib is part of UITableViewCell, so I don't have access to viewDidLayoutSubviews() as in this thread.
I've tried to call contentView.layoutIfNeeded() from the code version of this view. I called it when UITableView cellForRowAtIndexPath gets called. But it's no effect.
I prepare the gradient in awakeFromNib().
let colors = [
UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1.0).cgColor,
UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 1.0).cgColor]
let gradient = CAGradientLayer()
gradient.frame = gradientView.bounds
gradient.colors = colors
gradientView.layer.insertSublayer(gradient, at: 0)
Is there something wrong with my code?
You should use view's boundsinstead of frame, because your layer is inside of the view, and the frame may have an offset.
The layout of the view is changed after awakeFromNib. So you should resize the layer in layoutSubviews of the view. For this create a property gradient and:
let gradient: CAGradientLayer = CAGradientLayer()
override func awakeFromNib() {
...
let colors = [
UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1.0).cgColor,
UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 1.0).cgColor]
gradient.frame = bounds
gradient.colors = colors
gradientView.layer.insertSublayer(gradient, at: 0)
...
}
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = bounds
}
EDIT: An alternative is a custom view with own layer class:
public class GradientLayer: UIView {
#IBInspectable var startColor: UIColor! = UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1.0)
#IBInspectable var endColor: UIColor! = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 1.0)
override class var layerClass : AnyClass {
return CAGradientLayer.self
}
override func awakeFromNib() {
super.awakeFromNib()
let colors = [ startColor.cgColor, endColor.cgColor ]
if let gradient = self.layer as? CAGradientLayer {
gradient.colors = colors
}
}
}
This is more elegant, because you can replace the static colors with IB inspectables, and you have a reusable component view.
override func layoutSubviews() {
super.layoutSubviews()
if let gradient = baseView.layer.sublayers?[0] as? CAGradientLayer {
gradient.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 currently have a single gradient color background (see below), but I want to gradually change the colors to shades that I will pre-define in an array and have it loop through. How can I achieve this?
var colorTop = UIColor(red: 255.0/255.0, green: 149.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor
var colorBottom = UIColor(red: 255.0/255.0, green: 94.0/255.0, blue: 58.0/255.0, alpha: 1.0).cgColor
func configureGradientBackground(colors:CGColor...){
let gradient: CAGradientLayer = CAGradientLayer()
let maxWidth = max(self.view.bounds.size.height,self.view.bounds.size.width)
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1)
let squareFrame = CGRect(origin: self.view.bounds.origin, size: CGSize(width: maxWidth, height: maxWidth * 0.935))
gradient.frame = squareFrame
gradient.colors = colors
view.layer.insertSublayer(gradient, at: 0)
}
override func viewDidLoad() {
super.viewDidLoad()
configureGradientBackground(colors: colorTop, colorBottom)
}
Really quite straightforward; I can't imagine what difficulty you are having. CAGradientLayer's colors property is itself animatable, so you simply animate the change of colors and, when the animation ends, proceed to the next animation of the change of colors.
In this example I alternate between just two sets of colors, yours and blue-and-green. So my state is a simple Bool. But obviously the example could be extended to any number of states, that is, any number of pairs of colors.
let colorTop = UIColor(red: 255.0/255.0, green: 149.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor
let colorBottom = UIColor(red: 255.0/255.0, green: 94.0/255.0, blue: 58.0/255.0, alpha: 1.0).cgColor
var state = false
var grad : CAGradientLayer?
override func viewDidLoad() {
// configure the gradient layer, start the animation
}
func animate() {
let arr = self.state ? [colorTop,colorBottom] : [UIColor.green.cgColor,UIColor.blue.cgColor]
self.state = !self.state
let anim = CABasicAnimation(keyPath: "colors")
anim.duration = 10 // or whatever
anim.fromValue = self.grad!.colors
anim.toValue = arr
anim.delegate = self
self.grad?.add(anim, forKey: nil)
DispatchQueue.main.async {
CATransaction.setDisableActions(true)
self.grad?.colors = arr
}
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
DispatchQueue.main.async {
self.animate()
}
}
You could have a variable in your viewDidLoad that is an int that keeps track of the index of the array that contains all your colors. For example, 0 is your first color, 1 is your second and so on. Then in your infinite while loop (also in the viewDidLoad), you can write:
if tracker == 0 {
// apply first color
} else if tracker == 1{
// apply second color
} ...
Then you can increment the tracker by one at the end and then check your if your tracker is past the size of your array (for example, you only have 11 colors).
tracker += 1
if tracker > 10 {
tracker = 0
}
I want to color from touchpoint of gradient layer.
I tried this :
func viewtap(sender: UITapGestureRecognizer) {
let touchPoint = sender.locationInView(self.gradientview) // Change to whatever view you want the point for
print("\(touchPoint))")
colorOfPoint(touchPoint)
}
This method I used but it gives original color only.
func colorOfPoint(point:CGPoint) -> UIColor
{
let colorSpace:CGColorSpace = CGColorSpaceCreateDeviceRGB()!
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue)
var pixelData:[UInt8] = [0, 0, 0, 0]
let context = CGBitmapContextCreate(&pixelData, 1, 1, 8, 4, colorSpace, bitmapInfo.rawValue)
CGContextTranslateCTM(context, -point.x, -point.y);
self.view.layer.renderInContext(context!)
let red:CGFloat = CGFloat(pixelData[0])/CGFloat(255.0)
let green:CGFloat = CGFloat(pixelData[1])/CGFloat(255.0)
let blue:CGFloat = CGFloat(pixelData[2])/CGFloat(255.0)
let alpha:CGFloat = CGFloat(pixelData[3])/CGFloat(255.0)
let color:UIColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)
colorChange.tintColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)
return color
}
Gradient Layer:
Always give .CGColor after uicolor. now i will pick color from any in UIView
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.shadowview.bounds
let colorTop = UIColor(red:CGFloat(RedColor)/255.0 , green: CGFloat (GreenColor)/255.0 , blue: CGFloat(BlueColor)/255.0,alpha: CGFloat(alphalab)/255.0)**.CGColor**
let colorBottom = UIColor(red: 35.0/255.0, green: 2.0/255.0, blue: 2.0/255.0, alpha: 1.0)**.CGColor**
colorChange.tintColor = UIColor(red:CGFloat(RedColor)/255.0 , green: CGFloat (GreenColor)/255.0 , blue: CGFloat(BlueColor)/255.0,alpha: CGFloat(alphalab)/255.0)
gradientLayer.colors = [colorTop, colorBottom]
gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.locations = [ 0.0, 1.0]
self.shadowview.layer.addSublayer(gradientLayer)