How to create the shadow of material-like table view cells? - ios

I am trying to make my table view cells look "material". Here is something similar to what I want to do (source):
Note that there is a shadow around the whole table view in the above image. What I want is that shadow, but applied to each table view cell, instead of the whole table view.
I first designed my cell in an XIB file. I put a UIView called containerView as a subview of the content view. I added constraints so that the containerView has a top, bottom, left, right margin of 8. This is so that the containerView is a little smaller than the content view, so that the shadow I put on it will be visible.
I also added a UILabel called label as the subview of containerView to show some text.
This is the UITableViewCell subclass:
class QueueItemCell: UITableViewCell {
#IBOutlet var label: UILabel!
#IBOutlet var container: UIView!
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
...
}
override func setSelected(_ selected: Bool, animated: Bool) {
...
}
override func awakeFromNib() {
container.layer.shadowColor = UIColor.black.cgColor
container.layer.shadowOpacity = 0.7
container.layer.shadowOffset = CGSize(width: 3, height: 9)
container.layer.shadowRadius = 4
container.layer.cornerRadius = 4
container.layer.shadowPath = UIBezierPath(roundedRect: container.bounds, cornerRadius: 4).cgPath
selectionStyle = .none
}
}
There is nothing special about the data source and delegate methods except that I set the cells' height to 61 in heightForRowAt.
When I run the app, I got something like this:
The shadow on the bottom and left edges are quite good. But the right edge is a total disaster. The top edge also does not have a shadow, which is undesirable. I tried to do trial and error with shadowPath and shadowOffset but there's always one or two edges that looks bad.
How can I achieve a shadow on all edges of the cell, as shown in the first image?

in awakeFromNib you have wrong view size. You need to move container.layer.shadowPath = UIBezierPath(roundedRect: container.bounds, cornerRadius: 4).cgPath into layoutSubviews
or remove this code
container.layer.shadowPath = UIBezierPath(roundedRect: container.bounds, cornerRadius: 4).cgPath
so shadow will be configured automatically

Related

Swift UITableViewCell Shadow not appearing

I am trying to create custom table view cell which works fine in my other UIViewControllers. However, in one of my controllers, the shadow is not growing, I can barely see the shadow.
Here is an image of the shadow being shown in red, you can see it is barely visible.
My cell has a UIView added inside the contentView to creating floating cell effects - the same code and same storyboard layouts are being used across my controllers but this is the only table view where the shadow issue is occurring - so I must be missing something.
My addShadow extension:
extension UIView {
func addShadow(offset: CGSize, color: UIColor, radius: CGFloat, opacity: Float) {
layer.masksToBounds = false
layer.shadowOffset = offset
layer.shadowColor = color.cgColor
layer.shadowRadius = radius
layer.shadowOpacity = opacity
}
}
My awakeFromNib on the custom cell:
:: cellContentView is my UIView added to the base contentView of the cell.
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = .clear
self.selectionStyle = .none
cellContentView?.layer.masksToBounds = true
cellContentView?.round(corners: [.topLeft, .topRight, .bottomLeft, .bottomRight], radius: 10)
cellContentView?.addShadow(offset: CGSize(width: 40, height: 60), color: UIColor.red, radius: 10, opacity: 1)
cellContentView?.layer.shouldRasterize = true
}
Note: The .round is an extension being used on all my cells.
No matter what radius or offset I add for this shadow, it does not get bigger than the image. Also, none of my other cells in the their controllers require the shouldRasterize property to be set, but this does.
Does anyone know what is happening here?
Thanks :)
Edit
Strangely, if I add constraints around my view to keep the gaps large between my view and the cell content view, the background colour disappears - this is set to white in the storyboard.
You should call in the layoutSubviews method. because shadow should add after the view is uploaded
override func awakeFromNib() {
super.awakeFromNib()
//init methods
}
override public func layoutSubviews() {
super.layoutSubviews()
//Added shadow
self.reloadLayers()
}
private func reloadLayers() {
self.layer.cornerRadius = 5
self.addShadow(.TransactionCell)
}
I hope it helps
Content view will fill you cell, so you need to add shadow to view inside content view which has all your components inside it. Then add constraints to it with gap between that view and content view. Second, 40 and 60 properties for shadow is likely too large, when I said too large I mean unbelievable large, because gap between content views in cells are no more than 15 - 30 even less. so try it with much less values, while radius can remain 10 but you will see what value fit the best. If cell content view is your custom view just values will did the job if your view is not kind of transparent or any inside it, in that case it won't, and there is hard to fix that, I tried many libraries and custom codes and it is never ok.
squircleView.layer.cornerRadius = 40
squircleView.layer.cornerCurve = CALayerCornerCurve.continuous
squircleView.layer.shadowColor = UIColor.systemGray.cgColor
squircleView.layer.shadowOpacity = 0.7
squircleView.layer.shadowOffset = CGSize(width: 0, height: 0.5)
squircleView.layer.shadowRadius = 5

