gradient keeps switching between cells in collection view in swift - ios

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
}

Related

Bottom part of tab bar does not get properly colored

I want to custom color my tab bar element in my custom UITabController sub-class and it works fine when I'm doing it with:
tabBar.barTintColor = .blue (With any system or custom color)
But when I'm trying to add a gradient using my custom UIImage extension
extension UIImage {
static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
gradientLayer.masksToBounds = true
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
tabBar.barTintColor = UIColor(patternImage: UIImage.gradientImageWithBounds(bounds: tabBar.bounds, colors: [Colors.tabBarTopGradient, Colors.tabBarBottomGradient]))
I having trouble with gradient not getting properly applied to the bottom part of the tab bar around the phone's safe area. What am I missing here? It looks like that:
Please try setting the gradient tint in viewWillLayoutSubviews(). Hope that helps!
I think you create the gradient based on tab bar height, what you need is to add the additional height to the tab bar height
override func viewDidLoad() {
super.viewDidLoad()
var frame = tabBar.bounds
let safeAreaHeight = safeAreaInsets.bottom
frame.size.height = frame.size.height + safeAreaHeight
tabBar.barTintColor = UIColor(patternImage: UIImage.gradientImageWithBounds(bounds: frame, colors: [UIColor.red.cgColor, UIColor.blue.cgColor]))
}
public var safeAreaInsets: UIEdgeInsets {
guard let window: UIWindow = UIApplication.shared.windows.first else {
return .zero
}
if #available(iOS 11.0, *),
UIWindow.instancesRespond(to: #selector(getter: window.safeAreaInsets)) {
return window.safeAreaInsets
}
return .zero
}

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

Swift - Most code-efficient way to reuse gradients?

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.

Resources