UITableViewCell: rounded corners and shadow - swift

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

Related

Add shadowPath to UITableView subclass

I want to create a shadow on a subview on my UITableView:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
mainBackground = UIView()
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
mainBackground.backgroundColor = .red
mainBackground.translatesAutoresizingMaskIntoConstraints = false
mainBackground.layer.cornerRadius = 15
mainBackground.layer.masksToBounds = true
contentView.addSubview(mainBackground)
setupConstraints()
}
func setupConstraints() {
NSLayoutConstraint.activate([
mainBackground.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
mainBackground.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
mainBackground.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
mainBackground.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.5)
])
}
I'm placing my shadow code in layoutSubviews, where I also use UIEdgeInsets to make my subview smaller:
override func layoutSubviews() {
super.layoutSubviews()
let margins = UIEdgeInsets(top: 5, left: 8, bottom: 5, right: 8)
contentView.frame = contentView.frame.inset(by: margins)
contentView.layer.cornerRadius = 8
setupShadow()
}
public func setupShadow() {
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowRadius = 3
self.layer.shadowOpacity = 1.0
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowPath = UIBezierPath(rect: self.mainBackground.frame).cgPath
self.clipsToBounds = true
}
Without the shadowPath this seems to work, but with it I don't get a shadow at all. I want to use a shadowPath to make the drawing of the shadow efficient. How do I add the shadowPath here?
I'm sorry I don't quite understand what kind of result do you expect to achieve.
I don't think it's possible to get any kind of "shadow" for a UITableViewCell because usually shadows are outside/around the view, but table view cells stay next to each other, without any gap in between. So any shadow of the first cell will be overlapped by the second cell.
self.layer.shadowPath = UIBezierPath(rect: self.mainBackground.frame).cgPath
here you add a shadow to the cell, but the shadow is overlapped by mainBackgroundView so the shadow is invisible
try that:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
mainBackground.backgroundColor = .red
mainBackground.translatesAutoresizingMaskIntoConstraints = false
mainBackground.layer.cornerRadius = 15
mainBackground.layer.shadowOffset = CGSize(width: 0, height: 3)
mainBackground.layer.shadowRadius = 3
mainBackground.layer.shadowOpacity = 1.0
mainBackground.layer.shadowColor = UIColor.black.cgColor
contentView.addSubview(mainBackground)
}
override func layoutSubviews() {
super.layoutSubviews()
mainBackground.frame = contentView.bounds.insetBy(dx: 20, dy: 20)
mainBackground.layer.shadowPath = UIBezierPath(rect: self.mainBackground.bounds).cgPath
}
and increase the height of your cells to ~100pt
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
100
}
and you will be able to play with a shadow applied to mainBackgroundView
Try this,
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()
}

Setting toValue for CAShapeLayer animation from UICollectionView cellForItemAt indexPath

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
}

Corner are not round of UILabel and UIbutton ios swift

