Setting toValue for CAShapeLayer animation from UICollectionView cellForItemAt indexPath - swift

I have the following parts:
- My main view is a UIViewController with a UICollectionView
- The cell for the UICollectionView
- A subclass of the UIView to build a CAShapeLayer with an CABasicAnimation
In my main view I have a UICollectionView which renders a bunch of cells with labels etc. It also is showing a progress graph.
In my subclass ProgressCirclePath() I am drawing a CAShapeLayer which is acting as the progress graph rendered in each cell of my UICollectionView.
I have been able to pass data to each cell, e.g. the labels as well as the CAShapeLayer strokeEnd values.
Everything is fine until I try to add a CABasicAnimation to my path. In this case I am not able to set the value for the animations toValue. Testing using the print console reveals that the value is available in my UICollectionView but not in the animation block in my subClass (which is where it simply returns nil). I have tried simply setting the toValue as well as creating a variable within my ProgressCirlePath and setting it from the UICollectionView. Neither worked.
I'd appreciate any hints on why this is happening and how to solve this. Thanks!!
Within my UICollectionView:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = decksCollectionView.dequeueReusableCell(withReuseIdentifier: "DeckCell", for: indexPath) as! DeckCell
cell.creatorLabel.text = deckCellCreator[indexPath.item]
cell.titleLabel.text = deckCellTitle[indexPath.item]
cell.progressLabel.text = "\(deckCellCompletionPercentage[indexPath.item])%"
cell.progressGraphView.animation.toValue = CGFloat(deckCellCompletionPercentage[indexPath.item])/100
return cell
}
The setup within my cell class:
let progressGraphView: ProgressCirclePath = {
let circlePath = ProgressCirclePath(frame: CGRect(x:0, y:0, width: 86, height: 86))
circlePath.progressLayer.position = circlePath.center
circlePath.progressBackgroundLayer.position = circlePath.center
circlePath.translatesAutoresizingMaskIntoConstraints = false
return circlePath
}()
And here my ProgressCirclePath()
class ProgressCirclePath: UIView {
let progressLayer = CAShapeLayer()
let progressBackgroundLayer = CAShapeLayer()
let animation = CABasicAnimation(keyPath: "strokeEnd")
// var percentageValue = CGFloat()
override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(progressBackgroundLayer)
layer.addSublayer(progressLayer)
let circularPath = UIBezierPath(arcCenter: .zero, radius: 43, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
progressBackgroundLayer.path = circularPath.cgPath
progressBackgroundLayer.lineWidth = 10
progressBackgroundLayer.strokeStart = 0
progressBackgroundLayer.strokeEnd = 1
progressBackgroundLayer.strokeColor = UIColor(red: 221/255.0, green: 240/255.0, blue: 226/255.0, alpha: 1.0).cgColor
progressBackgroundLayer.fillColor = UIColor.clear.cgColor
progressBackgroundLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
progressLayer.path = circularPath.cgPath
progressLayer.lineWidth = 10
progressLayer.lineCap = kCALineCapRound
progressLayer.strokeColor = UIColor(red: 72/255.0, green: 172/255.0, blue: 104/255.0, alpha: 1.0).cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
animation.duration = 1
animation.fromValue = 0
// animation.toValue = percentageValue
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
progressLayer.add(animation, forKey: "animateGraph")
print("animation.toValue \(animation.toValue)")
}
required init?(coder aDecoder: NSCoder) {
fatalError("has not been implemented")
}
}

You're handling the injection of data and animation too early in the lifecycle of the custom view. Instead of handling them in the object's initializer, move them to a later and more appropriate method, such as layoutSubviews:
override open func layoutSubviews() {
super.layoutSubviews()
// handle post-init stuff here, like animations and passing in data
// when it doesn't get passed in on init
}

Related

Swift UICollectionReusableView didn't update the frame when reuse

