UITableViewCell with gradient not working until first reuse of cell - ios

I want to add a gradient to part of a custom UITableViewCell. This is my gradient Code.
func addGradient() {
gradient = CAGradientLayer()
gradient.frame = CGRect(x: 0, y: 0, width: gradientView.frame.width, height: gradientView.frame.height)
gradient.colors = [
UIColor(red:0.23, green:0.24, blue:0.28, alpha:1).cgColor,
UIColor(red:0.11, green:0.11, blue:0.12, alpha:0.6).cgColor
]
gradient.locations = [0, 1]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1)
gradientView.layer.addSublayer(gradient)
}
This is my gradientView code.
func addGradientView() {
containerView.addSubview(gradientView)
gradientView.translatesAutoresizingMaskIntoConstraints = false
gradientView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
gradientView.topAnchor.constraint(equalTo: barChart.bottomAnchor).isActive = true
gradientView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
gradientView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
}
addGradient() is called in the layoutSubiviews() method. But the real height and width don't seem to be reflecting until the first reuse of the cell.

Option 1: Call your addGradient() from tableview's willDisplayCell delegate method.
public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if cell is YourCustomTableViewCell {
(cell as! YourCustomTableViewCell).addGradient()
}
}
Make sure you add the gradient once. Because of the cell reuse addGradient() may get called several times on the same cell. So better rename your function to addGradientfNeeded() and adjust its logic accordingly.
Option 2:
Instead of adding your gradient in willDisplay method, add the gradient view and the layer in the cell (once) and only update the frame of the gradient layer.
public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if cell is YourCustomTableViewCell {
(cell as! YourCustomTableViewCell).updateGradientLayerFrame()
}
}
Option 3:
Create a GradientView class and add it either in the interface builder or programatically to your cell, the gradient will resize as the view's frame changes:
public class GradientView: UIView {
private var gradientLayer: CAGradientLayer?
override public func layoutSubviews() {
super.layoutSubviews()
guard gradientLayer == nil else {
gradientLayer?.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
return
}
gradientLayer = CAGradientLayer()
gradientLayer!.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
gradientLayer!.colors = [
UIColor(red:0.23, green:0.24, blue:0.28, alpha:1).cgColor,
UIColor(red:0.11, green:0.11, blue:0.12, alpha:0.6).cgColor
]
gradientLayer!.locations = [0, 1]
gradientLayer!.startPoint = CGPoint(x: 0.5, y: 0)
gradientLayer!.endPoint = CGPoint(x: 0.5, y: 1)
self.layer.addSublayer(gradientLayer!)
}
}

Your TableViewCell does not have a frame yet. You may print the frame to check that, it will be a zero frame.
Move the addGradientView method to the layoutSubView override.
override func layoutSubviews() {
super.layoutSubviews()
addGradient()
}
Edit:
I see that you have mentioned it didn't work in layoutSubView. I believe there is some problem in the way you are calling the method.
The below code works for me
class GradientCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
print(self.frame)
addGradient()
}
func addGradient() {
let gradient = CAGradientLayer()
gradient.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
gradient.colors = [
UIColor(red:0.23, green:0.24, blue:0.28, alpha:1).cgColor,
UIColor(red:0.11, green:0.11, blue:0.12, alpha:0.6).cgColor
]
gradient.locations = [0, 1]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1)
self.layer.addSublayer(gradient)
}
}
EDIT 2:
It would be better if you move the didMoveToWindow

Create a global variable inside the cell.
private let gradientLayer: CAGradientLayer = {
let layer = CAGradientLayer()
layer.colors = [UIColor.magenta.cgColor, UIColor.black.cgColor]
layer.locations = [0.0, 1.0]
layer.cornerRadius = 5
layer.masksToBounds = true
return layer
}()
Then add this gradient layer in the initialize method where you are adding UI components.
Now you have to update the frame of the gradient layer.
override func layoutSubviews() {
super.layoutSubviews()
updateGradientFrame()
}
private func updateGradientFrame() {
gradientLayer.frame = // set your frame here...
}

Related

How to add a gradient shadow to tableView cell similar to Apple Health app?