How to correctly round edges of a table view cell with dynamic height

I am having a problem with one of my table views. I am writing a messaging page for my app that uses a table view to display the messages sent and received. The table cells need to change height based on each cells content. I have the sizing working correctly but I now need to round the cells edges to fit the UI design. The way that I have done this in the past with non-dynamic heights is by calling a function to round each corner in the override function "layoutSubViews()" in the tableViewCell:
func roundAllCorners(radius: CGFloat) {
let allCorners: UIRectCorner = [.topLeft, .bottomLeft, .bottomRight, .topRight]
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: allCorners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
If I try calling this function but the cell is dynamically sized then the left edge cuts off half a centimeter. If you scroll the cell off screen and back again though it fixes it. Hope you can find a solution to my problem, has been a pain in the neck for a while. Thanks.
It might be you also need to override the setter for frame and call it in there. Any any case this is not a good idea for multiple reasons. The thing is that table view cell has many views (including itself being a view) like content view and background view...
I suggest that you add yet another view on the content view which holds all your cell views. Then make this view a subclass and handle all the rounding in there. So from the storyboard perspective you would have something like:
- UITableViewCell
- contentView
- roundedContainer
- imageView
- button
- label
...
The rounded view has (or should have) constraints so layoutSubViews should be enough to override for setting up corner radius.
You can have a neat class you can use to round your view like:
class RoundedView: UIView {
#IBInspectable var cornerRadius: CGFloat = 0.0 {
didSet {
refresh()
}
}
#IBInspectable var fullyRounded: Bool = false {
didSet {
refresh()
}
}
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
private func refresh() {
layer.cornerRadius = fullyRounded ? min(bounds.width, bounds.height) : cornerRadius
}
}
As already mentioned by #iDeveloper it might be better to use cornerRadius of a layer. But if you need to use a shape layer you can do that as well in this class.
Make sure to clip bounds on this view.
Make sure you RELOAD THE TABLEVIEW after calling your function
yourTableView.reloadData()
You can use self sizing table view cell according to the content. Now you can follow the previous implementation for rounded corner cell.
Place the below code inside viewDidLoad.
tableView.estimatedRowHeight = YourEstimatedTableViewHeight
tableView.rowHeight = UITableViewAutomaticDimension
Note: You have to give the top and bottom constraints to the content properly.
For detailed implementation you can follow self-sizing-table-view-cells

How to change the height of the separator in UITableView Swift

This question has been around for a while, but this is a slightly different variation. I want to increase the height of the separator in the UITableView so it REALLY increases the height and doesn't just take up space within the cell. I am also putting a radius on the cell as well.
The code I have to customise the cell is below. I have also tried a variant in cellForRowAtindexPath adding a view to each cell, which has the same effect. They only take up space within the cell, rather than replacing the separator with a new view.
I want the space between the cells to be 20pt and clear color.... is this possible?
class CustomTVC: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
let mScreenSize = UIScreen.main.bounds
let mSeparatorHeight = CGFloat(20.0) // Change height of speatator as you want
let mAddSeparator = UIView.init(frame: CGRect(x: 0, y: self.frame.size.height - mSeparatorHeight, width: mScreenSize.width, height: mSeparatorHeight))
mAddSeparator.backgroundColor = UIColor.orange // Change backgroundColor of separator
self.addSubview(mAddSeparator)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// THIS ADDS THE CORNER RADIUS
self.layer.cornerRadius = 20
self.layer.masksToBounds = true
}
What we usually do is embed cell's subviews in a view and constrain this view so that it is 10px from top and bottom of the contentview. Make sure to se tableView's backgound color to .clear.

How to add padding to left/right of a static cell in UITableView swift