I tried to build a multiple section with a same UICollectionReusableView as a decorationItems. But when the view is reused during the scrolling, the gradientLayer inside the ReusableView didn't updated to meet the new section size. I have no ideas what the problem. How can I reset the gradientLayer frame when the UICollectionReusableView reused.
class RoundedBackgroundView: UICollectionReusableView {
private var insetView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 6
view.clipsToBounds = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
let gradientLayer = CAGradientLayer()
// Set the size of the layer to be equal to size of the display.
gradientLayer.frame = self.bounds
// Set an array of Core Graphics colors (.cgColor) to create the gradient.
gradientLayer.colors = [UIColor(red: 246, green: 186, blue: 58).cgColor, UIColor(red: 233, green: 115, blue: 58).cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
// Rasterize this static layer to improve app performance.
gradientLayer.shouldRasterize = true
insetView.layer.addSublayer(gradientLayer)
addSubview(insetView)
NSLayoutConstraint.activate([
insetView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0),
insetView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0),
insetView.topAnchor.constraint(equalTo: topAnchor),
insetView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
For reusable views I would always recommend to make a separated view, do all the layout in that view and add this to the cell/reusableView.
Anyways, the problem is probably, that the bounds aren't set at the init-method.
A way to solve it would be adding this function to your reusable view.
override func layoutSubviews() {
super.layoutSubviews()
if insetView.bounds.height > 0 {
gradientLayer.frame = insetView.bounds
}
}

Place gradient color inside UIView in tableViewCell

I have added two views inside a stack view. I am having that stack view inside my tableViewCell. Now my stackView is placed in the centre of the content view with width and height. I want to apply gradient color inside my view which is inside my stackView .
Just look at the view below In this case I am giving view a background colour of red and green which I don't want. I want to apply gradient of four colours to this views.
enter image description here
Here is the code of a simple progress bar that I have written in Swift:
import Foundation
import Cocoa
public class GradientProgressView : NSView
{
// MARK: - Fields
private let bgLayer = CAShapeLayer()
private var progressLayer = CAShapeLayer()
private var gradientLayer = CAGradientLayer()
private var blockLayer = CAShapeLayer()
private var mValue: CGFloat = 0.0
// MARK: - Properties
public var colors: [NSColor] = [NSColor.systemGreen, NSColor.systemYellow, NSColor.sytemRed]
public var value: CGFloat
{
set
{
self.mValue = newValue
if self.mValue > 1.0
{
self.mValue = 1.0
}
if self.mValue < 0.0 || self.mValue.isNaN || self.mValue.isInfinite
{
self.mValue = 0.0
}
self.progressLayer.strokeEnd = self.mValue
}
get
{
return self.mValue
}
}
// MARK: - Overrides
public override func layout()
{
super.layout()
self.initialize()
}
// MARK: - Helper
fileprivate func initialize()
{
self.wantsLayer = true
self.layer?.masksToBounds = true // Optional
self.layer?.cornerRadius = self.bounds.height / 2.0 // Optional
self.bgLayer.removeFromSuperlayer()
self.bgLayer.contentsScale = NSScreen.scale
self.bgLayer.fillColor = NSColor.separatorColor.cgColor
self.bgLayer.path = NSBezierPath(rect: self.bounds).cgPath
self.layer?.addSublayer(self.bgLayer)
self.blockLayer.removeAllAnimations()
self.blockLayer.removeFromSuperlayer()
let path = NSBezierPath()
path.move(to: CGPoint(x: self.bounds.height / 2.0, y: self.bounds.height / 2.0))
path.line(to: CGPoint(x: self.bounds.width - self.bounds.height / 2.0, y: self.bounds.height / 2.0))
self.progressLayer.path = path.cgPath
self.progressLayer.strokeColor = NSColor.black.cgColor
self.progressLayer.fillColor = nil
self.progressLayer.lineWidth = self.bounds.height
self.progressLayer.lineCap = .round
self.progressLayer.strokeStart = 0.0
self.progressLayer.strokeEnd = 0.0
self.progressLayer.contentsScale = NSScreen.scale
self.gradientLayer.removeAllAnimations()
self.gradientLayer.removeFromSuperlayer()
self.gradientLayer.contentsScale = NSScreen.scale
self.gradientLayer.colors = self.colors.map({ $0.cgColor })
self.gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
self.gradientLayer.mask = self.progressLayer
self.gradientLayer.frame = self.bounds
self.gradientLayer.contentsScale = NSScreen.scale
self.layer?.addSublayer(self.gradientLayer)
}
}
This progress bar supports multiple colors.
Simply replace your progress bar logic with the one I posted above (or add the logic for the triangle that is shown below the progress bar).
Preview:
Create GradientLayer on your tableViewCell(collectionViewCell)
1. Create CAGradientLayer Instance on your tableViewCell
class MyCell: UITableViewCell {
let gradientLayer = CAGradientLayer()
}
2. Set the layer frame to view's bounds from layoutSublayers(of:)
If you set your layer's frame some other place, frame becomes weird.
class MyCell: UITableViewCell {
let gradientLayer = CAGradientLayer()
// Setting it from layoutSubViews also fine.
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: self.layer)
gradientLayer.frame = yourView.bounds
}
}
3. Add GradientLayer on the View.
Here is my UIView extension for adding gradient layer.
extension UIView {
func addGradient(with layer: CAGradientLayer, gradientFrame: CGRect? = nil, colorSet: [UIColor],
locations: [Double], startEndPoints: (CGPoint, CGPoint)? = nil) {
layer.frame = gradientFrame ?? self.bounds
layer.frame.origin = .zero
let layerColorSet = colorSet.map { $0.cgColor }
let layerLocations = locations.map { $0 as NSNumber }
layer.colors = layerColorSet
layer.locations = layerLocations
if let startEndPoints = startEndPoints {
layer.startPoint = startEndPoints.0
layer.endPoint = startEndPoints.1
}
self.layer.insertSublayer(layer, above: self.layer)
}
}
* Option 1 - Add it from layoutSublayers(of:)
class MyCell: UITableViewCell {
let gradientLayer = CAGradientLayer()
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: self.layer)
gradientLayer.frame = yourView.bounds
let colorSet = [UIColor(red: 255/255, green: 255/255, blue: 255/255, alpha: 1.0),
UIColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1.0)]
let location = [0.2, 1.0]
yourView.addGradient(with: gradientLayer, colorSet: colorSet, locations: location)
}
}
* Option 2 - Add it from cellForRowAt
For example, if we have cellForRowAt and like this,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "yourCellId", for: indexPath) as? MyCell {
cell.configureCell(content: someItems[indexPath.row])
}
return UITableViewCell()
}
Now, we can handle gradientLayer from configureCell(content:).
class MyCell: UITableViewCell {
let gradientLayer = CAGradientLayer()
func configureCell(content: YourType) {
let colorSet = [UIColor(red: 255/255, green: 255/255, blue: 255/255, alpha: 1.0),
UIColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1.0)]
let location = [0.2, 1.0]
yourView.addGradient(with: gradientLayer, colorSet: colorSet, locations: location)
}
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: self.layer)
gradientLayer.frame = yourView.bounds
}
}

