I've been trying to create a function that generates a random gradient background every time a new view controller is presented, as shown with the code provided below, yet I'm unable to get it to properly work. The issue being that the "random" colors are always the same and are generated in the same order.
import Foundation
import UIKit
extension UIView {
func setGradientBackground() {
let redValue = CGFloat(drand48())
let greenValue = CGFloat(drand48())
let blueValue = CGFloat(drand48())
let redValue2 = CGFloat(drand48())
let greenValue2 = CGFloat(drand48())
let blueValue2 = CGFloat(drand48())
let random = UIColor(red: redValue, green: greenValue, blue: blueValue, alpha: 1).cgColor
let random2 = UIColor(red: redValue2, green: greenValue2, blue: blueValue2, alpha: 1).cgColor
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = [random, random2]
return layer.insertSublayer(gradient, at: 0)
}
}
If you use drand48, you need to seed it (e.g. with srand48). The quick and dirty solution would be to seed it with some value derived from time from the number of seconds passed since some reference date or the like.
Even better, I’d suggest using CGFloat.random(in: 0...1) instead, which doesn’t require any seeding.
extension UIView {
func addGradientBackground() {
let random = UIColor.random().cgColor
let random2 = UIColor.random().cgColor
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = [random, random2]
layer.insertSublayer(gradient, at: 0)
}
}
extension UIColor {
static func random() -> UIColor {
let red = CGFloat.random(in: 0...1)
let green = CGFloat.random(in: 0...1)
let blue = CGFloat.random(in: 0...1)
return UIColor(red: red, green: green, blue: blue, alpha: 1)
}
}
For what it’s worth, we should appreciate that this technique of adding a sublayer is problematic if the view changes size (e.g., you rotate the device, etc.). You’d have to manually hook into viewDidLayoutSubviews or layoutSubviews and adjust this gradient layer frame. I might instead suggest actually having a UIView subclass that does this gradient layer.
#IBDesignable
class RandomGradientView: UIView {
override static var layerClass: AnyClass { return CAGradientLayer.self }
var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
gradientLayer.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
}
}
private extension RandomGradientView {
func configure() {
let random = UIColor.random().cgColor
let random2 = UIColor.random().cgColor
gradientLayer.colors = [random, random2]
}
}
Because this UIView subclass actually specifies the layer class to be used for the main backing layer, and because that main layer always resizes automatically as the view changes size, then it will gracefully handle any size changes (including animated ones).
All of the having been said, I appreciate the convenience of the extension (you can apply it to existing UIView subclasses), but you have to offset that with the more cumbersome process of handling frame size changes. It’s your choice, but I wanted to provide the alternative.
Related
I want to be able to change to change the colours of a CAGradientLayer in realtime with values i receive over #IBAction in the main ViewController. UISlider moves, color changes in background. I m asking for a theoretical approach.
What is the pattern or technique used to achieve this ? i really got lost and the mighty internet didn't reveal any usable help.
Subclasssing a UIView and add the gradientLayer there ? Then observe the variable with the incoming values and let KVO update the array entries (color sets) with cgColor values ?
Instantiate the gradientLayer in the main ViewController and update its color Properties there when value changes come in via #IBAction ?
Code helps, but its secondary here. i am asking more for a theoretical solution. I try to follow MVC but i am hardly confused where the gradientLayer should be instantiated, whats the best method to change the colours dynamically ect…
open for inputs, thx
Using a very simple extension for UIView you can get what you need. Here is a quick example using semi-static colors just for simplicity, but you can extend it as you need.
extension UIView {
/**
Adds colors to a CAGradient Layer.
- Parameter value: The Float coming from the slider.
- Parameter gradientLayer: The CAGradientLayer to change the colors.
*/
func addColorGradient (value: CGFloat, gradientLayer: CAGradientLayer) {
let topColor = UIColor(red: value / 255, green: 1, blue: 1, alpha: 1)
let bottomColor = UIColor(red: 1, green: 1, blue: value / 255, alpha: 1)
gradientLayer.colors = [topColor.cgColor, bottomColor.cgColor]
}
}
Usage example:
import UIKit
class ViewController: UIViewController {
let slider : UISlider = {
let slider = UISlider(frame: .zero)
slider.maximumValue = 255
slider.minimumValue = 0.0
return slider
}()
var gradientLayer = CAGradientLayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(slider)
slider.translatesAutoresizingMaskIntoConstraints = false
slider.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
slider.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
slider.widthAnchor.constraint(equalToConstant: 200).isActive = true
slider.heightAnchor.constraint(equalToConstant: 20).isActive = true
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
gradientLayer.bounds = self.view.bounds
self.view.layer.insertSublayer(gradientLayer, at: 0)
}
#objc func sliderValueChanged(sender: UISlider) {
let currentValue = CGFloat(sender.value)
self.view.addColorGradient(value: currentValue, gradientLayer: gradientLayer)
}
}
Just change the topColor and bottomColor as you need and manipulate their values with the slider. Hope it helps
class ViewController: UIViewController {
var gradientLayer = CAGradientLayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
gradientLayer.bounds = self.view.bounds
self.view.layer.insertSublayer(gradientLayer, at: 0)
}
// Slider Input
#IBAction func sliderValueChanged(_ sender: UISlider) {
self.view.addColorGradient(value: CGFloat(sender.value), gradientLayer: gradientLayer)
}
}
After adding a gradient layer to the statusbar and make it light or dark for different view controllers, at some random point in navigation, my clock in the status bar of notch iPhones has become truncated. like 1... or ...
I have tried these two solutions for make statusbar content white; But this has no effect on this random clock behaviour.
this:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.barStyle = .blackOpaque
self.setNeedsStatusBarAppearanceUpdate()
}
or this:
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
Both of them changes my status bar color of content. Any idea about what is cause of this behavior???
and this is how I make the status bar gradient:
extension UIViewController {
func makeStatusBarGradient(){
let statusBarView = UIView(frame: UIApplication.shared.statusBarFrame)
let gradientLayer1 = CAGradientLayer()
gradientLayer1.frame = statusBarView.frame
gradientLayer1.colors = [UIColor.APColors.redGradient.cgColor, UIColor.APColors.orangeGradient.cgColor]
gradientLayer1.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer1.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer1.cornerRadius = 0
gradientLayer1.zPosition = -10
gradientLayer1.name = "gradient"
//Change status bar color
if let statusBar = UIApplication.shared.value(forKey: "statusBar") as? UIView{
statusBar.layer.addSublayer(gradientLayer1)
}
setNeedsStatusBarAppearanceUpdate()
}
func clearStatusBarGradient(){
if let statusBar = UIApplication.shared.value(forKey: "statusBar") as? UIView{
if let layers = statusBar.layer.sublayers?.filter({$0.name == "gradient"}){
if (layers.count > 0){
layers.first?.removeFromSuperlayer()
}
}
}
setNeedsStatusBarAppearanceUpdate()
}
}
and, the APColors are:
extension UIColor {
struct APColors {
static var redGradient : UIColor { return UIColor(red: 1.00, green: 0.42, blue: 0.24, alpha: 1) }
static var orangeGradient : UIColor { return UIColor(red: 0.95, green: 0.19, blue: 0.42, alpha: 1)}
}
}
You are using extension on UILabel, which override it's intrinsicContentSize. (or maybe you swizzling this method) So status bar clock label trigger it and got wrong intrinsicContentSize. Try to fix textSize calculation in this overrided method, or avoid overriding it if possible.
I can guess you're using UILabel+Padding from popular SO answer, so check if you've got this:
if let insets = padding {
insetsWidth += insets.left + insets.right
insetsHeight += insets.top + insets.bottom
textWidth -= insetsWidth
}
if you do, then simply change it:
if let insets = padding {
insetsWidth += insets.left + insets.right
insetsHeight += insets.top + insets.bottom
textWidth -= insetsWidth
} else {
return contentSize
}
As i do not know what's the APColor, so I changed the line with below & everything is working fine.
gradientLayer1.colors = [UIColor.red.cgColor, UIColor.orange.cgColor]
Try to change the same line and check your issue is solved or not. Most probably, APColor does not caused the issue.
Check is their any multiple stuffs is in you statusbar is available or not. e.g. indicator etc.
If you added anything else in your status bar then let me know.
Output
I have gradient(clear - black) applied to an imageview and on top of it I have a float button. Everything works fine. The only issue is, whenever there is a tap on float button, the gradient start increasing to black more and more. my gradient is clear to black from top to bottom. But on interaction, It start to slowely blacken towards upside.
I am really unable to solve this error.
This image view is in a UIcollectionResuableView. Below is the code for yhe following.
func addBlackGradientLayerprof(frame: CGRect){
let gradient = CAGradientLayer()
gradient.frame = frame
let black = UIColor.init(red: 0/255, green: 0/255, blue: 0/255, alpha: 0.65).cgColor
gradient.colors = [UIColor.clear.cgColor, black]
gradient.locations = [0.0, 1.0]
self.layer.addSublayer(gradient)
}
Header View with floating button:
override func layoutSubviews() {
super.layoutSubviews()
profilePic.addBlackGradientLayerprof(frame: profilePic.bounds)
}
override func awakeFromNib() {
super.awakeFromNib()
layoutFAB()
}
func layoutFAB() {
floaty.openAnimationType = .slideDown
floaty.addItem("New Post", icon: UIImage(named: "photo-camera")) { item in
self.delegate.fabUploadClicked()
}
floaty.addItem("Settings", icon: UIImage(named: "settingsB")) { item in
self.delegate.fabSettingsClicked()
}
floaty.paddingY = (frame.height - 30) - floaty.frame.height/2
floaty.fabDelegate = self
floaty.buttonColor = UIColor.white
floaty.hasShadow = true
floaty.size = 45
addSubview(floaty)
}
layoutSubviews is bad place to do anything other than adjust a few frames as needed. It can be called many times in the lifecycle of a view. You should not be adding layers from layoutSubviews.
You should call addBlackGradientLayerprof from a place guaranteed to only be called once in the lifetime of the object. awakeFromNib would be one possible place.
I'm trying to make a line (so basically UIView) that has fixed height and width and is divided to nine segments. I want to be able to control the height of each segment and its color. E.g. I want the first segment be yellow and 30% of the total height of the line, the second to be red and 8% of the total height etc.
I'm not really skilled in Swift, so my solution would be to make 9 UIViews, stack them on top of each other on my storyboard and then manually set the height and background color of every view, so they'd seem like a one multicolored line. Is there cleaner and less bulky solution? Thanks
I would highly recommend using Core Graphics for this.
As the drawing is dead simple (you just want to stack some colored lines within a view), you can easily achieve this by subclassing UIView and overriding drawRect() and drawing them in Core Graphics.
It's certainly a much cleaner solution than adding 9 subviews!
Something like this should achieve the desired result:
class LineView : UIView {
let colors:[UIColor] = [UIColor.redColor(), UIColor.blueColor(), UIColor.greenColor()]
let values:[CGFloat] = [0.35, 0.45, 0.2]
override func drawRect(rect: CGRect) {
let r = self.bounds // the view's bounds
let numberOfSegments = values.count // number of segments to render
let ctx = UIGraphicsGetCurrentContext() // get the current context
var cumulativeValue:CGFloat = 0 // store a cumulative value in order to start each line after the last one
for i in 0..<numberOfSegments {
CGContextSetFillColorWithColor(ctx, colors[i]) // set fill color to the given color
CGContextFillRect(ctx, CGRectMake(0, cumulativeValue*r.size.height, r.size.width, values[i]*r.size.height)) // fill that given segment
cumulativeValue += values[i] // increment cumulative value
}
}
}
Going further...
You could allow the colors and values properties to be changed from outside the LineView class, allowing for much greater flexibility. You just have to override the didSet to trigger the view to be redrawn when the properties change.
For example:
class LineView : UIView {
/// An array of optional UIColors (clearColor is used when nil is provided) defining the color of each segment.
var colors : [UIColor?] = [UIColor?]() {
didSet {
self.setNeedsDisplay()
}
}
/// An array of CGFloat values to define how much of the view each segment occupies. Should add up to 1.0.
var values : [CGFloat] = [CGFloat]() {
didSet {
self.setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
let r = self.bounds // the view's bounds
let numberOfSegments = values.count // number of segments to render
let ctx = UIGraphicsGetCurrentContext() // get the current context
var cumulativeValue:CGFloat = 0 // store a cumulative value in order to start each line after the last one
for i in 0..<numberOfSegments {
CGContextSetFillColorWithColor(ctx, colors[i]?.CGColor ?? UIColor.clearColor().CGColor) // set fill color to the given color if it's provided, else use clearColor
CGContextFillRect(ctx, CGRectMake(0, cumulativeValue*r.size.height, r.size.width, values[i]*r.size.height)) // fill that given segment
cumulativeValue += values[i] // increment cumulative value
}
}
}
Usage:
let lineView = LineView(frame: CGRectMake(50, 50, 20, view.bounds.size.height-100))
lineView.colors = [
UIColor(red: 1.0, green: 31.0/255.0, blue: 73.0/255.0, alpha: 1.0), // red
UIColor(red:1.0, green: 138.0/255.0, blue: 0.0, alpha:1.0), // orange
UIColor(red: 122.0/255.0, green: 108.0/255.0, blue: 1.0, alpha: 1.0), // purple
UIColor(red: 0.0, green: 100.0/255.0, blue: 1.0, alpha: 1.0), // dark blue
UIColor(red: 100.0/255.0, green: 241.0/255.0, blue: 183.0/255.0, alpha: 1.0), // green
UIColor(red: 0.0, green: 222.0/255.0, blue: 1.0, alpha: 1.0) // blue
]
lineView.values = [0.15, 0.1, 0.35, 0.15, 0.1, 0.15]
view.addSubview(lineView);
(I've only added 6 colors here, but you can add as many as you want).
Full project: https://github.com/hamishknight/Color-Segment-Line-View
I've just realized that this was not what you needed.
I leave the answer anyway so that maybe could be helpful to somebody else in the future.
Make sure your line view has it's own UIView subclass, so that we can override drawRect and achieve your goal.
Then a simple implementation would be:
class BarLine: UIView {
override func drawRect(rect: CGRect) {
//Height of each segment, in percentage
var heights : [CGFloat] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
//Lets create 9 rects and set each rect width to be 1/9th of the view size, then add them to the array
let width : CGFloat = rect.size.width / 9.0
var i : Int = Int()
//Loop to generate 9 segmnets
for (i = 0; i < 9; i++) {
//Each rect origin must be translated by i * width
let origin = CGPointMake(CGFloat(i) * width, rect.height)
//Generate a random color
let color = UIColor(red: heights[i], green: 0.5, blue: 0.5, alpha: 1)
let segment = CGRect(x: origin.x, y: origin.y, width: width, height: -heights[i] * rect.height)
//Set the color
color.set()
//Add the segment to the view by drawing it
UIRectFill(segment)
}
}
}
This will produce something like :
(Remember to set your UIView class to you custom class in IB)
I hope this helped
To make #Hamish code compatible for Swift 5, here the LineView class (and invert width & height in draw->fill to make it horizontal):
import UIKit
public class ColorLineView : UIView {
public var colors : [UIColor?] = [UIColor?]() {
didSet {
self.setNeedsDisplay()
}
}
public var values : [CGFloat] = [CGFloat]() {
didSet {
self.setNeedsDisplay()
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override func draw(_ rect: CGRect) {
let r = self.bounds
let numberOfSegments = values.count
guard let ctx = UIGraphicsGetCurrentContext() else { return }
var cumulativeValue:CGFloat = 0
for i in 0..<numberOfSegments {
ctx.setFillColor(colors[i]?.cgColor ?? UIColor.clear.cgColor)
ctx.fill(CGRect(x: 0, y: cumulativeValue*r.size.height, width: r.size.width, height: values[i]*r.size.height))
cumulativeValue += values[i]
}
}
}
So I'm working on a iOS application written in Swift and I came across this exact same issue as this:
CAGradientLayer not resizing nicely.
I add a CAGradientLayer to my UIView and I override willAnimateRotationToInterfaceOrientation in order to update the gradient bounds. This only seems to update AFTER the rotation is complete and thus you see the bounds of the original frame when rotating (which causes the user to see the white background of my app).
Here is my code:
in viewDidLoad, I have the following code:
var color1: UIColor = UIColor(red: CGFloat(0.12), green: CGFloat(0.13), blue: CGFloat(0.70), alpha: CGFloat(1.0))
var color2: UIColor = UIColor(red: CGFloat(0.00), green: CGFloat(0.59), blue: CGFloat(1.00), alpha: CGFloat(1.0))
var color3: UIColor = UIColor(red: CGFloat(0.00), green: CGFloat(1.00), blue: CGFloat(1.00), alpha: CGFloat(1.0))
let gradient : CAGradientLayer = CAGradientLayer()
let arrayColors: [AnyObject] = [color1.CGColor, color2.CGColor, color3.CGColor]
gradient.frame = self.view.bounds
gradient.colors = arrayColors
view.layer.insertSublayer(gradient, atIndex: 0)
and I override this function:
override func willAnimateRotationToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {
var gradient:CAGradientLayer = self.view.layer.sublayers[0] as CAGradientLayer
gradient.frame = self.view.bounds
}
The solution is seems to be this:
// GradientView.h
#interface GradientView : UIView
#property (nonatomic, strong, readonly) CAGradientLayer *layer;
#end
// GradientView.m
#implementation GradientView
+ (Class)layerClass {
return [CAGradientLayer class];
}
#end
But I'm having a really hard time converting this class to Swift. Can someone help me? So far I have this:
import Foundation
import UIKit
class GradientView:UIView
{
var gradientLayer:CAGradientLayer = CAGradientLayer()
func layerClass() -> CAGradientLayer {
return gradientLayer.self
}
}
But it doesn't seem to work. I'm not understanding what would be the Swift equivalent of the 'class' message being used in [CAGradientLayer class]?
I've been trying to make this work for a few hours now and I just can't seem to get it. Any help would be highly appreciated!
Thanks
You need to override the layerClass() class method, and return a class:
class GradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
}
At some point, you will also want to set your gradientLayer property to self.layer.