I'm using a UITableViewController with static cells and I want to make it so that the cells do not take up the entirety of the view. I want something more akin to this image: https://i.stack.imgur.com/4nO09.png
I can't quite figure out how to do so. I've been able to change the height thanks to self.tableView.contentInset, but I'm not sure how to change the width.
How would I do this?
EDIT:
Here's my Code for Fay007 as well as an image.
import UIKit
class ContactFormViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Sets the background images
if let patternImage = UIImage(named: "Pattern") {
view.backgroundColor = UIColor(patternImage: patternImage)
}
}
override func viewWillAppear(_ animated: Bool) {
let numberOfRows: CGFloat = CGFloat(self.tableView.numberOfRows(inSection: 0))
let headerHeight: CGFloat = (self.view.frame.size.height - (self.tableView.rowHeight * numberOfRows)) / numberOfRows
self.tableView.contentInset = UIEdgeInsetsMake(headerHeight, 0, -headerHeight, 0)
}
http://imgur.com/a/Q2kfH
The first image I linked has its cells away from the left/right edges, as in my comment I explained I believe they did using autolayout. Since the tableview is a subview of the UIView of the UIViewController, I believe one would be able to assure that. however, when using a UITableViewController, which is required to use static cells in a UITableView, there is no UIView parent.
One easy way of doing this is as follows: In storyboard, or interface builder, add a UIView subview to the UITableviewcell. Create constraints that define your desired distance of this subview to the edges of the cell.
To add rounded corners, you can do so within awakeFromNib by setting the cornerRadius of the subview's layer property to your desired radius.

Mask in UITableViewCell subclass not properly rendering on first load

I am trying to create a UITableViewCell subclass containing two rounded views, one on top and one on bottom, that together end up as a rounded rectangular view inside the cell, with indented space on all 4 sides (set by auto layout constrains in the storyboard for the prototype cell). These cells are part of a tableview that is loaded into a UIContainerView which has its contents swapped out based on the selection of a selection control.
Here is what I want the cell to look like (blacked out):
Here is what it looks like briefly, when first loading:
Here is what it looks like after it first loads:
When I switch to a different tab, then come back, it renders the cell correctly.
I use this method in the parent view controller (adapted from this)
func cycleFromViewController(oldViewController: UIViewController, toViewController newViewController: UIViewController) {
oldViewController.willMoveToParentViewController(nil)
self.addChildViewController(newViewController)
self.addSubView(newViewController.view, toView:self.containerView!)
newViewController.view.alpha = 0
newViewController.view.layoutIfNeeded()
UIView.animateWithDuration(0.25, animations: {
newViewController.view.alpha = 1
oldViewController.view.alpha = 0
},
completion: { finished in
oldViewController.view.removeFromSuperview()
oldViewController.removeFromParentViewController()
newViewController.didMoveToParentViewController(self)
})
}
The parent view controller's viewDidLoad method is called like this:
override func viewDidLoad() {
... // grab data in a background network call, populating the array of model objects
self.currentSelectedViewController!.view.translatesAutoresizingMaskIntoConstraints = false
self.addChildViewController(self.currentSelectedViewController!)
self.addSubView(self.currentSelectedViewController!.view, toView: self.containerView)
self.refreshContainerView()
super.viewDidLoad()
}
refreshContainerView looks like this:
func refreshContainerView() {
let currentVC = self.currentSelectedViewController as! MyTableViewController
currentVC.modelObjectList = self.modelObjectList
self.label.hidden = true
self.button.hidden = true
currentVC.tableView.reloadData()
}
Here is my cell's layout subviews method:
override func layoutSubviews() {
super.layoutSubviews()
self.reminderView.backgroundColor = UIColor.grayColor()
if let aModel = self.model {
self.configureWithModel(aModel)
}
self.setMaskToView(self.topView, corners: UIRectCorner.TopLeft.union(UIRectCorner.TopRight))
self.setMaskToView(self.bottomView, corners: UIRectCorner.BottomLeft.union(UIRectCorner.BottomRight))
}
Any thoughts as to how to fix
1. the initial brief loading without the insets and
2. the final rendering of the initial load with the rounded corners on the right side not properly rendering?
This cell exists in a storyboard as a prototype, with the insets created via auto layout constraints. (a constant setting the top and bottom view's distance from the top, bottom, right and left as appropriate). Clearly these constraints work when the cell is reloaded, but not on the initial load for some reason that is escaping me.
Evidently the answer was fairly simple. The mask method was being called in layoutSubviews for the cell, the the views themselves did not yet have their bounds set. So I subclassed the view into a new RoundedView class, and added a var for the corners and a modified mask method:
class RoundedView: UIView {
var corners : UIRectCorner = []
override func layoutSubviews() {
self.setMaskForCorners(corners)
}
func setMaskForCorners(corners: UIRectCorner) {
let rounded = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 10, height: 10))
let mask = CAShapeLayer()
mask.path = rounded.CGPath
self.layer.mask = mask
}
}
Then I changed the views to be that subclass and then call it like this:
self.topView.corners = UIRectCorner.TopLeft.union(UIRectCorner.TopRight)
self.bottomView.corners = UIRectCorner.BottomLeft.union(UIRectCorner.BottomRight)

Resources