I am trying to round three corners of label and button by following this:
rounded button
But the result is:
Image of issue
some corners of labels are rounding and some are not.Similarly its happening with button and width is exceeding and button going out of tableview.
this is the code of extension i am using:
extension UIButton{
func roundedButton(){
let maskPAth1 = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.topLeft, .bottomLeft, .bottomRight],
cornerRadii:CGSize(width:8,height:8))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = self.bounds
maskLayer1.masksToBounds=true
maskLayer1.path = maskPAth1.cgPath
self.layer.mask = maskLayer1
}
}
extension UILabel{
func roundedLabel(){
let maskPAth1 = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.topRight,.bottomRight,.bottomLeft],
cornerRadii:CGSize(width:10,height:10))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = self.bounds
maskLayer1.cornerRadius=5
maskLayer1.masksToBounds=true
maskLayer1.path = maskPAth1.cgPath
self.layer.mask = maskLayer1
}
}
I am calling these function in
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}
That happens because bounds of these labels and buttons are most probably different when you call rounded...() methods and when they are presented in a table view.
That's because the bounds value that you use to create the UIBezierPath is different from one that these views have after they are presented in the cell. Table view layouts a cell and if the CAShapeLayer you create exceeds its bounds, it gets "cut off".
You should create a subclasses of UIButton and UILabel that would update the bezier path of their masks every time they layout.
class RoundedLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
let maskPath = UIBezierPath(roundedRect: bounds,
byRoundingCorners: [.topRight, .bottomRight, .bottomLeft],
cornerRadii: CGSize(width: 10, height: 10))
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.cornerRadius = 5
maskLayer.masksToBounds = true
maskLayer.path = maskPath.cgPath
layer.mask = maskLayer
}
}
As further optimization you could lazily initialize the mask layer (meaning it would be created when it's accessed for the first time) and only change it's frame and path in the layoutSubviews method.
class RoundedLabel: UILabel {
private lazy var maskLayer: CAShapeLayer = {
let maskLayer = CAShapeLayer()
maskLayer.cornerRadius = 5
maskLayer.masksToBounds = true
self.layer.mask = maskLayer
return maskLayer
}()
override func layoutSubviews() {
super.layoutSubviews()
let maskPath = UIBezierPath(roundedRect: bounds,
byRoundingCorners: [.topRight, .bottomRight, .bottomLeft],
cornerRadii: CGSize(width: 10, height: 10))
maskLayer.path = maskPath.cgPath
maskLayer.frame = bounds
}
}

Adding shadow to tableview cell cause laggy scroll and duplicate shadow

I have added white space around my tableview cell and every time I scroll this shadows getting bigger and bigger when I scroll, and its get lag when I scroll for the second and third time with the bigger shadow
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
cell.backgroundColor = UIColor.clear
cell.contentView.backgroundColor = UIColor.clear
let whiteRoundedView : UIView = UIView(frame: CGRect(x:10,y: 5,width: self.view.frame.size.width - 20,height: 117))
whiteRoundedView.layer.backgroundColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1.0, 1.0, 1.0, 0.9])
//whiteRoundedView.layer.masksToBounds = true
whiteRoundedView.layer.cornerRadius = 5.0
whiteRoundedView.layer.shadowOffset = CGSize(width: -1,height: 1)
whiteRoundedView.layer.shadowOpacity = 0.2
let shadowPath = UIBezierPath(rect: whiteRoundedView.layer.bounds)
whiteRoundedView.layer.shouldRasterize = true
whiteRoundedView.layer.shadowPath = shadowPath.cgPath
cell.contentView.addSubview(whiteRoundedView)
cell.contentView.sendSubview(toBack: whiteRoundedView)
return cell
}
Just put code inside the awakefrom nib
class CustomCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.backgroundColor = UIColor.clear
self.contentView.backgroundColor = UIColor.clear
let whiteRoundedView : UIView = UIView(frame: CGRect(x:10,y: 5,width: self.contentView.frame.size.width - 20,height: 117))
whiteRoundedView.layer.backgroundColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1.0, 1.0, 1.0, 0.9])
//whiteRoundedView.layer.masksToBounds = true
whiteRoundedView.layer.cornerRadius = 5.0
whiteRoundedView.layer.shadowOffset = CGSize(width: -1,height: 1)
whiteRoundedView.layer.shadowOpacity = 0.2
let shadowPath = UIBezierPath(rect: whiteRoundedView.layer.bounds)
whiteRoundedView.layer.shouldRasterize = true
whiteRoundedView.layer.shadowPath = shadowPath.cgPath
self.contentView.addSubview(whiteRoundedView)
self.contentView.sendSubview(toBack: whiteRoundedView)
}
}
You keep adding shadow views on top of each other without ever removing them. If all your cells will need the shadow you can just add a tag and check if a view with that tag already exists like so:
whiteRoundedView.tag = 12345
if cell.contentView.viewWithTag(12345) == nil {
cell.contentView.addSubview(whiteRoundedView)
}
If some cells have the shadow but some cells don't you could do it like this:
if let shadowView = cell.contentView.viewWithTag(12345) && noShadow {
shadowView.removeFromSuperview
} else if !noShadow {
cell.contentView.addSubview(whiteRoundedView)
}
Alternatively like mentioned in the comments of the question you would add it to your custom cell class:
override func awakeFromNib() {
super.awakeFromNib()
let whiteRoundedView : UIView = UIView(frame: CGRect(x:10,y: 5,width: self.contentView.frame.size.width - 20,height: 117))
whiteRoundedView.layer.backgroundColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1.0, 1.0, 1.0, 0.9])
whiteRoundedView.layer.cornerRadius = 5.0
whiteRoundedView.layer.shadowOffset = CGSize(width: -1,height: 1)
whiteRoundedView.layer.shadowOpacity = 0.2
let shadowPath = UIBezierPath(rect: whiteRoundedView.layer.bounds)
whiteRoundedView.layer.shouldRasterize = true
whiteRoundedView.layer.shadowPath = shadowPath.cgPath
self.contentView.addSubview(whiteRoundedView)
}
You are adding whiteRoundedView every time. If you are using a storyboard or Nib for designing your Cell UI then you can add whiteRoundedView there Or you can add a tag in whiteRoundedView and check if there are already any view added with that tag before adding the whiteRoundedView as subview.

Adding rounded corner and drop shadow to UICollectionViewCell

