Related
I am using tableView.beginUpdates() and tableView.endUpdates() to expand/contract a cell that has shadow. When the table updates, it removes all shadows from cells and then puts them back as shown here
I have tried using willDisplayCell and also tried changing the shadow to its own view, shadow on contentView and shadow on cell, none worked.
How do i keep the shadow?
extension UIView {
func addTutShadow(shadowOpacity: Float? = nil) {
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowRadius = 12 * kHeightFactor
self.layer.shadowOpacity = shadowOpacity ?? 0.12
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell =
tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TopicListCard
cell.topic = rankedTopics[indexPath.section]
cell.backgroundColor = .clear
cell.backgroundView = UIView()
cell.selectedBackgroundView = UIView()
cell.delegate = self
cell.addTutShadow()
cell.setup()
return cell
}
var selectedIndex = -1
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if selectedIndex != indexPath.section {
selectedIndex = indexPath.section
tableView.beginUpdates()
tableView.endUpdates()
} else {
selectedIndex = -1
tableview.deselectRow(at: indexPath, animated: true)
tableView.beginUpdates()
tableView.endUpdates()
}
}
It's tough to say exactly, without seeing you full cell setup...
I whipped up a quick example of one way to do this...
Looks like this:
Data Structs
struct Topic {
var title: String = ""
var status: String = ""
var icon: String = ""
}
struct TopicCellStruct {
var topic: Topic = Topic()
var expanded: Bool = false
}
Simple gradient view - for the background
class MyGradView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
private var gLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
gLayer.colors = [
UIColor(red: 0.71, green: 0.88, blue: 1.0, alpha: 1).cgColor,
UIColor(red: 0.95, green: 0.95, blue: 1.0, alpha: 1).cgColor
]
gLayer.locations = [0, 1]
gLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
}
}
Example controller class - with some sample generated data
class ShadowTestVC: UIViewController {
var myData: [TopicCellStruct] = []
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// some sample data
let p1 = "You have correctly answered "
let p2 = "% of the questions you attempted in this topic."
var pct: Int = 5
let titles: [String] = [
"Basic Algebra",
"Isolating a Variable",
"Absolute Value",
"Solving Linear Equations",
"Solving Radical Equations",
]
let syms: [String] = [
"function",
"multiply.circle",
"ruler.fill",
"arrow.up.arrow.down",
"x.squareroot",
]
for (title, sym) in zip(titles, syms) {
pct += 4
let s: String = p1 + "\(pct)" + p2
let t = Topic(title: title, status: s, icon: sym)
let tcs = TopicCellStruct(topic: t, expanded: false)
myData.append(tcs)
}
for i in 6...9 {
pct += 4
let s: String = p1 + "\(pct)" + p2
let t = Topic(title: "Title \(i)", status: s, icon: "\(i).circle.fill")
let tcs = TopicCellStruct(topic: t, expanded: false)
myData.append(tcs)
}
// a label above the table view
let topUI = UILabel()
topUI.font = .systemFont(ofSize: 24, weight: .regular)
topUI.numberOfLines = 0
topUI.text = "This is some text to represent the UI elements above the table view."
// a gradient view for the background
let gradientBKGView = MyGradView()
[gradientBKGView, topUI, tableView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradientBKGView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
gradientBKGView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
gradientBKGView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
gradientBKGView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
topUI.topAnchor.constraint(equalTo: g.topAnchor, constant: 32.0),
topUI.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 32.0),
topUI.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -48.0),
tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 160.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -16.0),
])
tableView.backgroundColor = .clear
tableView.register(ShadowedCell.self, forCellReuseIdentifier: ShadowedCell.ident)
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
}
}
extension ShadowTestVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: ShadowedCell.ident, for: indexPath) as! ShadowedCell
c.fillData(myData[indexPath.row])
c.selectionStyle = .none
return c
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let c = tableView.cellForRow(at: indexPath) as? ShadowedCell else { return }
myData[indexPath.row].expanded.toggle()
c.isExpanded = myData[indexPath.row].expanded
UIView.animate(withDuration: 0.3, animations: {
tableView.performBatchUpdates(nil, completion: nil)
})
}
}
Example cell class - I made some guesses at your layout, and I set the shadow darker than yours (.shadowOpacity = 0.75) to make it a bit more visible.
class ShadowedCell: UITableViewCell {
public static let ident: String = "sc"
public var isExpanded: Bool = false {
didSet {
expandedConstraint.isActive = isExpanded
ivVerticalExpandedConstraint.isActive = isExpanded
ivHorizontalExpandedConstraint.isActive = isExpanded
}
}
private let titleLabel = UILabel()
private let statusLabel = UILabel()
private let theImageView = UIImageView()
private let getStartedBtn = UIButton()
private let containerView = UIView()
private let shadowView = UIView()
private var collapsedConstraint: NSLayoutConstraint!
private var expandedConstraint: NSLayoutConstraint!
private var ivVerticalCollapsedConstraint: NSLayoutConstraint!
private var ivVerticalExpandedConstraint: NSLayoutConstraint!
private var ivHorizontalCollapsedConstraint: NSLayoutConstraint!
private var ivHorizontalExpandedConstraint: NSLayoutConstraint!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
clipsToBounds = true
contentView.clipsToBounds = true
containerView.clipsToBounds = true
[shadowView, containerView, theImageView, titleLabel, statusLabel, getStartedBtn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
[titleLabel, statusLabel, getStartedBtn].forEach { v in
v.setContentHuggingPriority(.required, for: .vertical)
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
[titleLabel, statusLabel, theImageView, getStartedBtn].forEach { v in
containerView.addSubview(v)
}
contentView.addSubview(shadowView)
contentView.addSubview(containerView)
contentView.backgroundColor = .clear
self.backgroundColor = .clear
shadowView.backgroundColor = .white
titleLabel.numberOfLines = 0
titleLabel.font = .systemFont(ofSize: 20.0, weight: .regular)
statusLabel.numberOfLines = 0
statusLabel.font = .systemFont(ofSize: 18.0, weight: .regular)
getStartedBtn.setTitle("Get Started", for: [])
getStartedBtn.setTitleColor(.white, for: .normal)
getStartedBtn.setTitleColor(.lightGray, for: .highlighted)
getStartedBtn.backgroundColor = .black
getStartedBtn.layer.cornerRadius = 12
let g = contentView.layoutMarginsGuide
collapsedConstraint = titleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0)
expandedConstraint = getStartedBtn.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16.0)
ivVerticalCollapsedConstraint = theImageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5.0)
ivVerticalExpandedConstraint = theImageView.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor, constant: 0.0)
ivHorizontalCollapsedConstraint = theImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16.0)
ivHorizontalExpandedConstraint = theImageView.leadingAnchor.constraint(equalTo: statusLabel.trailingAnchor, constant: 16.0)
collapsedConstraint.priority = .required - 2
expandedConstraint.priority = .required - 1
ivVerticalCollapsedConstraint.priority = .required - 2
ivVerticalExpandedConstraint.priority = .required - 1
ivHorizontalCollapsedConstraint.priority = .required - 2
ivHorizontalExpandedConstraint.priority = .required - 1
NSLayoutConstraint.activate([
shadowView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
shadowView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
shadowView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
shadowView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
containerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16.0),
titleLabel.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.6),
titleLabel.heightAnchor.constraint(equalToConstant: 60.0),
statusLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12.0),
statusLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16.0),
statusLabel.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.6),
theImageView.widthAnchor.constraint(equalToConstant: 50.0),
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor),
getStartedBtn.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 32.0),
getStartedBtn.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16.0),
getStartedBtn.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16.0),
collapsedConstraint, ivVerticalCollapsedConstraint, ivHorizontalCollapsedConstraint,
])
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0.0, height: 3.0)
shadowView.layer.shadowOpacity = 0.75
shadowView.layer.cornerRadius = 16.0
}
public func fillData(_ t: TopicCellStruct) {
titleLabel.text = t.topic.title
statusLabel.text = t.topic.status
if let img = UIImage(systemName: t.topic.icon) {
theImageView.image = img
}
isExpanded = t.expanded
}
}
Give that a try... it should avoid the shadow issue you were seeing. Then compare my approach to yours.
As a side note: I'm sure you already noticed in your development... while the design is very nice with transparent cells and table view, the expand/collapse process looks a bit "quirky" as the cells that were "out of view" (below the bottom of the table view frame) don't really "slide up" along with the other cells.
Edit -- a couple of very minor changes to improve the "glitchy" lower-cells-animation...
All classes*
struct Topic {
var title: String = ""
var status: String = ""
var icon: String = ""
}
struct TopicCellStruct {
var topic: Topic = Topic()
var expanded: Bool = false
}
class ShadowedCell: UITableViewCell {
public static let ident: String = "sc"
private let titleLabel = UILabel()
private let statusLabel = UILabel()
private let theImageView = UIImageView()
private let getStartedBtn = UIButton()
private let containerView = UIView()
private let shadowView = UIView()
private var collapsedConstraint: NSLayoutConstraint!
private var expandedConstraint: NSLayoutConstraint!
private var ivVerticalCollapsedConstraint: NSLayoutConstraint!
private var ivVerticalExpandedConstraint: NSLayoutConstraint!
private var ivHorizontalCollapsedConstraint: NSLayoutConstraint!
private var ivHorizontalExpandedConstraint: NSLayoutConstraint!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
clipsToBounds = true
contentView.clipsToBounds = true
containerView.clipsToBounds = true
[shadowView, containerView, theImageView, titleLabel, statusLabel, getStartedBtn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
[titleLabel, statusLabel, getStartedBtn].forEach { v in
v.setContentHuggingPriority(.required, for: .vertical)
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
[titleLabel, statusLabel, theImageView, getStartedBtn].forEach { v in
containerView.addSubview(v)
}
contentView.addSubview(shadowView)
contentView.addSubview(containerView)
contentView.backgroundColor = .clear
self.backgroundColor = .clear
shadowView.backgroundColor = .white
titleLabel.numberOfLines = 0
titleLabel.font = .systemFont(ofSize: 20.0, weight: .regular)
statusLabel.numberOfLines = 0
statusLabel.font = .systemFont(ofSize: 18.0, weight: .regular)
getStartedBtn.setTitle("Get Started", for: [])
getStartedBtn.setTitleColor(.white, for: .normal)
getStartedBtn.setTitleColor(.lightGray, for: .highlighted)
getStartedBtn.backgroundColor = .black
getStartedBtn.layer.cornerRadius = 12
let g = contentView.layoutMarginsGuide
collapsedConstraint = titleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0)
expandedConstraint = getStartedBtn.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16.0)
ivVerticalCollapsedConstraint = theImageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5.0)
ivVerticalExpandedConstraint = theImageView.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor, constant: 0.0)
ivHorizontalCollapsedConstraint = theImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16.0)
ivHorizontalExpandedConstraint = theImageView.leadingAnchor.constraint(equalTo: statusLabel.trailingAnchor, constant: 16.0)
collapsedConstraint.priority = .required - 2
expandedConstraint.priority = .required - 1
ivVerticalCollapsedConstraint.priority = .required - 2
ivVerticalExpandedConstraint.priority = .required - 1
ivHorizontalCollapsedConstraint.priority = .required - 2
ivHorizontalExpandedConstraint.priority = .required - 1
NSLayoutConstraint.activate([
shadowView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
shadowView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
shadowView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
shadowView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
containerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16.0),
titleLabel.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.6),
titleLabel.heightAnchor.constraint(equalToConstant: 60.0),
statusLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12.0),
statusLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16.0),
statusLabel.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.6),
theImageView.widthAnchor.constraint(equalToConstant: 50.0),
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor),
getStartedBtn.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 32.0),
getStartedBtn.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16.0),
getStartedBtn.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16.0),
collapsedConstraint, ivVerticalCollapsedConstraint, ivHorizontalCollapsedConstraint,
])
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0.0, height: 3.0)
shadowView.layer.shadowOpacity = 0.75
shadowView.layer.cornerRadius = 16.0
}
public func fillData(_ t: TopicCellStruct) {
titleLabel.text = t.topic.title
statusLabel.text = t.topic.status
if let img = UIImage(systemName: t.topic.icon) {
theImageView.image = img
}
expandedConstraint.isActive = t.expanded
ivVerticalExpandedConstraint.isActive = t.expanded
ivHorizontalExpandedConstraint.isActive = t.expanded
}
public func expand(_ isExpanded: Bool) {
expandedConstraint.isActive = isExpanded
ivVerticalExpandedConstraint.isActive = isExpanded
ivHorizontalExpandedConstraint.isActive = isExpanded
UIView.animate(withDuration: 0.3, animations: {
self.layoutIfNeeded()
})
}
}
class ShadowTestVC: UIViewController {
var myData: [TopicCellStruct] = []
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// some sample data
let p1 = "You have correctly answered "
let p2 = "% of the questions you attempted in this topic."
var pct: Int = 5
let titles: [String] = [
"Basic Algebra",
"Isolating a Variable",
"Absolute Value",
"Solving Linear Equations",
"Solving Radical Equations",
]
let syms: [String] = [
"function",
"multiply.circle",
"ruler.fill",
"arrow.up.arrow.down",
"x.squareroot",
]
for (title, sym) in zip(titles, syms) {
pct += 4
let s: String = p1 + "\(pct)" + p2
let t = Topic(title: title, status: s, icon: sym)
let tcs = TopicCellStruct(topic: t, expanded: false)
myData.append(tcs)
}
for i in 6...19 {
pct += 4
let s: String = p1 + "\(pct)" + p2
let t = Topic(title: "Title \(i)", status: s, icon: "\(i).circle.fill")
let tcs = TopicCellStruct(topic: t, expanded: false)
myData.append(tcs)
}
// a label above the table view
let topUI = UILabel()
topUI.font = .systemFont(ofSize: 24, weight: .regular)
topUI.numberOfLines = 0
topUI.text = "This is some text to represent the UI elements above the table view."
// a gradient view for the background
let gradientBKGView = MyGradView()
[gradientBKGView, topUI, tableView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradientBKGView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
gradientBKGView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
gradientBKGView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
gradientBKGView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
topUI.topAnchor.constraint(equalTo: g.topAnchor, constant: 32.0),
topUI.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 32.0),
topUI.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -48.0),
tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 160.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -16.0),
])
tableView.backgroundColor = .clear
tableView.register(ShadowedCell.self, forCellReuseIdentifier: ShadowedCell.ident)
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
}
}
extension ShadowTestVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: ShadowedCell.ident, for: indexPath) as! ShadowedCell
c.fillData(myData[indexPath.row])
c.selectionStyle = .none
return c
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let c = tableView.cellForRow(at: indexPath) as? ShadowedCell else { return }
myData[indexPath.row].expanded.toggle()
c.expand(myData[indexPath.row].expanded)
tableView.beginUpdates()
tableView.endUpdates()
}
}
class MyGradView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
private var gLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
gLayer.colors = [
UIColor(red: 0.71, green: 0.88, blue: 1.0, alpha: 1).cgColor,
UIColor(red: 0.95, green: 0.95, blue: 1.0, alpha: 1).cgColor
]
gLayer.locations = [0, 1]
gLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
}
}
I am currently using a UIViewController and adding a UITableView to the view. With this tableView I am adding a UIView called containerView to its tableHeaderView. I set the height of the container view and then adding a second UIView to its subview, that is pinned to the bottom of the containerView.
When I add it to the header view the cells are being overlapped. What's odd though is that if I don't add the subview to the container view the headerView is not being overlapped by the cells, it is only occurring when I am adding the second view as a subview to the container view.
class ViewController: UIViewController {
private var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0.7
view.backgroundColor = .red
return view
}()
private var bottomView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
containerView.addSubview(bottomView)
tableView.tableHeaderView = containerView
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
containerView.topAnchor.constraint(equalTo: tableView.topAnchor),
containerView.heightAnchor.constraint(equalToConstant: 214),
containerView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
bottomView.topAnchor.constraint(equalTo: containerView.bottomAnchor),
bottomView.heightAnchor.constraint(equalToConstant: 114),
bottomView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.contentInset = UIEdgeInsets(top: -view.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
tableView.tableHeaderView?.autoresizingMask = []
tableView.tableHeaderView?.layoutIfNeeded()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
}
The reason your "blue view" is overlapping the cells is because you are constraining its Top to the red view's Bottom, but you're not updating the header view size.
One good approach is to create a UIView subclass to use as your header view. Setup all of its content with proper auto-layout constraints.
Then, in the controller's viewDidLayoutSubviews(), we use .systemLayoutSizeFitting(...) to determine the header view's height and update its frame:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
Here is a complete example...
First, our custom view class:
class SampleHeaderView: UIView {
let redView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let blueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
let redTopLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.numberOfLines = 0
return v
}()
let redBottomLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .green
v.numberOfLines = 0
return v
}()
let multiLineLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .cyan
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// all views will use auto-layout
[redView, blueView, redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// prevent label vertical compression
[redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add top and bottom labels to red view
redView.addSubview(redTopLabel)
redView.addSubview(redBottomLabel)
// add multi-line label to blue view
blueView.addSubview(multiLineLabel)
// add red and blue views to self
addSubview(redView)
addSubview(blueView)
// the following constraints need to have less-than required to avoid
// auto-layout warnings
// blue view bottom to self
let c1 = blueView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
// labels trailing contraints
let c2 = redTopLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c3 = redBottomLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c4 = multiLineLabel.trailingAnchor.constraint(equalTo: blueView.trailingAnchor, constant: -8.0)
[c1, c2, c3, c4].forEach { c in
c.priority = .required - 1
}
NSLayoutConstraint.activate([
// red view top to self
redView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
// leading / trailing to self
redView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
redView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// blue view top to red view bottom
blueView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 0.0),
// leading / trailing to self
blueView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
blueView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// top and bottom labels, constrained in red view
// with a little "padding"
redTopLabel.topAnchor.constraint(equalTo: redView.topAnchor, constant: 8.0),
redTopLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.topAnchor.constraint(equalTo: redTopLabel.bottomAnchor, constant: 8.0),
redBottomLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.bottomAnchor.constraint(equalTo: redView.bottomAnchor, constant: -8.0),
// multi-line label, constrained in blue view
// with a little "padding"
multiLineLabel.topAnchor.constraint(equalTo: blueView.topAnchor, constant: 8.0),
multiLineLabel.leadingAnchor.constraint(equalTo: blueView.leadingAnchor, constant: 8.0),
multiLineLabel.bottomAnchor.constraint(equalTo: blueView.bottomAnchor, constant: -8.0),
// the less-than-required priority constraints
c1, c2, c3, c4,
])
}
}
and a sample controller:
class TableHeaderViewController: UIViewController {
var sampleHeaderView = SampleHeaderView()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
sampleHeaderView.redTopLabel.text = "The Red Top Label"
sampleHeaderView.redBottomLabel.text = "The Red Bottom Label, with enough text that is should wrap."
sampleHeaderView.multiLineLabel.text = "This text is for the Label in the Blue View. It is also long enough that it will require word-wrapping. Note that the header updates itself when the frame changes, such as on device rotation."
tableView.tableHeaderView = sampleHeaderView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
}
extension TableHeaderViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
}
Output:
and rotated:
I am currently using a UIViewController and adding a UITableView to the view. With this tableView I am adding a UIView called containerView to its tableHeaderView. I set the height of the container view and then adding a second UIView to its subview, that is pinned to the bottom of the containerView.
When I add it to the header view the cells are being overlapped. What's odd though is that if I don't add the subview to the container view the headerView is not being overlapped by the cells, it is only occurring when I am adding the second view as a subview to the container view.
class ViewController: UIViewController {
private var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0.7
view.backgroundColor = .red
return view
}()
private var bottomView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
containerView.addSubview(bottomView)
tableView.tableHeaderView = containerView
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
containerView.topAnchor.constraint(equalTo: tableView.topAnchor),
containerView.heightAnchor.constraint(equalToConstant: 214),
containerView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
bottomView.topAnchor.constraint(equalTo: containerView.bottomAnchor),
bottomView.heightAnchor.constraint(equalToConstant: 114),
bottomView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.contentInset = UIEdgeInsets(top: -view.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
tableView.tableHeaderView?.autoresizingMask = []
tableView.tableHeaderView?.layoutIfNeeded()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
}
The reason your "blue view" is overlapping the cells is because you are constraining its Top to the red view's Bottom, but you're not updating the header view size.
One good approach is to create a UIView subclass to use as your header view. Setup all of its content with proper auto-layout constraints.
Then, in the controller's viewDidLayoutSubviews(), we use .systemLayoutSizeFitting(...) to determine the header view's height and update its frame:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
Here is a complete example...
First, our custom view class:
class SampleHeaderView: UIView {
let redView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let blueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
let redTopLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.numberOfLines = 0
return v
}()
let redBottomLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .green
v.numberOfLines = 0
return v
}()
let multiLineLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .cyan
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// all views will use auto-layout
[redView, blueView, redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// prevent label vertical compression
[redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add top and bottom labels to red view
redView.addSubview(redTopLabel)
redView.addSubview(redBottomLabel)
// add multi-line label to blue view
blueView.addSubview(multiLineLabel)
// add red and blue views to self
addSubview(redView)
addSubview(blueView)
// the following constraints need to have less-than required to avoid
// auto-layout warnings
// blue view bottom to self
let c1 = blueView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
// labels trailing contraints
let c2 = redTopLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c3 = redBottomLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c4 = multiLineLabel.trailingAnchor.constraint(equalTo: blueView.trailingAnchor, constant: -8.0)
[c1, c2, c3, c4].forEach { c in
c.priority = .required - 1
}
NSLayoutConstraint.activate([
// red view top to self
redView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
// leading / trailing to self
redView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
redView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// blue view top to red view bottom
blueView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 0.0),
// leading / trailing to self
blueView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
blueView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// top and bottom labels, constrained in red view
// with a little "padding"
redTopLabel.topAnchor.constraint(equalTo: redView.topAnchor, constant: 8.0),
redTopLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.topAnchor.constraint(equalTo: redTopLabel.bottomAnchor, constant: 8.0),
redBottomLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.bottomAnchor.constraint(equalTo: redView.bottomAnchor, constant: -8.0),
// multi-line label, constrained in blue view
// with a little "padding"
multiLineLabel.topAnchor.constraint(equalTo: blueView.topAnchor, constant: 8.0),
multiLineLabel.leadingAnchor.constraint(equalTo: blueView.leadingAnchor, constant: 8.0),
multiLineLabel.bottomAnchor.constraint(equalTo: blueView.bottomAnchor, constant: -8.0),
// the less-than-required priority constraints
c1, c2, c3, c4,
])
}
}
and a sample controller:
class TableHeaderViewController: UIViewController {
var sampleHeaderView = SampleHeaderView()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
sampleHeaderView.redTopLabel.text = "The Red Top Label"
sampleHeaderView.redBottomLabel.text = "The Red Bottom Label, with enough text that is should wrap."
sampleHeaderView.multiLineLabel.text = "This text is for the Label in the Blue View. It is also long enough that it will require word-wrapping. Note that the header updates itself when the frame changes, such as on device rotation."
tableView.tableHeaderView = sampleHeaderView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
}
extension TableHeaderViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
}
Output:
and rotated:
I am trying to implement swipe to delete feature with two options in tableview, one is to delete and another one is to Update.The things I want is these options should be vertical rather than horizontal.I have checked so many question but nothing find.
Thanks in advance for support.
.
As I mentioned in the comments, here is one approach:
add your buttons to the cell
add a "container" view to the cell
constrain the container view so it overlays / covers the buttons
add a Pan gesture recognizer to the container view so you can drag it left / right
as you drag it left, it will "reveal" the buttons underneath
You lose all of the built-in swipe functionality, but this is one approach that might give you the design you're going for.
First, an example of creating a "drag view":
class DragTestViewController: UIViewController {
let backgroundView = UIView()
let containerView = UIView()
// leading and trailing constraints for the drag view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(60.0)
private let origTrailing = CGFloat(-60.0)
private var currentLeading = CGFloat(60.0)
private var currentTrailing = CGFloat(-60.0)
override func viewDidLoad() {
super.viewDidLoad()
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .cyan
backgroundView.clipsToBounds = true
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .red
// add a label to the container view
let exampleLabel = UILabel()
exampleLabel.translatesAutoresizingMaskIntoConstraints = false
exampleLabel.text = "Drag Me"
exampleLabel.textColor = .yellow
containerView.addSubview(exampleLabel)
backgroundView.addSubview(containerView)
view.addSubview(backgroundView)
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: origTrailing)
NSLayoutConstraint.activate([
// constrain backgroundView top to top + 80
backgroundView.topAnchor.constraint(equalTo: view.topAnchor, constant: 80.0),
// constrain backgroundView leading / trailing to leading / trailing with 40-pt "padding"
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40.0),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40.0),
// constrain height to 100
backgroundView.heightAnchor.constraint(equalToConstant: 100.0),
// constrain containerView top / bottom to backgroundView top / bottom with 8-pt padding
containerView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 8.0),
containerView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -8.0),
// activate leading / trailing constraints
leadingConstraint,
trailingConstraint,
// constrain the example label centered in the container view
exampleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
exampleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the containerView - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
// update current vars
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
default:
break
}
}
}
That code will produce this:
A red view with a centered label, inside a cyan view. You can drag the red "container" view left and right.
Add a view controller to a new project and assign its Custom Class to DragTestViewController from the above code. There are no #IBOutlet or #IBAction connections, so you should be able to run it as-is. See if you can drag the red view.
Using that as a starting point, we can get this:
with this code:
// simple rounded-corner shadowed view
class ShadowRoundedView: UIView {
let shadowLayer: CAShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.layer.addSublayer(shadowLayer)
clipsToBounds = false
backgroundColor = .clear
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
shadowLayer.shadowRadius = 4.0
shadowLayer.shadowOpacity = 0.6
shadowLayer.shouldRasterize = true
shadowLayer.rasterizationScale = UIScreen.main.scale
}
override func layoutSubviews() {
super.layoutSubviews()
let pth = UIBezierPath(roundedRect: bounds, cornerRadius: 16.0)
shadowLayer.path = pth.cgPath
}
}
// simple rounded button
class RoundedButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height * 0.5
}
}
class DragRevealCell: UITableViewCell {
// callback closure for button taps
var callback: ((Int) -> ())?
// this will hold the "visible" labels, and will initially cover the buttons
let containerView: ShadowRoundedView = {
let v = ShadowRoundedView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// this will hold the buttons
let buttonsView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.clipsToBounds = true
return v
}()
// a "delete" button
let deleteButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete", for: [])
v.setTitleColor(.blue, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .white
return v
}()
// an "update" button
let updateButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Update", for: [])
v.setTitleColor(.white, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
// single label for this example cell
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
return v
}()
// leading and trailing constraints for the container view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(8.0)
private let origTrailing = CGFloat(-8.0)
private var currentLeading = CGFloat(0.0)
private var currentTrailing = CGFloat(0.0)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// cell background color
backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// add buttons to buttons container view
buttonsView.addSubview(deleteButton)
buttonsView.addSubview(updateButton)
// add label to container view -- this is where you would add all your labels, stack views, image views, etc.
containerView.addSubview(myLabel)
// add buttons view first
addSubview(buttonsView)
// add container view second - this will "overlay" it on top of the buttons view
addSubview(containerView)
// containerView leading / trailing constraints - these will be updated as we drag
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: origTrailing)
// needed to avoid layout warnings
let bottomConstraint = containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0)
bottomConstraint.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
leadingConstraint,
trailingConstraint,
bottomConstraint,
myLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
myLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
myLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
myLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -8.0),
myLabel.heightAnchor.constraint(equalToConstant: 120.0),
buttonsView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
buttonsView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
deleteButton.topAnchor.constraint(equalTo: buttonsView.topAnchor, constant: 0.0),
deleteButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
deleteButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: 0.0),
updateButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
updateButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.topAnchor.constraint(equalTo: deleteButton.bottomAnchor, constant: 12.0),
updateButton.heightAnchor.constraint(equalTo: deleteButton.heightAnchor),
updateButton.widthAnchor.constraint(equalTo: deleteButton.widthAnchor),
deleteButton.widthAnchor.constraint(equalToConstant: 120.0),
deleteButton.heightAnchor.constraint(equalToConstant: 40.0),
])
// delete button border
deleteButton.layer.borderColor = UIColor.blue.cgColor
deleteButton.layer.borderWidth = 1.0
// targets for button taps
deleteButton.addTarget(self, action: #selector(self.deleteTapped(_:)), for: .touchUpInside)
updateButton.addTarget(self, action: #selector(self.updateTapped(_:)), for: .touchUpInside)
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the container view - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
// don't allow drag-to-the-right
if currentLeading + translation.x <= origLeading {
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
}
default:
// if the drag-left did not fully reveal the buttons, animate the container view back in place
if containerView.frame.maxX > buttonsView.frame.minX {
self.leadingConstraint.constant = self.origLeading
self.trailingConstraint.constant = self.origTrailing
UIView.animate(withDuration: 0.3, animations: {
self.layoutIfNeeded()
}, completion: { _ in
//self.dragX = 0.0
})
}
}
}
#objc func deleteTapped(_ sender: Any?) -> Void {
callback?(0)
}
#objc func updateTapped(_ sender: Any?) -> Void {
callback?(1)
}
}
class DragRevealTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(DragRevealCell.self, forCellReuseIdentifier: "DragRevealCell")
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "DragRevealCell", for: indexPath) as! DragRevealCell
c.myLabel.text = "Row \(indexPath.row)" + "\n" + "This is where you would populate the cell's labels, image views, any other UI elements, etc."
c.selectionStyle = .none
c.callback = { value in
if value == 0 {
print("Delete action")
} else {
print("Update action")
}
}
return c
}
}
Add a UITableViewController the project and assign its Custom Class to DragRevealTableViewController from the above code. Again, there are no #IBOutlet or #IBAction connections, so you should be able to run it as-is.
NOTE: This is example code only, and should not be considered "production ready"!!! It is only partially implemented and will likely need quite a bit more work. But, it may give you a good starting point.
I have view hierarchy like below;
UITableViewCell ->
-> UIView -> UIStackView (axis: vertical, distribution: fill)
-> UIStackView (axis: horizontal, alignment: top, distribution: fillEqually)
-> UIView -> UIStackView(axis:vertical, distribution: fill)
-> TwoLabelView
My problem is that labels don't get more than one line. I read every question in SO and also tried every possibility but none of them worked. On below screenshot, on the top left box, there should be two pair of label but even one of them isn't showing.
My Question is that how can I achieve multiline in the first box (both for left and right)?
If I change top stack views distribution to fillProportionally, labels get multiline but there will be a gap between last element of first box and the box itself
My first top stack views
//This is the Stackview used just below UITableViewCell
private let stackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.axis = .vertical
s.spacing = 10
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
//This is used to create two horizontal box next to each other
private let myStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .horizontal
//s.alignment = .center
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
UILabel Class:
fileprivate class FixAutoLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
}
}
#IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0
#IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
#IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
fileprivate var firstLabel: FixAutoLabel!
fileprivate var secondLabel: FixAutoLabel!
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel = FixAutoLabel()
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
firstLabel.numberOfLines = 0
firstLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
secondLabel = FixAutoLabel()
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
secondLabel.numberOfLines = 1
secondLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// pin both labels' left-edges to left-edge of self
firstLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
secondLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
// pin both labels' right-edges to right-edge of self
firstLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
secondLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin).isActive = true
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing).isActive = true
// pin bottom of self to bottom of secondLabel + bottomMargin (padding)
bottomAnchor.constraint(equalTo: secondLabel.bottomAnchor, constant: bottomMargin).isActive = true
// call common "refresh" func
updateView()
}
func updateView() {
firstLabel.preferredMaxLayoutWidth = self.bounds.width
secondLabel.preferredMaxLayoutWidth = self.bounds.width
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
firstLabel.sizeToFit()
secondLabel.sizeToFit()
setNeedsUpdateConstraints()
}
override open var intrinsicContentSize : CGSize {
// just has to have SOME intrinsic content size defined
// this will be overridden by the constraints
return CGSize(width: 1, height: 1)
}
}
UIView -> UIStackView class
class ViewWithStack: UIView {
let verticalStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .vertical
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0
self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)
addSubview(verticalStackView)
let lessThan = verticalStackView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: 0)
lessThan.priority = UILayoutPriority(1000)
lessThan.isActive = true
verticalStackView.leftAnchor.constraint(equalTo: self.leftAnchor,constant: 0).isActive = true
verticalStackView.rightAnchor.constraint(equalTo: self.rightAnchor,constant: 0).isActive = true
verticalStackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
verticalStackView.isLayoutMarginsRelativeArrangement = true
}
convenience init(orientation: NSLayoutConstraint.Axis,labelsArray: [UIView]) {
self.init()
verticalStackView.axis = orientation
for label in labelsArray {
verticalStackView.addArrangedSubview(label)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Example Controller Class (This is a minimized version of the whole project):
class ViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let viewWithStack = BoxView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.delegate = self
tableView.dataSource = self
tableView.register(TableViewCell.self, forCellReuseIdentifier: "myCell")
tableView.rowHeight = UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "myCell") as! TableViewCell
if (indexPath.row == 0) {
cell.setup(viewWithStack: self.viewWithStack)
} else {
cell.backgroundColor = UIColor.black
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//return 500
if ( indexPath.row == 0) {
return UITableView.automaticDimension
} else {
return 40
}
}
}
EDIT I created a minimal project then I found that my problem is that my project implements heightForRow function which overrides UITableViewAutomaticDimension so that It gives wrong height for my component. I think I should look how to get height size of the component? because I can't delete heightForRow function which solves my problem.
Example Project Link https://github.com/emreond/tableviewWithStackView/tree/master/tableViewWithStackViewEx
Example project has ambitious layouts when you open view debugger. I think when I fix them, everything should be fine.
Here is a full example that should do what you want (this is what I mean by a minimal reproducible example):
Best way to examine this is to:
create a new project
create a new file, named TestTableViewController.swift
copy and paste the code below into that file (replace the default template code)
add a UITableViewController to the Storyboard
assign its Custom Class to TestTableViewController
embed it in a UINavigationController
set the UINavigationController as Is Initial View Controller
run the app
This is what you should see as the result:
I based the classes on what you had posted (removed unnecessary code, and I am assuming you have the other cells working as desired).
//
// TestTableViewController.swift
//
// Created by Don Mag on 10/21/19.
//
import UIKit
class SideBySideCell: UITableViewCell {
let horizStackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 10
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForReuse() {
horizStackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
}
func commonInit() -> Void {
contentView.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
contentView.addSubview(horizStackView)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
horizStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
horizStackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
horizStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
horizStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
])
}
func addViewWithStack(_ v: ViewWithStack) -> Void {
horizStackView.addArrangedSubview(v)
}
}
class TestTableViewController: UITableViewController {
let sideBySideReuseID = "sbsID"
override func viewDidLoad() {
super.viewDidLoad()
// register custom SideBySide cell for reuse
tableView.register(SideBySideCell.self, forCellReuseIdentifier: sideBySideReuseID)
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell
let twoLabelView1 = TwoLabelView()
twoLabelView1.firstLabelText = "Text for first label on left-side."
twoLabelView1.secondLabelText = "10.765,00TL"
let twoLabelView2 = TwoLabelView()
twoLabelView2.firstLabelText = "Text for second-first label on left-side."
twoLabelView2.secondLabelText = "10.765,00TL"
let twoLabelView3 = TwoLabelView()
twoLabelView3.firstLabelText = "Text for the first label on right-side."
twoLabelView3.secondLabelText = "10.765,00TL"
let leftStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView1, twoLabelView2])
let rightStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView3])
cell.addViewWithStack(leftStackV)
cell.addViewWithStack(rightStackV)
return cell
}
// create ViewWithStack using just a simple label
let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell
let v = UILabel()
v.text = "This is row \(indexPath.row)"
let aStackV = ViewWithStack(orientation: .vertical, labelsArray: [v])
cell.addViewWithStack(aStackV)
return cell
}
}
#IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0
#IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
#IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
fileprivate var firstLabel: UILabel = {
let v = UILabel()
return v
}()
fileprivate var secondLabel: UILabel = {
let v = UILabel()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
firstLabel.numberOfLines = 0
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
secondLabel.numberOfLines = 1
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// Note: recommended to use Leading / Trailing rather than Left / Right
NSLayoutConstraint.activate([
// pin both labels' left-edges to left-edge of self
firstLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
secondLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
// pin both labels' right-edges to right-edge of self
firstLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
secondLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin),
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing),
// pin bottom of self to >= (bottom of secondLabel + bottomMargin (padding))
bottomAnchor.constraint(greaterThanOrEqualTo: secondLabel.bottomAnchor, constant: bottomMargin),
])
}
func updateView() -> Void {
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
}
}
class ViewWithStack: UIView {
let verticalStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.spacing = 10
s.axis = .vertical
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0
// self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)
addSubview(verticalStackView)
NSLayoutConstraint.activate([
// constrain to all 4 sides
verticalStackView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
verticalStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
verticalStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
verticalStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
])
verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
verticalStackView.isLayoutMarginsRelativeArrangement = true
}
convenience init(orientation: NSLayoutConstraint.Axis, labelsArray: [UIView]) {
self.init()
verticalStackView.axis = orientation
for label in labelsArray {
verticalStackView.addArrangedSubview(label)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}