Swift - Most code-efficient way to reuse gradients? - ios

My UI calls for about 7-10 different gradients.
I have the first (default) gradient set up as followed:
let gradientLayer = CAGradientLayer()
And in my viewDidLoad:
gradientLayer.frame = self.view.bounds
gradientLayer.colors = [lightBlue, lightPurple]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.zPosition = -1
self.view.layer.addSublayer(gradientLayer)
gradientLayer is the default gradient. so let's say I want to define:
gradientVariantOne / gradientVariantTwo / gradientVariantThree (etc.)
but ALL of these new gradients will have the exact frame / locations / zPosition as gradientLayer.
How can I write my code so that these new gradients inherit those properties? This will keep the code much leaner, and easy to modify in the future.
Thank you

Actually I don't see what is gained by inheritance here. Just encapsulate the setting of the frame, locations, and zPosition into a method:
extension CAGradientLayer {
func configure(view:UIView) {
self.frame = view.bounds
self.locations = [0.0, 1.0]
self.zPosition = -1
}
}
Now you can just call configure to get the configuration of each gradient layer started. You could even add more parameters to configure (such as the colors array) and turn the whole thing into a one-liner:
extension CAGradientLayer {
func configure(view:UIView, colors:[CGColor]) {
self.frame = view.bounds
self.locations = [0.0, 1.0]
self.zPosition = -1
self.colors = colors
}
}
Thus a single call to configure configures the whole layer and you're done.

You can create a UIView extension and add your gradients by calling a method.
extension UIView {
private func prepareGradient() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
//add all common setup here
return gradientLayer
}
func addGradientVariantOne() {
let gradientLayer = prepareGradient()
gradientLayer.colors = [lightBlue, lightPurple]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.zPosition = -1
self.layer.addSublayer(gradientLayer)
}
}
Then you can simply call:
self.view.addGradientVariantOne()
Just create a similar function for each gradient.

Related

Custom UINavigationBar gradient background and image in logo

I have some problems with inheriting UINavigationBar.
First of all, I have to use the same class in multiple UINavigationBar, since everything is embedded in UITabBar. So I have a UITabBar connected to 5 different UINavigationBar.
All these UINavigationBars should have a gradient background and an image as title. I tried inheriting and setting this class to each of them, but it does not work.
class NavBar: UINavigationBar {
func setupNavBar(){
self.topItem?.titleView = UIImageView(image: UIImage(named: "image"))
self.topItem?.titleView?.contentMode = .scaleAspectFit
let gradient = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.blue.cgColor]
gradient.locations = [0.0, 1.0]
self.layer.insertSublayer(gradient, at: 0)
self.setNeedsLayout()
}
override func didMoveToSuperview() {
self.setupNavBar()
}
}
I have tried different combinations, but in most of them I cannot see the image. Also it happened to have a different behavior by calling the same class in different UINavigationBar, for instance in the first tab I can see the gradient, but if I change it then it is not there.
Please help me to solve this by using the subclass, I do not want to put the code in controllers.
Extra: I would also like to fill the status bar with gradient, but I can only fill static color
Solved by inheriting UITabBarController instead and putting gradient as backgroundImage
class NavBarController: UINavigationController {
override func viewDidLayoutSubviews() {
self.navigationBar.topItem?.titleView = UIImageView(image: UIImage(named: "image"))
self.navigationBar.topItem?.titleView?.contentMode = .scaleAspectFit
let gradient = CAGradientLayer()
var bounds = self.navigationBar.bounds
bounds.size.height += self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
bounds.origin.y -= self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
gradient.frame = bounds
gradient.colors = [UIColor.black, UIColor.white]
gradient.locations = [0.0, 1.0]
self.navigationBar.setBackgroundImage(image(fromLayer: gradient), for: .default)
}
func image(fromLayer layer: CALayer) -> UIImage {
UIGraphicsBeginImageContext(layer.frame.size)
layer.render(in: UIGraphicsGetCurrentContext()!)
let outputImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return outputImage!
}
}

Add Gradient Layer to UIImageView in IOS thru CAGradientLayer

