Label on Row Separator - Swift Tableview - Hourly Calendar - ios

I want to create an hourly calendar view that is relatively basic, but similar to Apple's native calendar view. How do you add labels to be in line with the row/cell separators, and not contained in a cell. Like this:
Is there a property that lets you add a label to the lines? Do the labels have to be placed outside of the table view? Or is there a separate table that occurs?
In terms of creating colored blocks to represent events on the calendar, what would be the best way to go about doing this? Would it just be a CGRect in a prototype cell? Would you need to create separate xib files?
Thanks in advance for the help, I am still new to learning Swift!

It's not possible (or technically, it would be possible, but the overhead is too high, considering your other options).
Instead of using cell separators, set separatorStyle = .none, and draw the line in the cell (e.g., as a UIView with view.height = 1 and view.backgroundColor = .grey) and normally add the label in the cell.
Basically the solution is very simple: disable standard separator lines, and rather draw separator inside the cell (bottom or top) along with the labels. That's how I've been doing things when the client asked for some custom fancy separators - I added a custom line at the bottom of the cell and used the rest of the cell's contentView as for the cell's content.
EDIT
You can use a following example to start with (note that this is just one of several different approaches how to manage it):
class TimeCellViewController: UITableViewController {
override func loadView() {
super.loadView()
// you can use UITableViewAutomaticDimension instead of static height, if
// there will be variable heights that you don't know upfront
// https://stackoverflow.com/a/18746930/2912282
// or mine:
// https://stackoverflow.com/a/47963680/2912282
tableView.rowHeight = 80
tableView.estimatedRowHeight = 80
tableView.separatorStyle = .none
// to allow scrolling below the last cell
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 40))
tableView.register(TimeCell.self, forCellReuseIdentifier: "timeCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 24
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "timeCell", for: indexPath) as! TimeCell
if indexPath.row > 0 {
cell.topTime = "\(indexPath.row):00"
} else {
cell.topTime = ""
}
cell.bottomTime = "\(indexPath.row + 1):00"
return cell
}
}
class TimeCell: UITableViewCell {
// little "hack" using two labels to render time both above and below the cell
private let topTimeLabel = UILabel()
private let bottomTimeLabel = UILabel()
private let separatorLine = UIView()
var topTime: String = "" {
didSet {
topTimeLabel.text = topTime
}
}
var bottomTime: String = "" {
didSet {
bottomTimeLabel.text = bottomTime
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
contentView.addSubview(topTimeLabel)
contentView.addSubview(bottomTimeLabel)
contentView.addSubview(separatorLine)
topTimeLabel.textColor = UIColor.gray
topTimeLabel.textAlignment = .right
bottomTimeLabel.textColor = UIColor.gray
bottomTimeLabel.textAlignment = .right
separatorLine.backgroundColor = UIColor.gray
bottomTimeLabel.translatesAutoresizingMaskIntoConstraints = false
topTimeLabel.translatesAutoresizingMaskIntoConstraints = false
separatorLine.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
bottomTimeLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0),
bottomTimeLabel.centerYAnchor.constraint(equalTo: self.bottomAnchor),
bottomTimeLabel.widthAnchor.constraint(equalToConstant: 50),
topTimeLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0),
topTimeLabel.centerYAnchor.constraint(equalTo: self.topAnchor),
topTimeLabel.widthAnchor.constraint(equalToConstant: 50),
separatorLine.leftAnchor.constraint(equalTo: bottomTimeLabel.rightAnchor, constant: 8),
separatorLine.bottomAnchor.constraint(equalTo: self.bottomAnchor),
separatorLine.heightAnchor.constraint(equalToConstant: 1),
separatorLine.rightAnchor.constraint(equalTo: self.rightAnchor, constant: 0),
])
// if you use UITableViewAutomaticDimension instead of static height,
// you will have to set priority of one of the height constraints to 999, see
// https://stackoverflow.com/q/44651241/2912282
// and
// https://stackoverflow.com/a/48131525/2912282
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Related

