Add shadowPath to UITableView subclass - swift

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()
}

Related

Adjusting UILabels size (i.e., width and height) inside a UITableViewCell Class?

I created a custom UITableViewCell Class as illustrated below, whereby i defined the items I would like to be displayed inside my custom cell (specifically UILabel items):
import UIKit
import ChameleonFramework
class IsectionsCustomTableViewCell: UITableViewCell {
let sectionDesignationLabelTopPadding: CGFloat = 10
let sectionDesignationLabelLeftPadding: CGFloat = 10
let sectionDesignationLabelRightPadding: CGFloat = 10
let depthOfSectionLabelTopPadding: CGFloat = 0
let depthOfSectionLabelLeftPadding: CGFloat = 10
let depthOfSectionLabelRightPadding: CGFloat = 10
let webThicknessLabelTopPadding: CGFloat = 10
let webThicknessLabelLeftPadding: CGFloat = 10
let webThicknessLabelRightPadding: CGFloat = 10
var sectionDesignationLabel: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.yellow
label.textAlignment = .left
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = UIColor(hexString: "#F27E63")
return label
}()
var depthOfSectionLabel: UILabel = {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
label.backgroundColor = UIColor.blue
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = UIColor(hexString: "#F27E63")
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(sectionDesignationLabel)
addSubview(depthOfSectionLabel)
applyAppropriateSizeAndConstraintsForCellItems()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func applyAppropriateSizeAndConstraintsForCellItems() {
sectionDesignationLabel.frame.size.width = 200
NSLayoutConstraint.activate([
sectionDesignationLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: sectionDesignationLabelLeftPadding),
sectionDesignationLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -1*sectionDesignationLabelRightPadding),
sectionDesignationLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: sectionDesignationLabelTopPadding),
depthOfSectionLabel.topAnchor.constraint(equalTo: sectionDesignationLabel.bottomAnchor, constant: depthOfSectionLabelTopPadding),
depthOfSectionLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: depthOfSectionLabelLeftPadding),
depthOfSectionLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -1 * (((self.frame.size.width)/2) + depthOfSectionLabelRightPadding)),
])
}
}
As it can be seen from the declaration of depthOfSectionLabel, whereby I stated the width and height of its frame. However, these values are not reflected when my table gets displayed (please refer to attached image). The width is displayed much more greater than the 50 I specified for this specific label?
My question is how can I adjust the widths of the various labels to be displayed inside my tableView custom Cell?

Multiple overlayed images in the Navigation bar title