UICollectionviewcell badge masked behind view

I am using a badge library to help with adding large amounts of badges to UICollectionViewCells however for whatever reason, the badge itself stays behind the view.
However, once I go into the view debugger, this is what I see.
This is the code for my collection cell. I've tried different values in relation to maskToBounds and clipToBounds to no avail. Along with self and contentView.
import UIKit
import BadgeControl
class cellView: UICollectionViewCell {
private var upperLeftBadge: BadgeController!
override init(frame: CGRect) {
super.init(frame: frame)
roundCorners(corners: .allCorners, radius: 10)
backgroundColor = .init(hexString: "#37495b")
clipsToBounds = false
// contentView.isOpaque = true
//isOpaque = true
layer.masksToBounds = false
upperLeftBadge = BadgeController(for: self, in: .upperLeftCorner, badgeBackgroundColor: #colorLiteral(red: 0.9674351811, green: 0.2441418469, blue: 0.4023343325, alpha: 1) , badgeTextColor: UIColor.white, borderWidth: 0, badgeHeight: 20)
upperLeftBadge.addOrReplaceCurrent(with: "1", animated: true)
upperLeftBadge.animateOnlyWhenBadgeIsNotYetPresent = true
upperLeftBadge.animation = BadgeAnimations.leftRight
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I was using this view extension on the cells view to round corners. Removing the reference to this, and rounding the corners via layer.cornerRadius = 10 resolved my problem. Not too sure why this would hide overlapping views.
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
Have you tried this in your UICollectionViewCell class?
self.bringSubviewToFront(view: upperLeftBadge.view)
for notes self is your UICollectionViewCell I mean

Gradient Layer not showing on UIButton

I am trying to create a custom UIButton that i can use throughout my application. I would like to apply a gradient as its background however the gradient is not showing up using this very simple Code.
It would be very helpful if someone could point out my mistake in the code below.
class GradientBtn: UIButton {
let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
themeConfig()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
themeConfig()
}
private func themeConfig() {
//shadow
layer.shadowOffset = CGSize.zero
layer.shadowColor = UIColor.gray.cgColor
layer.shadowOpacity = 1.0
//titletext
setTitleColor(Theme.colorWhite, for: .normal)
titleLabel?.font = UIFont(name: Theme.fontAvenir, size: 18)
//rounded corners
layer.cornerRadius = frame.size.height / 2
//gradient
gradientLayer.locations = [0.0, 1.0]
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.frame = bounds
layer.insertSublayer(gradientLayer, at: 0)
}
}
Replace
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue]
with
gradientLayer.colors = [Theme.colorlightBlue.cgColor, Theme.colorMidBlue.cgColor]
And it's always good to add
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = self.bounds
}
Sh_Khan is right about the CGColor references, but a few additional refinements:
Rather than adding the gradient as a sublayer, make it the layerClass of the UIButton. This makes animations much better (e.g. if the button changes size on device rotation).
When you do this, remove all attempts to set the frame of the gradientLayer.
In layoutSubviews, you’ll no longer need to update the gradientLayer.frame, so remove that entirely, but you do want to put your corner rounding there.
It’s extremely unlikely to ever matter, but when you set layer.cornerRadius (or any property of the view, its layer, or that layer's sublayers), you should never refer to self.frame. The frame of the view is the CGRect in the superview’s coordinate system. Only refer to the view’s bounds within the subclass; never its frame.
Thus:
class GradientBtn: UIButton {
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
themeConfig()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
themeConfig()
}
private func themeConfig() {
//shadow
layer.shadowOffset = .zero
layer.shadowColor = UIColor.gray.cgColor
layer.shadowOpacity = 1
//titletext
setTitleColor(Theme.colorWhite, for: .normal)
titleLabel?.font = UIFont(name: Theme.fontAvenir, size: 18)
//gradient
gradientLayer.locations = [0, 1]
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue].map { $0.cgColor }
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.height, bounds.width) / 2
}
}