Scroll tableView when there is no enough cells

I have a UITableView that for some reasons, I set a contentInset.top and contentOffset.yon it in the ViewDidLoad
tableView.contentInset.top = 150
tableView.contentOffset.y = -150
The concept is, when the it gets opened, the rows start -150 point from the top, and when scroll begins, the rows come back to the top first, and then the actual scrolling starts (new cells appears from bottom and old cell disappear in the top).
The only issue is, when there isn't enough cell on the UITableView, it won't scroll to back to the top.
I actually don't care about the actual scrolling starts (new cells appears from bottom and old cell disappear in the top), I want that in any case with any number of cells, the table view scroll to the top like that:
tableView.contentInset.top = 0
tableView.contentOffset.y = 0
and then when there is no enough cell, it won't go for the actual scrolling. Is there any way to do that?
BTW, I use scrollViewDidScroll to smoothly move it up and down with user finger, want to do that when there is no enough cell
Thank you so much
What you want to do is set the table view's .contentInset.bottom if the resulting height of the rows is less than the height of the table view's frame.
We'll start with a simple dynamic height cell (a multiline label):
class DynamicHeightCell: UITableViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
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()
}
func commonInit() {
theLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(theLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: g.topAnchor),
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
}
}
and a basic controller with a table view, inset by 40-points from each side:
class TableInsetVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
// set the number of rows to use
// once we get past 7 (or so), the rows will be
// taller than the tableView frame
let testRowCount = 5
let tableView: UITableView = {
let v = UITableView()
return v
}()
// we'll cycle through colors for the cell backgrounds
// to make it easier to see the cell frames
let bkgColors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue, .systemYellow, .systemCyan, .systemBrown,
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[tableView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
tableView.register(DynamicHeightCell.self, forCellReuseIdentifier: "c")
tableView.dataSource = self
tableView.delegate = self
// so we can see the tableView frame
tableView.backgroundColor = .lightGray
tableView.contentInset.top = 150
tableView.contentOffset.y = -150
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return testRowCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! DynamicHeightCell
// we want dynamic height cells, so
var s = "Row: \(indexPath.row + 1)"
// to make it easy to see it's the last row
if indexPath.row == tableView.numberOfRows(inSection: 0) - 1 {
s += " --- Last Row"
}
// fill cells with 1 to 4 rows of text
for i in 0..<(indexPath.row % 4) {
s += "\nThis is Line \(i + 2)"
}
c.theLabel.text = s
// cycle background color to make it easy to see the cell frames
c.contentView.backgroundColor = bkgColors[indexPath.row % bkgColors.count]
return c
}
}
It looks like this when run:
So far, though, it's in your current condition -- we can't scroll up to the top.
What we need to do is find a way to set the table view's .contentInset.bottom:
So, we'll implement scrollViewDidScroll(...):
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// unwrap optional
if let rows = tableView.indexPathsForVisibleRows {
// get indexPath of final cell
let n = tableView.numberOfRows(inSection: 0)
let lastRowIndexPath = IndexPath(row: n - 1, section: 0)
// if final cell is visible
if rows.contains(lastRowIndexPath) {
// we now know the tableView's contentSize, so
// if .contentSize.height is less than tableView.frame.height
if tableView.contentSize.height < tableView.frame.height {
// calculate and set bottom inset
tableView.contentInset.bottom = tableView.frame.height - tableView.contentSize.height
}
}
}
}
Now when we run that, we can "scroll to the top":
Change the testRowCount at the top of the controller from 5 to 6, 7, 8, 20, 30, etc. Once there are enough rows (or the rows are taller) so the table view can scroll to the top without the .contentInset.bottom we get "normal" scrolling while maintaining the 150-point top inset.
Worth noting: the above scrollViewDidScroll code will end up running every time the table is scrolled. Ideally, we would only let it run until we've determined the bottom offset (if one is needed).
To do that, we need a couple new var properties and some if testing.
Here's another version of that controller that stops testing once we know what's needed:
class TableInsetVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
// we'll use this for both the .contentInset.bottom
// AND to stop testing the height when we've determined whether it's needed or not
var bottomInset: CGFloat = -1
// we don't want to start testing the height until AFTER initial layout has finished
// so we'll use this as a flag
var hasAppeared: Bool = false
// set the number of rows to use
// once we get past 7 (or so), the rows will be
// taller than the tableView frame
let testRowCount = 5
let tableView: UITableView = {
let v = UITableView()
return v
}()
// we'll cycle through colors for the cell backgrounds
// to make it easier to see the cell frames
let bkgColors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue, .systemYellow, .systemCyan, .systemBrown,
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[tableView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
tableView.register(DynamicHeightCell.self, forCellReuseIdentifier: "c")
tableView.dataSource = self
tableView.delegate = self
// so we can see the tableView frame
tableView.backgroundColor = .lightGray
tableView.contentInset.top = 150
tableView.contentOffset.y = -150
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
hasAppeared = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// initial layout has finished, AND we have not yet changed bottomInset
if hasAppeared, bottomInset == -1 {
// unwrap optional
if let rows = tableView.indexPathsForVisibleRows {
// get indexPath of final cell
let n = tableView.numberOfRows(inSection: 0)
let lastRowIndexPath = IndexPath(row: n - 1, section: 0)
// if final cell is visible
if rows.contains(lastRowIndexPath) {
// we now know the tableView's contentSize, so
// if .contentSize.height is less than tableView.frame.height
if tableView.contentSize.height < tableView.frame.height {
// calculate and set bottom inset
bottomInset = tableView.frame.height - tableView.contentSize.height
tableView.contentInset.bottom = bottomInset
} else {
// .contentSize.height is greater than tableView.frame.height
// so we don't set .contentInset.bottom
// and we set bottomInset to -2 so we stop testing
bottomInset = -2
}
} else {
// final cell is not visible, so
// if we have scrolled up past the top,
// we know the full table is taller than the tableView
// and we set bottomInset to -2 so we stop testing
if tableView.contentOffset.y >= 0 {
bottomInset = -2
}
}
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return testRowCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! DynamicHeightCell
// we want dynamic height cells, so
var s = "Row: \(indexPath.row + 1)"
// to make it easy to see it's the last row
if indexPath.row == tableView.numberOfRows(inSection: 0) - 1 {
s += " --- Last Row"
}
// fill cells with 1 to 4 rows of text
for i in 0..<(indexPath.row % 4) {
s += "\nThis is Line \(i + 2)"
}
c.theLabel.text = s
// cycle background color to make it easy to see the cell frames
c.contentView.backgroundColor = bkgColors[indexPath.row % bkgColors.count]
return c
}
}

Scrollable StackView inside a UITableViewCell - ScrollView ContentSize not being updated

I've recently started learning swift and iOS app development. I've been doing php backend and low level iOS/macOS programming till now and working with UI is a little hard for me, so please tolerate my stupidity.
If I understand this correctly, stackviews automatically space and contain its subviews in its frame. All the math and layout is done automatically by it. I have a horizontal stackview inside a custom UITableViewCell. The UIStackView is within a UIScrollView because I want the content to be scroll-able. I've set the anchors programmatically (I just can't figure out how to use the storyboard thingies). This is what the cells look like
When I load the view, the stackview doesn't scroll. But it does scroll if I select the cell at least once. The contentSize of the scrollview is set inside the layoutsubviews method of my custom cell.
My Custom Cell
class TableViewCell: UITableViewCell
{
let stackViewLabelContainer = UIStackView()
let scrollViewContainer = UIScrollView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)
{
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .black
stackViewLabelContainer.axis = .horizontal
stackViewLabelContainer.distribution = .equalSpacing
stackViewLabelContainer.alignment = .leading
stackViewLabelContainer.spacing = 5
for _ in 1...10
{
let labelView = UILabel();
labelView.backgroundColor = tintColor
labelView.textColor = .white
labelView.text = "ABCD 123"
stackViewLabelContainer.addArrangedSubview(labelView)
}
scrollViewContainer.addSubview(stackViewLabelContainer)
stackViewLabelContainer.translatesAutoresizingMaskIntoConstraints = false
stackViewLabelContainer.leadingAnchor.constraint(equalTo: scrollViewContainer.leadingAnchor).isActive = true
stackViewLabelContainer.topAnchor.constraint(equalTo: scrollViewContainer.topAnchor).isActive = true
addSubview(scrollViewContainer)
scrollViewContainer.translatesAutoresizingMaskIntoConstraints = false
scrollViewContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10).isActive = true
scrollViewContainer.topAnchor.constraint(equalTo: topAnchor, constant: 5).isActive = true
scrollViewContainer.heightAnchor.constraint(equalTo:stackViewLabelContainer.heightAnchor).isActive = true
scrollViewContainer.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
scrollViewContainer.showsHorizontalScrollIndicator = false
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews()
{
super.layoutSubviews()
scrollViewContainer.contentSize = CGSize(width: stackViewLabelContainer.frame.width, height: stackViewLabelContainer.frame.height)
}
}
Here's the TableViewController
class TableViewController: UITableViewController {
override func viewDidLoad()
{
super.viewDidLoad()
tableView.register(TableViewCell.self, forCellReuseIdentifier: "reuse_cell")
}
override func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "reuse_cell") as! TableViewCell
return cell
}
override func viewDidLayoutSubviews()
{
print("called")
super.viewDidLayoutSubviews()
// let cells = tableView.visibleCells as! Array<TableViewCell>
// cells.forEach
// {
// cell in
// cell.scrollViewContainer.contentSize = CGSize(width: cell.stackViewLabelContainer.frame.width, height: cell.stackViewLabelContainer.frame.height)
//
// }
}
}
I figured out a method to make this work but it affects abstraction and it feels like a weird hack. You get the visible cells from within the UITableViewController, access each scrollview and update its contentSize. There's another fix I found by reversing dyld_shared_cache where I override draw method and stop reusing cells. Both solutions feel like they're far from "proper".
You should constraint the scrollview to the contentView of the cell.
contentView.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor).isActive = true
scrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
Now you can loop your labels and add them as the arranged subviews
for _ in 1...10
{
let labelView = UILabel();
labelView.backgroundColor = tintColor
labelView.textColor = .white
labelView.text = "ABCD 123"
stackView.addArrangedSubview(labelView)
}