How can I customize a tableView cell to have a gradient shadow across the bottom of the cell similar to the orange tableView cells in the Health App.
I would like to make it as close to this as possible.
This is what I currently have, I've added the separation between the cells by using a white border and added a radius of 8 for the corners.
What can I add to make the gradient?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier: String = "stockCell"
let myCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
myCell.textLabel?.textAlignment = .center
myCell.textLabel?.font = .boldSystemFont(ofSize: 18)
myCell.textLabel?.textColor = UIColor.white
myCell.layer.backgroundColor = UIColor(red:0.86, green:0.25, blue:0.25, alpha:1.0).cgColor
let item: StockModel = feedItems[indexPath.row] as! StockModel
myCell.layer.cornerRadius = 8.0
myCell.layer.masksToBounds = true
myCell.layer.borderColor = UIColor.white.cgColor
myCell.layer.borderWidth = 5.0
myCell.layer.cornerRadius = 20
myCell.clipsToBounds = true
}
I believe instead of casting a "gradient shadow" across the bottom of the cell, you just want the cell to have a background gradient from a lighter color on the top to a darker color in the bottom.
I usually use en extension of UIView for adding gradients to views:
extension UIView {
func setGradient(colors: [CGColor]) {
let gradient = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colors
self.layer.insertSublayer(gradient, at: 0)
}
}
Then, try using something like this in your code:
myCell.setGradient([cgColorLight, cgColorDark])
This sometimes doesn't work if you do some custom drawing in your view's layers, but I believe it should be okay for your needs.
You can do both horizontal and vertical gradients with the following but define the following variables:
typealias GradientPoints = (startPoint: CGPoint, endPoint: CGPoint)
enum GradientOrientation {
case topRightBottomLeft
case topLeftBottomRight
case horizontal
case vertical
var startPoint: CGPoint {
return points.startPoint
}
var endPoint: CGPoint {
return points.endPoint
}
var points: GradientPoints {
switch self {
case .topRightBottomLeft:
return (CGPoint.init(x: 0.0, y: 1.0), CGPoint.init(x: 1.0, y: 0.0))
case .topLeftBottomRight:
return (CGPoint.init(x: 0.0, y: 0.0), CGPoint.init(x: 1, y: 1))
case .horizontal:
return (CGPoint.init(x: 0.0, y: 0.5), CGPoint.init(x: 1.0, y: 0.5))
case .vertical:
return (CGPoint.init(x: 0.0, y: 0.0), CGPoint.init(x: 0.0, y: 1.0))
}
}
}
And create an extension for uiview as follows:
extension UIView{
func applyGradient(withColours colours: [UIColor], gradientOrientation orientation: GradientOrientation) {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.startPoint = orientation.startPoint
gradient.endPoint = orientation.endPoint
self.layer.insertSublayer(gradient, at: 0)
}
}
Please add these lines in your cell class.
let gradient = CAGradientLayer()
gradient.frame = self.view.bounds;
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 0, y: 1)
gradient.colors = [UIColor.green.cgColor, UIColor.white.cgColor];
self.view.layer.insertSublayer(gradient, at: 0)

Adding a dashed bottom border to table view cell overlaps with text randomly in iOS