I know how to center a single image in UINavigationBar but no idea how to do that with dynamic number of images. I have a chat app that supports group chats. The number of people in a group chat could be as little as 3 but there's no upper limit.
In the UINavigationBar, I have to set the title to show at least 4 or 5 overlayed images (but not more than that since it looks odd in the UINavigationBar) and a UILabel showing how many more users are in the same group chat (ie + 15 more). The title (all the images and the label) should be centered in the UINavigationBar. The images are being downloaded from the server.
When the user taps on the title (any of the images or the label in the UINavigationBar) it should trigger an action to show a full list of the users in a separate UIViewController
The number of the overlayed images is dynamic (based on each group chat) but I can't figure out how to do this. Here's what the image of what the end result should look like:
Has anyone done this before or have an idea how to accomplish this? Help is very much appreciated
UPDATE:
I've tried to accomplish this with UIStackView but I have multiple problems. Here's the code:
var navStackView : UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.backgroundColor = .red
stack.alignment = .fill
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
var images = ["1", "2", "3", "4"]
override func viewDidLoad() {
super.viewDidLoad()
let navController = navigationController!
navController.navigationBar.addSubview(navStackView)
// x, y, w, h
navStackView.leadingAnchor.constraint(equalTo: navController.navigationBar.leadingAnchor).isActive = true
navStackView.trailingAnchor.constraint(equalTo: navController.navigationBar.trailingAnchor).isActive = true
navStackView.topAnchor.constraint(equalTo: navController.navigationBar.topAnchor).isActive = true
navStackView.bottomAnchor.constraint(equalTo: navController.navigationBar.bottomAnchor).isActive = true
for image in images {
let imageView = UIImageView()
imageView.image = UIImage(named: image)
imageView.layer.cornerRadius = imageView.bounds.height / 2
imageView.clipsToBounds = true
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
// imageView.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
navStackView.addArrangedSubview(imageView)
navStackView.layoutIfNeeded()
}
navigationItem.titleView = navStackView
}
Here's the result so far (I'm stuck though, not sure how to accomplish it):
I'm not sure about stackView. But for a simple implementation I've used collectionView. Check the below strategy. You should be able to modify accordingly per your requirement.
import UIKit
class OverlayCell: UICollectionViewCell {
func didplay(with number: String) {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 40.0, height: 40.0))
view.backgroundColor = UIColor.blue
view.layer.cornerRadius = 20.0
view.layer.masksToBounds = true
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 2.0
let label = UILabel(frame: CGRect(x: 2, y: 2, width: view.bounds.width - 4, height: view.bounds.height - 4))
label.textColor = .white
label.text = number
label.textAlignment = .center
view.addSubview(label)
contentView.addSubview(view)
contentView.transform = CGAffineTransform(scaleX: -1, y: 1)
}
}
class OverlayedView: UIView {
var mainView: UIView!
var imageCollection: UICollectionView!
//Static for now
let cellWidth: CGFloat = 40.0
let cellHeight: CGFloat = 40.0
var collectionWidth: CGFloat = 115.0
override init(frame: CGRect) {
super.init(frame: frame)
loadNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadNib()
}
private func loadNib() {
if let view = Bundle.main.loadNibNamed("OverlayedView", owner: self, options: nil)?.first as? UIView {
mainView = view
mainView.frame = self.bounds
self.backgroundColor = .black
addSubview(view)
}
}
var dataSource = ["4","3","2","1"]
func loadData() {
//dynamically calculate collectionWidth to be able to kepp it in center
collectionWidth = dataSource.count >= 4 ? CGFloat(dataSource.count) * cellWidth - CGFloat((dataSource.count - 1) * 15) : CGFloat(dataSource.count) * cellWidth - CGFloat((dataSource.count - 1) * 15) //CGFloat(dataSource.count * 15) here is the item spacing from delegate -15 inward so that we can get overlapping effect
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
imageCollection = UICollectionView(frame: CGRect(x: 0, y: 0, width: collectionWidth, height: self.bounds.height), collectionViewLayout: layout)
imageCollection.center = mainView.center
imageCollection.register(OverlayCell.self, forCellWithReuseIdentifier: "Cell")
//flip the collectionView so that it loads from right to left for overlapping effect
imageCollection.transform = CGAffineTransform(scaleX: -1, y: 1)
imageCollection.delegate = self
imageCollection.dataSource = self
mainView.addSubview(imageCollection)
}
}
extension OverlayedView: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if dataSource.count > 4 {
return 4
}
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! OverlayCell
cell.didplay(with: dataSource[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let size = CGSize(width: 40.0 , height: 40.0)
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return -15.0
}
}
Usage:
let navOverlay = OverlayedView(frame: CGRect(x: 0, y: 0, width: 250.0, height: 44.0))
navOverlay.loadData() . //pass your data to this method
navigationItem.titleView = navOverlay
I've figured it out finally. Not sure if this is the right way to accomplish it, but it's a way to accomplish it and it works great. The thing to notice - I have to calculate the navStackView width based on the number of images we have. More than 5-6 images gets too crouded, so, no more than 5 images.
The navStackView.spacing also is calculated based the width and the space you'd like between the images.
var navStackView : UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .fill
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
var moreLabel: UILabel = {
let label = UILabel()
label.text = "+ 5 more"
label.textColor = .black
label.textAlignment = .left
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var images = ["1", "2", "3", "4", "3", "3"]
override func viewDidLoad() {
super.viewDidLoad()
let navController = navigationController!
navController.navigationBar.addSubview(navStackView)
// x, y, w, h
navStackView.widthAnchor.constraint(equalToConstant: 95).isActive = true
navStackView.centerYAnchor.constraint(equalTo: navController.navigationBar.centerYAnchor).isActive = true
navStackView.heightAnchor.constraint(equalToConstant: 35).isActive = true
navStackView.centerXAnchor.constraint(equalTo: navController.navigationBar.centerXAnchor).isActive = true
// image height = 35, image width = 35
// when subtracting spacing from NavStackView, we need to subtrack from the width as well for (items - 1)
switch images.count {
case 0:
print("0 images")
case 1:
changeNavStackWidth(constant: 60, spacing: 0)
moreLabel.isHidden = true
case 2:
changeNavStackWidth(constant: 80, spacing: 10)
moreLabel.isHidden = true
case 3:
changeNavStackWidth(constant: 95, spacing: -5)
moreLabel.isHidden = true
case 4:
changeNavStackWidth(constant: 110, spacing: -10)
moreLabel.isHidden = true
case 5:
changeNavStackWidth(constant: 95, spacing: -20)
moreLabel.isHidden = true
case 6...1000:
changeNavStackWidth(constant: 95, spacing: -20)
moreLabel.isHidden = false
default:
print("default")
}
for image in images {
let imageView = UIImageView()
imageView.image = UIImage(named: image)
imageView.layer.borderColor = UIColor.white.cgColor
imageView.layer.borderWidth = 1
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
navStackView.addArrangedSubview(imageView)
navStackView.layoutIfNeeded()
}
navController.navigationBar.addSubview(moreLabel)
// x, y ,w, h
moreLabel.leadingAnchor.constraint(equalTo: navStackView.trailingAnchor, constant: 50).isActive = true
moreLabel.topAnchor.constraint(equalTo: navStackView.topAnchor).isActive = true
moreLabel.bottomAnchor.constraint(equalTo: navStackView.bottomAnchor).isActive = true
navigationItem.titleView = navStackView
let stackTap = UITapGestureRecognizer()
stackTap.addTarget(self, action: #selector(stackTapped))
navStackView.isUserInteractionEnabled = true
navStackView.addGestureRecognizer(stackTap)
}
#objc func stackTapped() {
print("tapp")
}
func changeNavStackWidth(constant: CGFloat, spacing: CGFloat) {
navStackView.constraints.forEach { constraint in
if constraint.firstAttribute == .width {
constraint.constant = constant
}
}
navStackView.spacing = spacing
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
navStackView.subviews.forEach { $0.layer.cornerRadius = $0.frame.height / 2 }
}

Changing items within custom UITableViewCell Class

I have 2 classes - a UITableViewController and a custom UITableViewCell. I want to change the cell height for my UITableViewController, so I implement the following:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.frame.height*(1/12)
}
and it works! The cell height changes. Now, I go into my custom UITableViewCell class
import UIKit
class TableViewCell: UITableViewCell {
var time:UILabel = UILabel()
var name:UILabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) // the common code is executed in this super call
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.frame.width*0.22, height: self.frame.height))
imageView.image = UIImage(named: "Person")
imageView.contentMode = .scaleAspectFit
imageView.isUserInteractionEnabled = true
self.addSubview(imageView)
let tap = UITapGestureRecognizer(target: self, action: #selector(signIn))
imageView.addGestureRecognizer(tap)
name = UILabel(frame: CGRect(x: self.frame.width*0.22, y: 0, width: self.frame.width*0.78, height: self.frame.height*0.7))
name.textColor = UIColor.gray
name.font = UIFont(name: name.font!.fontName, size: 30)
name.adjustsFontSizeToFitWidth = true
self.addSubview(name)
time = UILabel(frame: CGRect(x: self.frame.width*0.22, y: self.frame.height*0.65, width: self.frame.width*0.78, height: self.frame.height*0.3))
time.textColor = UIColor.gray
time.font = UIFont(name: time.font!.fontName, size: 15)
time.adjustsFontSizeToFitWidth = true
self.addSubview(time)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
#objc func signIn(tap: UITapGestureRecognizer) {
let tappedImage = tap.view as! UIImageView
tappedImage.image = UIImage(named: "PersonClocked")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
So, I now edit the function inside my controller to call the cells.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.layer.borderColor = UIColor.red.cgColor
cell.layer.borderWidth = 1
cell.name.text = names[indexPath.row]
cell.time.text = times[indexPath.row]
return cell
}
I expect it to all work now! But, it doesn't. For whatever reason, the methods get called in this order...
tableview(cellForRowAt) -> TableViewCell(override init) -> tableView(heightForRowAt)'
So, when I go to run it, it looks something like below. The cell is created with Swift's/Apple's default runtime tableView, then the cell size is changed, but everything inside the cell is still the size of the original default value. I need it to be the size of the cell. Any ideas?
Note - I added a border so you could see the size of the cell compared to the items.
Frame layout can't help in creating dynamic height tables you have to use dynamic tableViewCells and create the cell's contentView with constraints to get the height change with the current content
//
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) // the common code is executed in this super call
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.frame.width*0.22, height: self.frame.height))
imageView.image = UIImage(named: "Person")
imageView.contentMode = .scaleAspectFit
imageView.isUserInteractionEnabled = true
self.addSubview(imageView)
let tap = UITapGestureRecognizer(target: self, action: #selector(signIn))
imageView.addGestureRecognizer(tap)
name = UILabel(frame: CGRect(x: self.frame.width*0.22, y: 0, width: self.frame.width*0.78, height: self.frame.height*0.7))
name.textColor = UIColor.gray
name.font = UIFont(name: name.font!.fontName, size: 30)
name.adjustsFontSizeToFitWidth = true
self.addSubview(name)
time = UILabel(frame: CGRect(x: self.frame.width*0.22, y: self.frame.height*0.65, width: self.frame.width*0.78, height: self.frame.height*0.3))
time.textColor = UIColor.gray
time.font = UIFont(name: time.font!.fontName, size: 15)
time.adjustsFontSizeToFitWidth = true
self.addSubview(time)
imageView.translatesAutoresizingMaskIntoConstraints = false
name.translatesAutoresizingMaskIntoConstraints = false
time.translatesAutoresizingMaskIntoConstraints = false
imageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor,constant: 0).isActive = true
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor,constant: 20).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 30).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 30).isActive = true
name.topAnchor.constraint(equalTo: contentView.topAnchor,constant: 20).isActive = true
name.leadingAnchor.constraint(equalTo: imageView.leadingAnchor,constant: 20).isActive = true
name.trailingAnchor.constraint(equalTo: contentView.trailingAnchor,constant: -20).isActive = true
time.topAnchor.constraint(equalTo: name.topAnchor,constant: 20).isActive = true
time.leadingAnchor.constraint(equalTo: imageView.leadingAnchor,constant: 20).isActive = true
time.trailingAnchor.constraint(equalTo: contentView.trailingAnchor,constant: -20).isActive = true
time.bottomAnchor.constraint(equalTo: contentView.bottomAnchor,constant: -20).isActive = true
}
//
put this in viewDidLoad
tableview.estimatedRowHeight = 100
tableview.rowHeight = UITableViewAutomaticDimension
and don't implement heightForRowAt method