create UITableViewCell constraint programmatically

This is all taking place within this class:
class CustomCell2: UITableViewCell {
When I print the value of an existing width constraint (label = 0.7 * width of the cell) that was set in Storyboard, I see this:
<NSLayoutConstraint:0x36b8750 UILabel:0x3d2aac0'Label'.width == 0.7*appName.CustomCell2:0x3e01880'CustomCell2'.width>
When I try to create the same constraint programmatically, I get the error "The view hierarchy is not prepared for the constraint:"
<NSLayoutConstraint:0xee08590 UILabel:0x3d2aac0'Label'.width == 0.9*appName.CustomCell2:0x3e01880'CustomCell2'.width>
Seems exactly the same, so why can't that constraint be added?
Constraint code in awakeFromNib() of CustomCell2:
let widthConstraint = NSLayoutConstraint(item: labelName, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: self, attribute: NSLayoutAttribute.width, multiplier: 0.9, constant: 0)
labelName.addConstraint(widthConstraint)
Rest of the error message:
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView(UIConstraintBasedLayout) _viewHierarchyUnpreparedForConstraint:] to debug.
2017-12-03 20:18:51.183 appName[899:684382] View hierarchy unprepared for constraint.
Constraint: <NSLayoutConstraint:0xee08590 UILabel:0x3d2aac0'Label'.width == 0.9*appName.CustomCell2:0x3e01880'CustomCell2'.width>
Container hierarchy:
<UILabel: 0x3d2aac0; frame = (281 43; 674 54); text = 'Label'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x4233d30>>
View not found in container hierarchy: <appName.CustomCell2: 0x3e01880; baseClass = UITableViewCell; frame = (0 0; 963 160); layer = <CALayer: 0xee08250>>
That view's superview: NO SUPERVIEW
Unable to install constraint on view. Does the constraint reference something from outside the subtree of the view? That's illegal. constraint:<NSLayoutConstraint:0xee08590 UILabel:0x3d2aac0'Label'.width == 0.9*appName.CustomCell2:0x3e01880'CustomCell2'.width> view:<UILabel: 0x3d2aac0; frame = (281 43; 674 54); text = 'Label'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x4233d30>>
Thanks!
you should be adding your constraints in init for the cell, assuming it will be a dequeued reusable cell, remember to use contentView instead of View for the bounds of the cell:
class CustomCell2: UITableViewCell {
//MARK: Subviews
//Add View Programmatically or in xib
let titleLabel: UILabel = {
let label = UILabel()
label.text = " Title "
//…
label.translatesAutoresizingMaskIntoConstraints = false //Must use
return label
}()
//…
//MARK: init
//Add Subviews and then layout Contraints to the Cell’s contentView
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubViewsAndlayout()
}
/// Add and sets up subviews with programmically added constraints
func addSubViewsAndlayout() {
contentView.addSubview(titleLabel) //will crash if not added
let screenwidth = UIScreen.main.bounds.width //get any other properties you need
titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 12.0).isActive = true
titleLabel.heightAnchor.constraint(equalToConstant: 30).isActive = true
titleLabel.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 12).isActive = true
titleLabel.rightAnchor.constraint(equalTo: otherViewToTheSide.rightAnchor, constant: -24).isActive = true
//...
I also like to implement the following methods instead of using hard coded values / strings.
/// Get the Height of the cell
/// use this in heightForRowAt indexPath
/// - Returns: CGFloat
class func height() -> CGFloat {
return 175
}
//CustomCell2.height()
/// Get the string identifier for this class.
///
/// - Retruns: String
class var identifier: String {
return NSStringFromClass(self).components(separatedBy: ".").last!
}
//use when registering cell:
self.tableView.register(CustomCell2.self, forCellReuseIdentifier: CustomCell2.identifier)
You can create UITableViewCell constraint Programatically like :
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell2.self, forCellReuseIdentifier: "cell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? CustomCell2 else { return UITableViewCell() }
cell.model = CellModel(labelString: "set constriant by code")
return cell
}
}
Define Model :
struct CellModel {
let labelString : String
}
Define custom Cell :
class CustomCell2 : UITableViewCell {
private let label : UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false // enable auto-layout
label.backgroundColor = .green // to visualize
label.textAlignment = .center // text alignment in center
return label
}()
private func addLabel() {
addSubview(label)
NSLayoutConstraint.activate([
// label width is 70% width of cell
label.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.7),
// center along horizontally
label.centerXAnchor.constraint(equalTo: centerXAnchor)
])
}
var model : CellModel? {
didSet {
label.text = model?.labelString ?? "Test"
}
}
// Init
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addLabel()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Output
A user on Apple's Developer Forums pointed me in the right direction. The constraint was between the label and the cell (the cell being referred to as 'self' in the class for the custom cell) and the answer here was that I had to addConstraint to the cell instead of adding it to the label. Same exact constraint definition, but it only worked as self.addConstraint() and gave the error "The view hierarchy is not prepared for the constraint" when coded as labelName.addConstraint()