I am using the below code to add a dashed custom bottom border to tableview cells. It is now overlapping with content randomly. Sometimes, the border is not getting displayed.
class AppTableCell: UITableViewCell {
var shapeLayer: CAShapeLayer?
var isBorderAdded = false
func isBottomBorderAdded() -> Bool {
return isBorderAdded
}
func getBottomBorderShapeLayer() -> CAShapeLayer {
return self.shapeLayer!
}
func setBottomBorderShapedLayer(_ layer: CAShapeLayer) {
self.shapeLayer = layer
}
}
The extend tableview cell from the above class and calls the below function in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method.
func addDashedBottomBorder(to cell: AppTableCell) {
let color = UIColor.init(red: 191/255, green: 191/255, blue: 191/255, alpha: 1.0).cgColor
let shapeLayer:CAShapeLayer = CAShapeLayer()
let frameSize = cell.frame.size
let shapeRect = CGRect(x: 0, y: 0, width: frameSize.width, height: 0)
shapeLayer.bounds = shapeRect
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = color
shapeLayer.lineWidth = 2.0
shapeLayer.lineJoin = CAShapeLayerLineJoin.round
shapeLayer.lineDashPhase = 3.0
shapeLayer.lineDashPattern = [9,6]
shapeLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: shapeRect.height, width: shapeRect.width, height: 0), cornerRadius: 0).cgPath
if (cell.isBorderAdded) {
cell.shapeLayer!.removeFromSuperlayer()
}
cell.shapeLayer = shapeLayer
cell.isBorderAdded = true
cell.layer.addSublayer(shapeLayer)
}
How to display dashed bottom border at the end of each cell properly?
Your are adding a sublayer in your cell's layer with a fixed position:
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height)
[...]
cell.layer.addSublayer(shapeLayer)
So it doesn't stick to your cell's bottom side.
You could update this layer's position each time the cell's height change, but my suggestion from a performance and simplicity point of view would be use autolayout.
Add a subview at the bottom of your cell, add the constraints to make it stick, and add only once (in awakeFromNib for example if it's designed in IB) the dashedLayer inside.

UITableViewCell background gradient not working at first render

I have a table view. I create my cells using xib files.
I need 1 cell(which is the current user) to be a gradient background, I created a background gradient layer but it does not fit to view. After I pull list down, it works.
You can see screenshots i have and this is my code
//In the table cell,
if data.userUniqueId != nil && userUIID != nil && data.userUniqueId! == userUIID! {
let startColor = UIColor(red: 253/255.0, green: 162/255.0, blue: 75/255.0, alpha: 1)
let endColor = UIColor(red:251/255.0, green:215/255.0, blue: 126/255.0, alpha:1)
self.setGradient(colors: [startColor.cgColor, endColor.cgColor],angle: 90)
self.layer.borderColor = UIColor.white.cgColor
self.layer.borderWidth = 2
number.textColor = UIColor.white
}
And this is gradient method
func setGradient(colors: [CGColor], angle: Float = 0) {
let gradientLayerView: UIView = UIView(frame: CGRect(x:0, y: 0, width: bounds.width, height: bounds.height))
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = gradientLayerView.bounds
gradient.colors = colors
let alpha: Float = angle / 360
let startPointX = powf(
sinf(2 * Float.pi * ((alpha + 0.75) / 2)),
2
)
let startPointY = powf(
sinf(2 * Float.pi * ((alpha + 0) / 2)),
2
)
let endPointX = powf(
sinf(2 * Float.pi * ((alpha + 0.25) / 2)),
2
)
let endPointY = powf(
sinf(2 * Float.pi * ((alpha + 0.5) / 2)),
2
)
gradient.endPoint = CGPoint(x: CGFloat(endPointX),y: CGFloat(endPointY))
gradient.startPoint = CGPoint(x: CGFloat(startPointX), y: CGFloat(startPointY))
gradientLayerView.layer.insertSublayer(gradient, at: 0)
layer.insertSublayer(gradientLayerView.layer, at: 0)
}
And also how can I remove gradient from layer for next usage of cells?
EDIT:
Table View Codes
extension RecordsViewController : UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return values.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "ScoreBoardCell", for: indexPath) as? ScoreBoardCell {
let score = self.values[indexPath.row]
cell.setData(indexPath.row, score)
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
}
The problem is this line:
gradient.frame = gradientLayerView.bounds
At the time this code runs in cellForRowAt, your gradientLayerView has not yet been fully laid out, so its bounds are not yet known. Thus, you are saddling your gradient with a frame which will not fit its view after layout does take place.
I think you can solve the problem by moving the call to cell.setData to tableView(_:willDisplay:forRowAt:), at which point the cell size should be known.
And also how can I remove gradient from layer for next usage of cells?
Well, the problem here is that your condition has no alternative:
if data.userUniqueId != nil && userUIID != nil && data.userUniqueId! == userUIID!
The question is: what if not? In that case, if the gradient layer has been added, it needs to be removed. You have provided no logic for that situation. — Also you need to be careful because what if the gradient layer has been added and your logic now causes you to add another gradient layer? Basically your whole approach fails to take account of the most fundamental fact about table view cells, namely that they are reused.
[Xcode 12 Swift 5.2]
After looking for hours nothing worked for me. Changing gradient layer's bounds in layoutSubviews() or setting a gradient layer in willDisplayCell().
so I have created a gradient layer and created an UIImage of it and added that image as background. and that worked like charm here is a sweet and simple extension to convert the Gradient layer into UIImage.
func getImageFrom(gradientLayer:CAGradientLayer) -> UIImage? {
var gradientImage:UIImage?
UIGraphicsBeginImageContext(gradientLayer.frame.size)
if let context = UIGraphicsGetCurrentContext() {
gradientLayer.render(in: context)
gradientImage = UIGraphicsGetImageFromCurrentImageContext()?.resizableImage(withCapInsets: UIEdgeInsets.zero, resizingMode: .stretch)
}
UIGraphicsEndImageContext()
return gradientImage
}
Code for making a CAGradientLayer :
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [
UIColor.init().Hex(hexString: "#EB0089").cgColor,
UIColor.init().Hex(hexString: "#FD2E59").cgColor
]
gradientLayer.cornerRadius = self.layer.cornerRadius
gradientLayer.startPoint = CGPoint(x: 0, y: 1)
gradientLayer.endPoint = CGPoint(x: 0, y: 0)
gradientLayer.frame = Button1.bounds
let GradientImage = UIImage().getImageFrom(gradientLayer: gradientLayer)
set GradientImage wherever you wanted.
You need to call this inside
var once = true
override func layoutSubviews() {
super.layoutSubviews()
if once {
let startColor = UIColor(red: 253/255.0, green: 162/255.0, blue: 75/255.0, alpha: 1)
let endColor = UIColor(red:251/255.0, green:215/255.0, blue: 126/255.0, alpha:1)
self.setGradient(colors: [startColor.cgColor, endColor.cgColor],angle: 90)
once = false
}
}
Where the cell bounds are correctly rendered , When you want to remove
cell.contentView.subviews {
if $0.tag == 200 {
$0.removeFromSuperview()
}
}
And give it a tag
let gradientLayerView: UIView = UIView(frame: CGRect(x:0, y: 0, width: bounds.width, height: bounds.height))
gradientLayerView.tag = 200
When you add anything to cell don't froget tp add it to contentView
contentView.addSubview(gradientLayerView)