Making UITableView drop down with stackview

I am trying to achieve a UITableView drop down when I click on a button. Initially the tableView should be hidden, and when user presses button, it should drop down. I have been trying to achieve this with a UIStackView but to no success. Maybe I am doing it wrong or maybe there is another approach do do this.
let stackView = UIStackView()
var btn: UIButton!
var myTableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
myTableView.delegate = self
myTableView.dataSource = self
myTableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 1))
myTableView.frame = CGRect(x: 0, y: 0, width: 300, height: 200)
myTableView.center = CGPoint(x: self.view.frame.width/2, y: self.view.frame.height/2)
myTableView.showsVerticalScrollIndicator = false
btn = UIButton(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 50))
btn.backgroundColor = UIColor.blue.withAlphaComponent(0.3)
btn.setTitle("DropDownMenu", for: UIControlState.normal)
btn.titleLabel?.textColor = .white
btn.titleLabel?.textAlignment = .center
btn.center = CGPoint(x: self.view.frame.width/2, y: myTableView.center.y - myTableView.frame.height/2 - btn.frame.height/2)
btn.addTarget(self, action: #selector(btnPressed), for: UIControlEvents.touchUpInside)
stackView.axis = UILayoutConstraintAxis.vertical
stackView.distribution = UIStackViewDistribution.equalSpacing
stackView.alignment = UIStackViewAlignment.center
stackView.spacing = 16.0
stackView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(btn)
stackView.addSubview(myTableView)
self.view.addSubview(stackView)
}
#objc func btnPressed() {
UIView.animate(withDuration: 1) {
self.myTableView.isHidden = !self.myTableView.isHidden
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "This is cell " + indexPath.row.description
cell.textLabel?.textColor = .black
return cell
}
I can get the tableView to disappear but with no animations. Any thoughts?
The approach I ended up taking was not going via a UIStackView but insead simply having a button that animates the tableView's frame. The frame is initially set to the width of the screen and a height of 0. When user presses button, I set the height.
UIView.animate(withDuration: 0.25, animations: {
self.menuTable.frame = CGRect(x: 0, y: (sender.center.y + sender.frame.height/2), width: self.view.frame.width, height: yourHeight)
})

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