Problems with complex UITableViewCell

I'm trying to implement a custom complex UITableViewCell. My data source is relatively simple, but I could have some multiple elements.
class Element: NSObject {
var id: String
var titles: [String]
var value: String
init(id: String, titles: [String], value: String) {
self.id = id
self.titles = titles
self.value = value
}
}
I have an array of elements [Element] and, as you can see, for each element titles could have multiple string values. I must use the following layouts:
My first approach was to implement a dynamic UITableViewCell, trying to add content inside self.contentView at runtime. Everything is working, but it's not so fine and as you can see, reusability is not handled in the right way. Lag is terrible.
import UIKit
class ElementTableCell: UITableViewCell {
var titles: [String]!
var value: String!
var width: CGFloat!
var titleViewWidth: CGFloat!
var cellHeight: Int!
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:)")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
}
func drawLayout() {
titleViewWidth = (width * 2)/3
cellHeight = 46 * titles.count
for i in 0 ..< titles.count {
let view = initTitleView(title: titles[i], width: titleViewWidth, yPosition: CGFloat(cellHeight * i))
self.contentView.addSubview(view)
}
self.contentView.addSubview(initButton())
}
func initTitleView(title: String, width: CGFloat, yPosition: CGFloat) -> UIView {
let titleView: UILabel = UILabel(frame:CGRect(x:0, y:Int(yPosition), width: Int(width), height: 45))
titleView.text = title
return titleView
}
func initButton(value: String) -> UIButton {
let button = UIButton(frame:CGRect(x: 0, y: 0, width: 70, height:34))
button.setTitle(value, for: .normal)
button.center.x = titleViewWidth + ((width * 1)/3)/2
button.center.y = CGFloat(cellHeight/2)
return priceButton
}
}
And the UITableView delegate method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ElementTableCell(style: .default, reuseIdentifier: "ElementTableCell")
cell.width = self.view.frame.size.width
cell.titles = elements[indexPath.row].titles
cel.value = elements[indexPath.row].value
cell.drawLayout()
return cell
}
Now I'm thinking about a total different approach, such as using a UITableView Section for each element in elements array and a UITableViewCell for each title in titles. It could work, but I'm concerned about the right button.
Do you have any suggestion or other approach to share?
I solved changing application UI logic in order to overcome the problem. Thank you all.
Here's some code you can play with. It should work just be creating a new UITableView in a Storyboard and assigning it to BoxedTableViewController in this file...
//
// BoxedTableViewController.swift
//
import UIKit
class BoxedCell: UITableViewCell {
var theStackView: UIStackView!
var containingView: UIView!
var theButton: UIButton!
var brdColor = UIColor(white: 0.7, alpha: 1.0)
// "spacer" view is just a 1-pt tall UIView used as a horizontal-line between labels
// when there is more than one title label
func getSpacer() -> UIView {
let newView = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 1))
newView.backgroundColor = brdColor
newView.translatesAutoresizingMaskIntoConstraints = false
newView.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
return newView
}
// "label view" is a UIView containing on UILabel
// embedding the label in a view allows for convenient borders and insets
func getLabelView(text: String, position: Int) -> UIView {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
let newLabel = UILabel()
newLabel.font = UIFont.systemFont(ofSize: 15.0)
newLabel.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
newLabel.textColor = .black
newLabel.layer.borderWidth = 1
newLabel.layer.borderColor = brdColor.cgColor
newLabel.numberOfLines = 0
newLabel.text = text
newLabel.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(newLabel)
newLabel.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 8.0).isActive = true
newLabel.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -8.0).isActive = true
var iTop: CGFloat = 0.0
var iBot: CGFloat = 0.0
// the passed "position" tells me whether this label is:
// a Single Title only
// the first Title of more than one
// the last Title of more than one
// or a Title with a Title above and below
// so we can set up proper top/bottom padding
switch position {
case 0:
iTop = 16.0
iBot = 16.0
break
case 1:
iTop = 12.0
iBot = 8.0
break
case -1:
iTop = 8.0
iBot = 12.0
break
default:
iTop = 8.0
iBot = 8.0
break
}
newLabel.topAnchor.constraint(equalTo: v.topAnchor, constant: iTop).isActive = true
newLabel.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -iBot).isActive = true
return v
}
func setupThisCell(rowNumber: Int) -> Void {
// if containingView is nil, it hasn't been created yet
// so, create it + Stack view + Button
// else
// don't create new ones
// This way, we don't keep adding more and more views to the cell on reuse
if containingView == nil {
containingView = UIView()
containingView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(containingView)
containingView.layer.borderWidth = 1
containingView.layer.borderColor = brdColor.cgColor
containingView.backgroundColor = .white
containingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0).isActive = true
containingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0).isActive = true
containingView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6.0).isActive = true
containingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6.0).isActive = true
theStackView = UIStackView()
theStackView.translatesAutoresizingMaskIntoConstraints = false
containingView.addSubview(theStackView)
theStackView.axis = .vertical
theStackView.spacing = 4.0
theStackView.alignment = .fill
theStackView.distribution = .fill
theButton = UIButton(type: .custom)
theButton.translatesAutoresizingMaskIntoConstraints = false
containingView.addSubview(theButton)
theButton.backgroundColor = .blue
theButton.setTitleColor(.white, for: .normal)
theButton.setTitle("The Button", for: .normal)
theButton.setContentHuggingPriority(1000, for: .horizontal)
theButton.centerYAnchor.constraint(equalTo: containingView.centerYAnchor, constant: 0.0).isActive = true
theButton.trailingAnchor.constraint(equalTo: containingView.trailingAnchor, constant: -8.0).isActive = true
theStackView.topAnchor.constraint(equalTo: containingView.topAnchor, constant: 0.0).isActive = true
theStackView.bottomAnchor.constraint(equalTo: containingView.bottomAnchor, constant: 0.0).isActive = true
theStackView.leadingAnchor.constraint(equalTo: containingView.leadingAnchor, constant: 0.0).isActive = true
theStackView.trailingAnchor.constraint(equalTo: theButton.leadingAnchor, constant: -8.0).isActive = true
}
// remove all previously added Title labels and spacer views
for v in theStackView.arrangedSubviews {
v.removeFromSuperview()
}
// setup 1 to 5 Titles
let n = rowNumber % 5 + 1
// create new Title Label views and, if needed, spacer views
// and add them to the Stack view
if n == 1 {
let aLabel = getLabelView(text: "Only one title for row: \(rowNumber)", position: 0)
theStackView.addArrangedSubview(aLabel)
} else {
for i in 1..<n {
let aLabel = getLabelView(text: "Title number \(i)\n for row: \(rowNumber)", position: i)
theStackView.addArrangedSubview(aLabel)
let aSpacer = getSpacer()
theStackView.addArrangedSubview(aSpacer)
}
let aLabel = getLabelView(text: "Title number \(n)\n for row: \(rowNumber)", position: -1)
theStackView.addArrangedSubview(aLabel)
}
}
}
class BoxedTableViewController: UITableViewController {
let cellID = "boxedCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(BoxedCell.self, forCellReuseIdentifier: cellID)
tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1250
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! BoxedCell
// Configure the cell...
cell.setupThisCell(rowNumber: indexPath.row)
return cell
}
}
I'll check back if you run into any problems with it (gotta run, and haven't fully tested it yet -- and ran out of time to comment it - ugh).
You can also use tableview as tableviecell and adjust cell accordingly.
u need to layout cell in func layoutsubviews after set data to label and imageview;
Yes, split ElementTableCell to section with header and cells is much better approach. In this case you have no need to create constraints or dealing with complex manual layout. This would make your code simple and make scrolling smooth.
The button you use can be easily moved to the reusable header view
Is you still want to keep it in one complete cell, where is a way to draw manually the dynamic elements, such as titles and separators lines. Manually drawing is faster as usual. Or remove all views from cell.contentView each time you adding new. But this way is much more complicated.
Greate article about how to make UITableView appearence swmoth:
Perfect smooth scrolling in UITableViews