I am trying to add a Gradient in IOS like I did in Android. So I can see my label on top of the UIImageView and its not hidden.
In android I did this in a drawable `
<gradient
android:angle="90"
android:endColor="#00ffffff"
android:startColor="#aa000000"
android:centerColor="#00ffffff" />
<corners android:radius="0dp" />
`
I am trying to do this in IOS Swift 4.2 and I get the following :
let gradient: CAGradientLayer = CAGradientLayer()
gradient.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
gradient.locations = [0.0 , 1.0]
gradient.startPoint = CGPoint(x: 0.0, y: 1.0)
gradient.endPoint = CGPoint(x: 0.5, y: 0.5)
gradient.frame = CGRect(x: 0.0, y: 0.0, width: showImageView.frame.size.width, height: showImageView.frame.size.height)
showImageView.layer.addSublayer(gradient)
How do I get the Gradient to start black from the bottom and go up in 90 degrees?How do I change the opacity?Any idea?
A few thoughts:
Choose colors with alpha less than 1. Perhaps:
gradient.colors = [
UIColor.black.withAlphaComponent(0.8).cgColor,
UIColor.black.withAlphaComponent(0).cgColor
]
To have this gradient go vertically, choose start and end points that have the same x value. E.g. to cover bottom half, perhaps:
gradient.startPoint = CGPoint(x: 0.5, y: 1)
gradient.endPoint = CGPoint(x: 0.5, y: 0.5)
Be very careful about using frame. You want the layer to reference the bounds of the image view (using the image view’s coordinate system), not the frame (which specifies where the image view is in its superview’s coordinate system). If your image view happens to be at 0, 0, you might not see a difference, but if you move the image view around at all, these will start to deviate. So, assuming you’re adding this gradient to the image view, itself, you’d use the image view’s bounds:
gradient.frame = showImageView.bounds
Be aware that the frame/bounds of the image view can sometimes change. So, we will implement layoutSubviews in our UIImageView or UITableViewCell subclass, and update the gradient’s frame there, e.g.
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = bounds
}
That way it will update the gradient’s frame when the view’s layout changes.
The other solution is to define a UIView subclass, say GradientView, that renders the CAGradientLayer and define the base layerClass of that view to be a CAGradientLayer. Then add this view in between the image view and the label, define its constraints, and then you’ll have a gradient that changes dynamically as the GradientView size changes. (This layerClass approach has the advantage that it yields better animation/transitions than you’d get by just updating the frame programmatically.)
Thus, something like:
class GradientView: UIView {
override class var layerClass: AnyClass { return CAGradientLayer.self }
var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
var firstColor: UIColor = UIColor.black.withAlphaComponent(0.8) {
didSet { updateColors() }
}
var secondColor: UIColor = UIColor.black.withAlphaComponent(0) {
didSet { updateColors() }
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
}
private extension GradientView {
func configure() {
updateColors()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 1)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.5)
}
func updateColors() {
gradientLayer.colors = [firstColor, secondColor].map { $0.cgColor }
}
}
If you really want your white text to pop, in addition to adding gradient over the image, you might also add a black glow/shadow to the text. It’s subtle, but really makes the text pop. Just make its shadow color the same color as the gradient.
So you can see the effect, here are four renditions of a cell, with (a) no gradient; (b) with gradient; (c) with gradient and nice gaussian blur around text; and (d) with simple shadow around text:
The nice, gaussian blur around the text is rendered with:
customLabel.layer.shadowColor = UIColor.black.cgColor
customLabel.layer.shadowRadius = 3
customLabel.layer.shadowOpacity = 1
customLabel.layer.masksToBounds = false
customLabel.layer.shouldRasterize = true
I think that third rendition (with gradient over the image, with glow around the text) is best. It’s subtle, but the text really pops. But gaussian blurs are computationally expensive, so if you find this adversely affects your performance too much, you can use the fourth option, with the simple, non-blurred shadow:
customLabel.shadowColor = .black
// perhaps also
// customLabel.shadowOffset = CGSize(width: -1, height: -1)

gradient keeps switching between cells in collection view in swift