UITableViewCell: rounded corners and shadow

I'm changing the width of a UITableViewCell so that the cell is smaller but the user can still scroll along the edges of the tableview.
override func layoutSubviews() {
// Set the width of the cell
self.bounds = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, self.bounds.size.width - 40, self.bounds.size.height)
super.layoutSubviews()
}
Then I round the corners:
cell.layer.cornerRadius = 8
cell.layer.masksToBounds = true
All good so far. Problem happens with the shadow. The bounds are masked, so the shadow obviously won't show up. I've looked up other answers but can't seem to figure out how to round the corners along the bounds and show the shadow.
cell.layer.shadowOffset = CGSizeMake(0, 0)
cell.layer.shadowColor = UIColor.blackColor().CGColor
cell.layer.shadowOpacity = 0.23
cell.layer.shadowRadius = 4
So my question – how do I reduce the width, round the corners, and add a shadow to a UITableViewCell at the same time?
Update: Trying R Moyer's answer
This question comes at a good time! I literally JUST solved this same issue myself.
Create a UIView (let's refer to it as mainBackground) inside your cell's Content View. This will contain all of your cell's content. Position it and apply necessary constraints in the Storyboard.
Create another UIView. This one will be the one with the shadow (let's refer to it as shadowLayer). Position it exactly as you did mainBackground, but behind it, and apply the same constraints.
Now you should be able to set the rounded corners and the shadows as follows:
cell.mainBackground.layer.cornerRadius = 8
cell.mainBackground.layer.masksToBounds = true
cell.shadowLayer.layer.masksToBounds = false
cell.shadowLayer.layer.shadowOffset = CGSizeMake(0, 0)
cell.shadowLayer.layer.shadowColor = UIColor.blackColor().CGColor
cell.shadowLayer.layer.shadowOpacity = 0.23
cell.shadowLayer.layer.shadowRadius = 4
However, the problem here is: calculating the shadow for every single cell is a slow task. You'll notice some serious lag when you scroll through your table. The best way to fix this is to define a UIBezierPath for the shadow, then rasterize it. So you may want to do this:
cell.shadowLayer.layer.shadowPath = UIBezierPath(roundedRect: cell.shadowLayer.bounds, byRoundingCorners: .AllCorners, cornerRadii: CGSize(width: 8, height: 8)).CGPath
cell.shadowLayer.layer.shouldRasterize = true
cell.shadowLayer.layer.rasterizationScale = UIScreen.mainScreen().scale
But this creates a new problem! The shape of the UIBezierPath depends on shadowLayer's bounds, but the bounds are not properly set by the time cellForRowAtIndexPath is called. So, you need to adjust the shadowPath based on shadowLayer's bounds. The best way to do this is to subclass UIView, and add a property observer to the bounds property. Then set all the properties for the shadow in didSet. Remember to change the class of your shadowLayer in the storyboard to match your new subclass.
class ShadowView: UIView {
override var bounds: CGRect {
didSet {
setupShadow()
}
}
private func setupShadow() {
self.layer.cornerRadius = 8
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowRadius = 3
self.layer.shadowOpacity = 0.3
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 8, height: 8)).cgPath
self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale
}
}
The accepted answer works but adding an extra subview to get this effect make little to no sense. Here is the solution that works.
1st step: Add shadow and corner radius
// do this in one of the init methods
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// add shadow on cell
backgroundColor = .clear // very important
layer.masksToBounds = false
layer.shadowOpacity = 0.23
layer.shadowRadius = 4
layer.shadowOffset = CGSize(width: 0, height: 0)
layer.shadowColor = UIColor.black.cgColor
// add corner radius on `contentView`
contentView.backgroundColor = .white
contentView.layer.cornerRadius = 8
}
2nd step: Mask to bounds in willDisplay
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// this will turn on `masksToBounds` just before showing the cell
cell.contentView.layer.masksToBounds = true
}
Bonus: Smooth scrolling
// if you do not set `shadowPath` you'll notice laggy scrolling
// add this in `willDisplay` method
let radius = cell.contentView.layer.cornerRadius
cell.layer.shadowPath = UIBezierPath(roundedRect: cell.bounds, cornerRadius: radius).cgPath
cell.layer.cornerRadius = 10
cell.layer.masksToBounds = true
To create shadow and corner for cell you need only one backView. See my example below.
You have to add backView and set leading, trailing, top, bottom constraints equal to Content view.
Put you content to backView with appropriate constraints, but be sure your content not over cover backView.
After that in your cell initialisation code add these lines:
override func awakeFromNib() {
super.awakeFromNib()
backgroundColor = Colors.colorClear
self.backView.layer.borderWidth = 1
self.backView.layer.cornerRadius = 3
self.backView.layer.borderColor = Colors.colorClear.cgColor
self.backView.layer.masksToBounds = true
self.layer.shadowOpacity = 0.18
self.layer.shadowOffset = CGSize(width: 0, height: 2)
self.layer.shadowRadius = 2
self.layer.shadowColor = Colors.colorBlack.cgColor
self.layer.masksToBounds = false
}
Don't forget to create IBOutlet for Back View.
And here the result:
I have achieved the same thing using following code.But you have place it in layoutSubviews() method of your TableViewCell subclass.
self.contentView.backgroundColor = [UIColor whiteColor];
self.contentView.layer.cornerRadius = 5;
self.contentView.layer.shadowOffset = CGSizeMake(1, 0);
self.contentView.layer.shadowColor = [[UIColor blackColor] CGColor];
self.contentView.layer.shadowRadius = 5;
self.contentView.layer.shadowOpacity = .25;
CGRect shadowFrame = self.contentView.layer.bounds;
CGPathRef shadowPath = [UIBezierPath bezierPathWithRect:shadowFrame].CGPath;
self.contentView.layer.shadowPath = shadowPath;
One alternative approach you can try, take a UIView in UITableViewCell. Set background color of UITableViewCell to clear color. Now, you can make round corners and add shadow on UIVIew. This will appear as if cell width is reduced and user can scroll along the edges of the tableView.
create a UIVIEW inside cell's content view "backView"
and add an outlet of backView to cell class
then add these lines of code to awakeFromNib()
self.backView.layer.cornerRadius = 28
self.backView.clipsToBounds = true
the corner radius depends on your design...
the add these code to cellForRowAt function
cell.layer.shadowColor = UIColor.black.cgColor
cell.layer.shadowOffset = CGSize(width: 0, height: 0)
cell.layer.shadowOpacity = 0.6
and set the cell view background color to clear color
Don't forget to add a little space between 4 sides of the cell and the backView you just added inside cell contentView in StoryBoard
hope you liked it
Regarding answer R Moyer, the solution is excellent, but the bounds are not always installed after the cellForRowAt method, so as a possible refinement of his solution, it is to transfer the call of the setupShadow() method to the LayoutSubview() for example:
class ShadowView: UIView {
var setupShadowDone: Bool = false
public func setupShadow() {
if setupShadowDone { return }
print("Setup shadow!")
self.layer.cornerRadius = 8
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowRadius = 3
self.layer.shadowOpacity = 0.3
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 8, height:
8)).cgPath
self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale
setupShadowDone = true
}
override func layoutSubviews() {
super.layoutSubviews()
print("Layout subviews!")
setupShadow()
}
}
Never use UIBezierPath , bezierPathWithRect:shadowFrame etc as its really heavy and draws a layer on top of the views and would require to reload the table view again to make sure the cells are rendering in the right way and sometimes even reloading might not help. Instead use a section header and footer which has rounded edges as required and also which is inside the storyboard which will make the scrolling and loading of table view very smooth without any rendering issues ( sometimes called missing cells and appears on scroll )
refer here how to set the different integer values for rounded corners here : Setting masked corners in Interface Builder
Just use the above values for your section header and footer.
1- Create Custom TableViewCell Class
. Paste the following code at class level right where you create IBOutlets. exerciseView is the view just inside ContentView to which you want to round.
#IBOutlet weak var exerciseView: UIView! {
didSet {
self.exerciseView.layer.cornerRadius = 15
self.exerciseView.layer.masksToBounds = true
}
}
didSet is variable observer basically. You can do this in awakeFromNib function as well as:
self.exerciseView.layer.cornerRadius = 15
self.exerciseView.layer.masksToBounds = true
Let's Assume
viewContents = its the view which contain all your views
viewContainer = Its the view which contains viewContents with leading, trailing, top, bottom all are equal to zero.
Now the idea is, we are adding the shadow to the viewContainer. And rounding the corners of the viewContents. Most important don't forget to set background color of viewContainer to nil.
Here's the code snippet.
override func layoutSubviews() {
super.layoutSubviews()
//viewContainer is the parent of viewContents
//viewContents contains all the UI which you want to show actually.
self.viewContents.layer.cornerRadius = 12.69
self.viewContents.layer.masksToBounds = true
let bezierPath = UIBezierPath.init(roundedRect: self.viewContainer.bounds, cornerRadius: 12.69)
self.viewContainer.layer.shadowPath = bezierPath.cgPath
self.viewContainer.layer.masksToBounds = false
self.viewContainer.layer.shadowColor = UIColor.black.cgColor
self.viewContainer.layer.shadowRadius = 3.0
self.viewContainer.layer.shadowOffset = CGSize.init(width: 0, height: 3)
self.viewContainer.layer.shadowOpacity = 0.3
// sending viewContainer color to the viewContents.
let backgroundCGColor = self.viewContainer.backgroundColor?.cgColor
//You can set your color directly if you want by using below two lines. In my case I'm copying the color.
self.viewContainer.backgroundColor = nil
self.viewContents.layer.backgroundColor = backgroundCGColor
}
Here's the result
Try this, it worked for me.
cell.contentView.layer.cornerRadius = 5.0
cell.contentView.layer.borderColor = UIColor.gray.withAlphaComponent(0.5).cgColor
cell.contentView.layer.borderWidth = 0.5
let border = CALayer()
let width = CGFloat(2.0)
border.borderColor = UIColor.darkGray.cgColor
border.frame = CGRect(x: 0, y: cell.contentView.frame.size.height - width, width: cell.contentView.frame.size.width, height: cell.contentView.frame.size.height)
border.borderWidth = width
cell.contentView.layer.addSublayer(border)
cell.contentView.layer.masksToBounds = true
cell.contentView.clipsToBounds = true
It works without additional views!
override func awakeFromNib() {
super.awakeFromNib()
layer.masksToBounds = false
layer.cornerRadius = Constants.cornerRadius
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
let layer = cell.layer
layer.shadowOffset = CGSize(width: 0, height: 1)
layer.shadowRadius = 2
layer.shadowColor = UIColor.lightGray.cgColor
layer.shadowOpacity = 0.2
layer.frame = cell.frame
cell.tagLabel.text = tagItems[indexPath.row].text
return cell
}
cell.layer.cornerRadius = 0.25
cell.layer.borderWidth = 0
cell.layer.shadowColor = UIColor.black.cgColor
cell.layer.shadowOffset = CGSize(width: 0, height: 0)
cell.layer.shadowRadius = 5
cell.layer.shadowOpacity = 0.1
cell.layer.masksToBounds = false
If it's useful, I have been using the code below to achieve this, which only needs to be run in cellForRowAt.
First, add an extension to UITableViewCell to enable you to create a shadow and rounded corners on a TableViewCell:
extension UITableViewCell {
func addShadow(backgroundColor: UIColor = .white, cornerRadius: CGFloat = 12, shadowRadius: CGFloat = 5, shadowOpacity: Float = 0.1, shadowPathInset: (dx: CGFloat, dy: CGFloat), shadowPathOffset: (dx: CGFloat, dy: CGFloat)) {
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
layer.shadowRadius = shadowRadius
layer.shadowOpacity = shadowOpacity
layer.shadowPath = UIBezierPath(roundedRect: bounds.insetBy(dx: shadowPathInset.dx, dy: shadowPathInset.dy).offsetBy(dx: shadowPathOffset.dx, dy: shadowPathOffset.dy), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
let whiteBackgroundView = UIView()
whiteBackgroundView.backgroundColor = backgroundColor
whiteBackgroundView.layer.cornerRadius = cornerRadius
whiteBackgroundView.layer.masksToBounds = true
whiteBackgroundView.clipsToBounds = false
whiteBackgroundView.frame = bounds.insetBy(dx: shadowPathInset.dx, dy: shadowPathInset.dy)
insertSubview(whiteBackgroundView, at: 0)
}
}
Then just reference this in cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourCellIdentifier", for: indexPath) as! YourCellClass
cell.addShadow(backgroundColor: .white, cornerRadius: 13, shadowRadius: 5, shadowOpacity: 0.1, shadowPathInset: (dx: 16, dy: 6), shadowPathOffset: (dx: 0, dy: 2))
// Or if you are happy with the default values in your extension, just use this instead:
// cell.addShadow()
return cell
}
Here is the result for me:
Screenshot
Since iOS 13 you just need to use the style "UITableViewStyleInsetGrouped".