How to set custom margins for a UITableViewCell's .selectedBackgroundView

I'm using a table view to display a tree structure. Each cell corresponds to a node that the user can expand or collapse. The level of each node is visualized by having increasingly large indents at the leading edge of the cells. Those indents are set by using layoutMargins. This seems to work well for the cell's label and separators. Here's some code:
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let cellLevel = cellLevelForIndexPath(indexPath)
let insets: UIEdgeInsets = UIEdgeInsetsMake(0.0, CGFloat(cellLevel) * 20.0, 0.0, 0.0)
cell.separatorInset = insets
cell.layoutMargins = insets
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("cellId") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cellId")
}
cell!.preservesSuperviewLayoutMargins = false
cell!.backgroundColor = UIColor.clearColor()
let cellLevel = cellLevelForIndexPath(indexPath)
if let textlabel = cell!.textLabel {
textlabel.text = "Cell # level \(cellLevel)"
textlabel.textColor = UIColor.blackColor()
}
cell!.selectedBackgroundView = UIView(frame: CGRectZero)
cell!.selectedBackgroundView.backgroundColor = UIColor.cyanColor()
return cell!
}
The resulting table looks like this:
The question I'm facing now is this: how can I elegantly apply the same indent to the cell's .selectedBackgroundView, so that it appears flush with the text and separator line? The end result should look something like this:
Note: I'm currently achieving the desired effect by making the .selectedBackgroundView more complex and adding background-colored subviews of varying size that effectively mask parts of the cell, e.g. like this:
let maskView = UIView(frame: CGRectMake(0.0, 0.0, CGFloat(cellLevel) * 20.0, cell!.bounds.height))
maskView.backgroundColor = tableView.backgroundColor
cell!.selectedBackgroundView.addSubview(maskView)
But I strongly feel that there must be a nicer way to do this.
Figured out a way to make it work. The trick for me was to stop thinking about the .selectedBackgroundView as the visible highlight itself (and thus trying to mask or resize it) and to treat it more like a canvas instead.
Here's what I ended up doing. First a more convenient way to get the appropriate inset for each level:
let tableLevelBaseInset = UIEdgeInsetsMake(0.0, 20.0, 0.0, 0.0)
private func cellIndentForLevel(cellLevel: Int) -> CGFloat {
return CGFloat(cellLevel) * tableLevelBaseInset.left
}
And then in the cellForRowAtIndexPath:
cell!.selectedBackgroundView = UIView(frame: CGRectZero)
cell!.selectedBackgroundView.backgroundColor = UIColor.clearColor()
let highlight = UIView(frame: CGRectOffset(cell!.bounds, cellIndentForLevel(cellLevel), 0.0))
highlight.backgroundColor = UIColor.cyanColor()
cell!.selectedBackgroundView.addSubview(highlight)
Seems to work nicely.
var selectedView = UIView()
override func awakeFromNib() {
super.awakeFromNib()
self.selectedBackgroundView = {
let view = UIView()
view.backgroundColor = UIColor.clear
selectedView.translatesAutoresizingMaskIntoConstraints = false
selectedView.backgroundColor = .hetro_systemGray6
selectedView.roundAllCorners(radius: 8)
view.addSubview(selectedView)
selectedView.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
selectedView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -5).isActive = true
selectedView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
selectedView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
return view
}()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
selectedBackgroundView?.isHidden = false
} else {
selectedBackgroundView?.isHidden = true
}
}

Resources