I have a gradient background for my collection view cell and each gradient is different depending on what tag the cell is (e.g Christmas is a purple gradient, birthday is a green gradient) however when scrolling through the collection view, the gradients keep on changing cells. is there a way to fix this from happening?
this is the code I use to set the gradient to the background view. it is in cellForItemAt
cell.mainView.setGradientBackground(colours: self.getColourFromTag(tag: self.lists[indexPath.item].tag))
.setGradientBackground is an extension of UIView which just sets a gradient to the background. shown here:
func setGradientBackground(colours: [CGColor]) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colours
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0)
layer.insertSublayer(gradientLayer, at: 0)
}
I use the method below to get what colours are in the gradient
func getColourFromTag(tag: String) -> [CGColor] {
if tag == "Christmas" {
return [Colours.gradients.festive.start.cgColor, Colours.gradients.festive.end.cgColor]
}else if tag == "Birthday" {
return [Colours.gradients.lime.start.cgColor, Colours.gradients.lime.end.cgColor]
}else if tag == "Valentines Day" {
return [Colours.gradients.strawberry.start.cgColor, Colours.gradients.strawberry.end.cgColor]
}else if tag == "Charity" {
return [Colours.gradients.blueberry.start.cgColor, Colours.gradients.blueberry.end.cgColor]
}else if tag == "Event" {
return [Colours.gradients.fire.start.cgColor, Colours.gradients.fire.end.cgColor]
}else{
return [Colours.gradients.midnight.start.cgColor, Colours.gradients.midnight.end.cgColor]
}
}
I have tried appending each [colour] into an array, then placing that into the .setGradientBackground at the indexPath.item like so:
var colours = [[CGColor]]()
colours[indexPath.item] = self.getColourFromTag(tag: self.lists[indexPath.item].tag)
cell.mainView.setGradientBackground(colours: colours[indexPath.item])
however, this does not work since it is out of range. Does anyone have a solution? thank you.
Reading your code in setGradientBackground(), you always init a new CAGradientLayer and insert it at the lowest position in your cell. Every time setGradientBackground() gets called again, the new gradient layer will be positioned below all other gradients you already inserted.
Try to get a already existing CAGradientLayer from sublayers and set the new colors.
var gradientLayer = CAGradientLayer()
if let sublayers = layer.sublayers {
for sublayer in sublayers {
if let gLayer = sublayer as? CAGradientLayer {
gradientLayer = gLayer
break
}
}
}
gradientLayer.frame = bounds
gradientLayer.colors = colours
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0)
layer.insertSublayer(gradientLayer, at: 0)
EDIT
This is a more swift like version (Swift 4.2) using compactMap() for getting the first existing gradient layer in layer.sublayers:
if let existingLayer = (layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
gradientLayer = existingLayer
}

Gradient Layer background

I am trying to create a gradient background. i have followed steps on different tutorials but cannot get my background view to appear, I just get a white background.
I cannot understand what I am doing wrong or not doing. Code is below.
Help greatly appreciated.
import UIKit
extension CAGradientLayer {
func backgroundGradient() -> CAGradientLayer {
self.colors = [UIColor.green,UIColor.blue]
self.locations = [0.0, 1.0]
self.startPoint = CGPoint(x: 0.0, y: 1.0)
self.endPoint = CGPoint(x: 1.0, y: 1.0)
return self
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let gradient = CAGradientLayer().backgroundGradient()
gradient.frame = self.view.bounds
print(gradient.frame)
print(gradient.startPoint)
self.view.layer.insertSublayer(gradient, at: 0)
}
}
Gradient layers take CGColor instead of UIColor. Easy fix...
In your extension, change the self.colors line to:
self.colors = [UIColor.green.cgColor, UIColor.blue.cgColor]

Change colors in a CAGradientLayer after inserting

I have a class function to create a CAGradient layer and insert it to a UIView which works fine.
However, I'd like to modify the gradient after the page is shown, and re-executing the same function does not work.
On the displayed page I use sliders to set new colors into two UIButton backgrounds, and call the function below. I'm expecting the gradient layer background to change when I change the slider but it doesn't. I've verified that the function is being called and the color values are valid.
How can I change the gradient as I move the sliders?
Here's the class function:
class func setGradientBG(_ firstColor: CGColor, secondColor: CGColor, vertical: Bool, thisView: UIView){
let gradientLayer = CAGradientLayer()
gradientLayer.frame = thisView.bounds
gradientLayer.colors = [firstColor, secondColor]
gradientLayer.locations = [0.0, 1.2]
if !vertical{
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
}
thisView.layer.insertSublayer(gradientLayer, at: 0)
}
And here's the function called from the sliders:
func doSlider(){
CommonFuncs.setGradientBG((self.TopColorBut.backgroundColor?.cgColor)!, secondColor: (self.BottomColorBut.backgroundColor?.cgColor)!, vertical: true, thisView: self.view)
}
You're adding another instance of CAGradientLayer every time you call setGradientBG. Do you really think that's a good idea?
It looks like your doSlider method is probably part of a custom UIViewController subclass. What you should do is store a reference to the gradient layer in the view controller, and update that existing layer. Example:
class ViewController: UIViewController {
private let gradientLayer = CAGradientLayer()
#IBOutlet var topColorButton: UIButton!
#IBOutlet var bottomColorButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
view.layer.insertSublayer(gradientLayer, at: 0)
self.updateGradientLayer()
}
#IBAction func doSlider() {
updateGradientLayer()
}
private func updateGradientLayer() {
gradientLayer.locations = [ 0.0, 1.2 ]
gradientLayer.startPoint = .zero
gradientLayer.endPoint = CGPoint(x: 0, y: 1)
gradientLayer.colors = [
(topColorButton.backgroundColor ?? .clear).cgColor,
(bottomColorButton.backgroundColor ?? .clear).cgColor ]
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
gradientLayer.frame = view.bounds
}
}
Sublayers are listed in back to front order. So insert the new sublayer at last. Or remove old ones.

Resources