So I already went through various posts on adding 2nd view for adding shadow, but I still cannot get it to work if I want to add it in UICollectionViewCell. I subclassed UICollectionViewCell, and here is my code where I add various UI elements to the cell's content view and adding shadow to the layer:
[self.contentView setBackgroundColor:[UIColor whiteColor]];
self.layer.masksToBounds = NO;
self.layer.shadowOffset = CGSizeMake(0, 1);
self.layer.shadowRadius = 1.0;
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowOpacity = 0.5;
[self.layer setShadowPath:[[UIBezierPath bezierPathWithRect:self.bounds] CGPath]];
I would like to know how to add rounded corner and shadow to UICollectionViewCell.
Neither of these solutions worked for me. If you place all your subviews into the UICollectionViewCell content view, which you probably are, you can set the shadow on the cell's layer and the border on the contentView's layer to achieve both results.
cell.contentView.layer.cornerRadius = 2.0f;
cell.contentView.layer.borderWidth = 1.0f;
cell.contentView.layer.borderColor = [UIColor clearColor].CGColor;
cell.contentView.layer.masksToBounds = YES;
cell.layer.shadowColor = [UIColor blackColor].CGColor;
cell.layer.shadowOffset = CGSizeMake(0, 2.0f);
cell.layer.shadowRadius = 2.0f;
cell.layer.shadowOpacity = 0.5f;
cell.layer.masksToBounds = NO;
cell.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:cell.bounds cornerRadius:cell.contentView.layer.cornerRadius].CGPath;
Swift 3.0
self.contentView.layer.cornerRadius = 2.0
self.contentView.layer.borderWidth = 1.0
self.contentView.layer.borderColor = UIColor.clear.cgColor
self.contentView.layer.masksToBounds = true
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 2.0)
self.layer.shadowRadius = 2.0
self.layer.shadowOpacity = 0.5
self.layer.masksToBounds = false
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.contentView.layer.cornerRadius).cgPath
Swift 3 version:
cell.contentView.layer.cornerRadius = 10
cell.contentView.layer.borderWidth = 1.0
cell.contentView.layer.borderColor = UIColor.clear.cgColor
cell.contentView.layer.masksToBounds = true
cell.layer.shadowColor = UIColor.gray.cgColor
cell.layer.shadowOffset = CGSize(width: 0, height: 2.0)
cell.layer.shadowRadius = 2.0
cell.layer.shadowOpacity = 1.0
cell.layer.masksToBounds = false
cell.layer.shadowPath = UIBezierPath(roundedRect:cell.bounds, cornerRadius:cell.contentView.layer.cornerRadius).cgPath
In case it helps: Here is the swift for rounding the corners:
cell.layer.cornerRadius = 10
cell.layer.masksToBounds = true
with cell being a variable controlling the cell: often, you will use this in override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
Enjoy!
Here's my answer, close to the others, but I add a corner radius to the layer otherwise the corners won't fill in correctly. Also, this makes a nice little extension on UICollectionViewCell.
extension UICollectionViewCell {
func shadowDecorate() {
let radius: CGFloat = 10
contentView.layer.cornerRadius = radius
contentView.layer.borderWidth = 1
contentView.layer.borderColor = UIColor.clear.cgColor
contentView.layer.masksToBounds = true
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 1.0)
layer.shadowRadius = 2.0
layer.shadowOpacity = 0.5
layer.masksToBounds = false
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath
layer.cornerRadius = radius
}
}
You can call it in collectionView(_:cellForItemAt:) of the datasource once you dequeue your cell.
Here the Swift 4 solution, updated to round every corners and not only the top corners:
contentView.layer.cornerRadius = 6.0
contentView.layer.borderWidth = 1.0
contentView.layer.borderColor = UIColor.clear.cgColor
contentView.layer.masksToBounds = true
layer.shadowColor = UIColor.lightGray.cgColor
layer.shadowOffset = CGSize(width: 0, height: 2.0)
layer.shadowRadius = 6.0
layer.shadowOpacity = 1.0
layer.masksToBounds = false
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: contentView.layer.cornerRadius).cgPath
layer.backgroundColor = UIColor.clear.cgColor
Set the layer attributes for the cell, not contentView.
CALayer * layer = [cell layer];
[layer setShadowOffset:CGSizeMake(0, 2)];
[layer setShadowRadius:1.0];
[layer setShadowColor:[UIColor redColor].CGColor] ;
[layer setShadowOpacity:0.5];
[layer setShadowPath:[[UIBezierPath bezierPathWithRect:cell.bounds] CGPath]];
SWIFT 4.2
One should add this in your custom cell or cellForItemAt:
If you are using the cellForItemAt: approach substitute self -> cell
self.layer.cornerRadius = 10
self.layer.borderWidth = 1.0
self.layer.borderColor = UIColor.lightGray.cgColor
self.layer.backgroundColor = UIColor.white.cgColor
self.layer.shadowColor = UIColor.gray.cgColor
self.layer.shadowOffset = CGSize(width: 2.0, height: 4.0)
self.layer.shadowRadius = 2.0
self.layer.shadowOpacity = 1.0
self.layer.masksToBounds = false
This will give you a cell with a rounded border and a drop shadow.
You simply need to (a) set cornerRadius and (b) set shadowPath to be a rounded rect with the same radius as cornerRadius:
self.layer.cornerRadius = 10;
self.layer.shadowPath = [[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.layer.cornerRadius] CGPath];
Here's my take on the solution. It's similar to other answers, with one key difference. It doesn't create a path that's dependent on the bounds of the view. Anytime you create a path based on the bounds and provide it to the layer you can run into issues when it's resizing, and need to setup methods to update the path.
A simpler solution is to avoid using anything that is bounds dependent.
let radius: CGFloat = 10
self.contentView.layer.cornerRadius = radius
// Always mask the inside view
self.contentView.layer.masksToBounds = true
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 1.0)
self.layer.shadowRadius = 3.0
self.layer.shadowOpacity = 0.5
// Never mask the shadow as it falls outside the view
self.layer.masksToBounds = false
// Matching the contentView radius here will keep the shadow
// in sync with the contentView's rounded shape
self.layer.cornerRadius = radius
Now when ever the cells size change the view API will do all the work internally.
I had to make some slight changes for Swift:
cell.contentView.layer.cornerRadius = 2.0;
cell.contentView.layer.borderWidth = 1.0;
cell.contentView.layer.borderColor = UIColor.clearColor().CGColor;
cell.contentView.layer.masksToBounds = true;
cell.layer.shadowColor = UIColor.grayColor().CGColor;
cell.layer.shadowOffset = CGSizeMake(0, 2.0);
cell.layer.shadowRadius = 2.0;
cell.layer.shadowOpacity = 1.0;
cell.layer.masksToBounds = false;
cell.layer.shadowPath = UIBezierPath(roundedRect:cell.bounds, cornerRadius:cell.contentView.layer.cornerRadius).CGPath;
I use the following method to accomplish this kind of effect:
extension UICollectionViewCell {
/// Call this method from `prepareForReuse`, because the cell needs to be already rendered (and have a size) in order for this to work
func shadowDecorate(radius: CGFloat = 8,
shadowColor: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3),
shadowOffset: CGSize = CGSize(width: 0, height: 1.0),
shadowRadius: CGFloat = 3,
shadowOpacity: Float = 1) {
contentView.layer.cornerRadius = radius
contentView.layer.borderWidth = 1
contentView.layer.borderColor = UIColor.clear.cgColor
contentView.layer.masksToBounds = true
layer.shadowColor = shadowColor.cgColor
layer.shadowOffset = shadowOffset
layer.shadowRadius = shadowRadius
layer.shadowOpacity = shadowOpacity
layer.masksToBounds = false
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath
layer.cornerRadius = radius
}
}
The answer of Mike Sabatini works fine, if you configure the cell properties directly on the collectionView cellForItemAt, but if you try to set them in awakeFromNib() of a custom UICollectionViewCell subclass, you will end with a wrong bezierPath set on the devices that doesn't match the width and height previously set in your Storyboard (IB).
Solution for me was create a func inside the subclass of UICollectionViewCell and calling it from the cellForItemAt like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellID", for: indexPath) as? CustomCollectionViewCell{
cell.configure())
return cell
}
else {
return UICollectionViewCell()
}
}
And on the CustomCollectionViewCell.swift :
class CustomCollectionViewCell: UICollectionViewCell{
func configure() {
contentView.layer.cornerRadius = 20
contentView.layer.borderWidth = 1.0
contentView.layer.borderColor = UIColor.clear.cgColor
contentView.layer.masksToBounds = true
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 2.0)
layer.shadowRadius = 2.0
layer.shadowOpacity = 0.5
layer.masksToBounds = false
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: contentView.layer.cornerRadius).cgPath}
}
This one 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
You can set shadow color, radius and offset in UICollectionViewDataSource method while creating a UICollectionViewCell
cell.layer.shadowColor = UIColor.gray.cgColor
cell.layer.shadowOffset = CGSize(width: 0, height: 2.0)
cell.layer.shadowRadius = 1.0
cell.layer.shadowOpacity = 0.5
cell.layer.masksToBounds = false
I found an important thing: the UICollectionViewCell must have the backgroundColor as clear color in order to make these above work.
Swift 5, Xcode 13, iOS 14
First config your collection as below:
self.collectionView.clipsToBounds = false
Then config your cell same as below:
override func awakeFromNib() {
super.awakeFromNib()
self.configView()
}
private func configView() {
self.clipsToBounds = false
self.backgroundColor = .systemBackground
self.layer.cornerRadius = 10
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 0.0)
self.layer.shadowRadius = 10
self.layer.shadowOpacity = 0.2
}
Note to those two "clipToBounds = false" commands.
Just that.