UICollectionView Issue

Xcode 10 beta 5, iOS 12.0 16A5339e
I use the UICollectionView inside my app with the following code:
extension MainWindowViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return itemMenuArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let itemCell = collectionView.dequeueReusableCell(withReuseIdentifier: "main_menu_cell", for: indexPath) as? MainWindowCollectionViewCell {
let item = itemMenuArray[indexPath.row]
itemCell.refresh(item)
return itemCell
}
return UICollectionViewCell()
}
And the problem is that method .refresh executes for separate cells when I scroll my CollectionView. So the cell that is out of the screen updates every time I scroll into its direction and causes wrong cell colors sometimes. Are there any ideas how to fix that?
.refresh code (I use that in my custom cell class):
public func refresh(_ menu: Menu) {
Title.text = menu.name
if let image = menu.img {
Image.image = UIImage(named: image)
}
print(SubscriptionTitle.text!)
self.setupCellAppearance(firstColor: UIColor(hexString: (menu.firstColor)!), secondColor: UIColor(hexString: (menu.secondColor)!))
}
setupCellAppearance code:
func setupCellAppearance(firstColor: UIColor, secondColor: UIColor) {
// Setting up the gradient for each cell
clipsToBounds = true
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
gradientLayer.cornerRadius = 10.0
self.layer.insertSublayer(gradientLayer, at: 0)
// Setting up the corner radius and shadows for each cell
self.layer.shadowColor = firstColor.cgColor
self.layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
self.layer.shadowRadius = 10
self.layer.shadowOpacity = 0.5
self.layer.masksToBounds = false;
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
}
What happening is cellForItemAt reuse the cell so every time cell is reuse which might already having gradient layer with it and you are trying add a new gradient so that creating a different color. So you need to remove the layer if its already there, to remove layer set name property of CAGradientLayer with some unique name and then before inserting in layer check layer with that name already exist, if it exist remove it.
func setupCellAppearance(firstColor: UIColor, secondColor: UIColor) {
//Check gradient is already exist
for layer in self.layer.sublayers ?? [] {
if layer.name == "BGGradient" {
layer.removeFromSuperlayer()
break
}
}
// Setting up the gradient for each cell
clipsToBounds = true
let gradientLayer = CAGradientLayer()
//set name property of gradient layer
gradientLayer.name = "BGGradient"
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
gradientLayer.cornerRadius = 10.0
self.layer.insertSublayer(gradientLayer, at: 0)
// Setting up the corner radius and shadows for each cell
self.layer.shadowColor = firstColor.cgColor
self.layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
self.layer.shadowRadius = 10
self.layer.shadowOpacity = 0.5
self.layer.masksToBounds = false;
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
}

How to add gradient layer which fills the view frame exactly

In my app i'm adding the gradient layer to UIView and UIToolBar but it doesn't fill the views exactly
let gradient:CAGradientLayer = CAGradientLayer()
gradient.frame = self.vw_gradientForToolBar.bounds
gradient.colors = [hexStringToUIColor(hex: "#5d8f32").cgColor,hexStringToUIColor(hex: "#04667f").cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 0)
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, 0.0)
let img : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
self.toolBar.setBackgroundImage(img, forToolbarPosition: .any, barMetrics: .default)
vw_gradientForToolBar.layer.addSublayer(gradient)
View Hirarchy
enter image description here
It's a little tough to tell exactly what you have going on, based on the images you posted, however... This may simplify things for you.
First, keep in mind that Layers do not auto-scale, so when your tool bar changes size (different devices, device rotation, etc), you want your gradient layer to also resize. Best way to do that is to use a UIView subclass and override layoutSubviews().
So, add this class to your code:
class GradientView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override func layoutSubviews() {
let gradientLayer = layer as! CAGradientLayer
gradient.colors = [hexStringToUIColor(hex: "#5d8f32").cgColor,hexStringToUIColor(hex: "#04667f").cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
}
}
Then in your controller's viewDidLoad() function:
override func viewDidLoad() {
super.viewDidLoad()
let vwGrad = GradientView()
vwGrad.frame = toolBar.frame
vwGrad.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.toolBar.insertSubview(vwGrad, at: 0)
}
Note: you would no longer need your vw_gradientForToolBar (which, I'm assuming, is a UIView connected via #IBOutlet).

Resources