How to create custom cells 100% programmatically in Swift? - ios

I am trying to build a TableView programmatically, but I cannot get a basic standard label to display; all I see is basic empty cells. Here's my code:
TableView Cell:
class TableCell: UITableViewCell {
let cellView: UIView = {
let view = UIView()
view.backgroundColor = .systemRed
return view
}()
let labelView: UILabel = {
let label = UILabel()
label.text = "Cell 1"
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
addSubview(cellView)
NSLayoutConstraint.activate([
cellView.topAnchor.constraint(equalTo: topAnchor),
cellView.bottomAnchor.constraint(equalTo: bottomAnchor),
cellView.leadingAnchor.constraint(equalTo: leadingAnchor),
cellView.trailingAnchor.constraint(equalTo: trailingAnchor)])
cellView.addSubview(labelView)
}
}
Data Source:
class TableDataSource: NSObject, UITableViewDataSource {
let cellID = "cell"
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! TableCell
return cell
}
}
And this is the VC:
class TableViewController: UITableViewController {
let dataSource = TableDataSource()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(TableCell.self, forCellReuseIdentifier: dataSource.cellID)
tableView.dataSource = dataSource
}
}
I am trying to keep the code as basic as possible for future references. I've set various breakpoints to see what could go wrong, but they all check out. Could it be the constraints that are wrong?
Any help is appreciated.

I see several errors in your cell.
Add subviews to contentView, not directly to cell:
contentView.addSubview(cellView)
cellView.addSubview(labelView)
The same is necessary for constraints:
NSLayoutConstraint.activate([
cellView.topAnchor.constraint(equalTo: contentView.topAnchor),
cellView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
cellView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
cellView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
])
Views created in code need to set translatesAutoresizingMaskIntoConstraints = false,
let cellView: UIView = {
let view = UIView()
view.backgroundColor = .systemRed
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let labelView: UILabel = {
let label = UILabel()
label.text = "Cell 1"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
There are no constraints for your label.

Your constraints don't work, because you need to change translatesAutoresizingMaskIntoConstraints for cellView in your setup():
func setup() {
addSubview(cellView)
cellView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cellView.topAnchor.constraint(equalTo: topAnchor),
cellView.bottomAnchor.constraint(equalTo: bottomAnchor),
cellView.leadingAnchor.constraint(equalTo: leadingAnchor),
cellView.trailingAnchor.constraint(equalTo: trailingAnchor)])
cellView.addSubview(labelView)
}

Related

UITableViewCell incorrect dynamic height calculation with SwiftUI view inside on iOS 15

I am trying to use SwiftUI view inside UITableViewCell that is working fine until iOS 15. On iOS 15 when I scroll table view and once initial visible cells go off screen and new cells being reused those are having extra height added.
Following is the source code.
import UIKit
import SwiftUI
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
tableView?.register(TableViewCell.self, forCellReuseIdentifier: "cell")
tableView?.rowHeight = UITableView.automaticDimension
tableView?.estimatedRowHeight = 100
tableView?.separatorColor = .white
tableView?.dataSource = self
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? TableViewCell
return cell ?? UITableViewCell()
}
}
class TableViewCell: UITableViewCell {
var stackView: UIStackView = {
let stack = UIStackView(frame: .zero)
stack.axis = .vertical
return stack
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
func setupView() {
contentView.clipsToBounds = true
contentView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
let hosting = UIHostingController(rootView: TestView())
stackView.addArrangedSubview(hosting.view)
hosting.view.backgroundColor = .red
}
}
struct TestView: View {
var body: some View {
Rectangle()
.fill(Color.green)
.frame(height: 100)
}
}
Any help will be highly appreciated. Thanks

Ambiguous UILabel height in autoresizing UITableViewCell

I am having a hard time making my implementation of UITableView with automatic cell sizing. I am trying to use a UIStackView with 2 labels inside cell with automatic size.
It looks good overall but I am getting the following runtime issues:
I feel like the constraints I have should be enough for the use case here but I would like to get rid of the ambiguity here.
How that can be achieved? What is the common approach to take here?
I have prepared a sample project that simplifies my use case but still demonstrates the issue. Here is my controller:
class ViewController: UIViewController {
struct CellData {
let title: String
let subtitle: String
}
let table = UITableView()
let data = [
CellData(title: "Foo", subtitle: "Bar"),
CellData(title: "Baz", subtitle: "FooBar")
]
private let cellReuseID = "CellReuseID"
override func viewDidLoad() {
super.viewDidLoad()
table.register(Cell.self, forCellReuseIdentifier: cellReuseID)
table.dataSource = self
table.rowHeight = UITableView.automaticDimension
table.tableFooterView = UIView(frame: .zero)
table.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(table)
NSLayoutConstraint.activate([
table.leadingAnchor.constraint(equalTo: view.leadingAnchor),
table.trailingAnchor.constraint(equalTo: view.trailingAnchor),
table.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
table.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseID) as? Cell else {
fatalError()
}
cell.title = data[indexPath.row].title
cell.subtitle = data[indexPath.row].subtitle
return cell
}
}
And the cell it uses:
class Cell: UITableViewCell {
var title: String? {
didSet {
titleLabel.text = title
}
}
var subtitle: String? {
didSet {
subtitleLabel.text = subtitle
}
}
let titleLabel = UILabel()
let subtitleLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .top
stackView.distribution = .fill
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
stackView.topAnchor.constraint(equalTo: contentView.topAnchor)
])
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(subtitleLabel)
}
}
The reason for the Ambiguous Height is because both labels have the same Content Hugging Priority.
Auto-layout makes its "best guess" and you get the desired output -- but you'll still see the issues in Debug View Hierarchy.
To get rid of it, you can give one label a higher Hugging Priority.
For example (in setupView()):
titleLabel.setContentHuggingPriority(.defaultHigh + 1, for: .vertical)
subtitleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)

Strange stretching effect when animating a subview to hidden in UIStackView

I am trying to create a UITableView that has a hidden subview at the bottom that will slide open when the cell is tapped. I have the following demo code:
class ViewController: UIViewController {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
tableView.separatorStyle = .none
tableView.register(ExpandTableCell.self, forCellReuseIdentifier: "Cell")
tableView.dataSource = self
tableView.delegate = self
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
return cell ?? UITableViewCell()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ExpandTableCell else { return }
tableView.performBatchUpdates({ cell.animate() }, completion: nil)
}
}
And the cell:
class ExpandTableCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
setupViews()
}
private let blueView = UIView()
// MARK: - Views
private func setupViews() {
selectionStyle = . none
let titleLabel = UILabel()
titleLabel.text = "Some Title"
let subtitleLabel = UILabel()
subtitleLabel.text = "Some othere sdfhdslkjl dsfljdslfj sdlj sdfldsjfldsjf sdfjdslfjds"
subtitleLabel.numberOfLines = 2
blueView.backgroundColor = .blue
blueView.translatesAutoresizingMaskIntoConstraints = false
blueView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel, blueView])
stackView.axis = .vertical
stackView.spacing = 8.0
blueView.isHidden = true
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
func animate() {
UIView.animate(withDuration: 0.1, animations: { [blueView] in
blueView.isHidden.toggle()
})
}
}
The problem is that the animation has the following effect:
It's squashing the contents of the label above it. It should just slide down from the bottom.
What am I doing wrong here?
Just change the animation timing to match that of the tableView. Try 0.3
func animate() {
UIView.animate(withDuration: 0.3, animations: { [blueView] in
blueView.isHidden.toggle()
})
}
The artifact is gone.

NSLayoutConstraints to limit label width

I am struggling to set the necessary constraints to limit the width of a label to the leading anchor of a button in a tableview cell.
Desired Result
Label to the left, button to the right. Label wraps before button if needed.
Current Result
The label is pulling the red button to the left.
Code
[button.centerYAnchor constraintEqualToAnchor:cell.contentView.centerYAnchor constant:0].active = YES;
[button.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-5].active = YES;
[label.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:10].active = YES;
[label.trailingAnchor constraintEqualToAnchor:button.leadingAnchor constant:0].active = YES;
I have tried various things like adding another label constraint to the right of the cell but that squeezed the button (I tried to fix by setting its compression resistance priority but that had no effect)
What constraints do I need to achieve the desired result please?
Just change
[label.trailingAnchor constraintEqualToAnchor:button.leadingAnchor constant:0].active = YES;
into
[label.trailingAnchor constraintLessThanOrEqualToAnchor:button.leadingAnchor constant:0].active = YES;
import UIKit
final class ViewController: UIViewController {
lazy var tableView: UITableView = {
let val = UITableView(frame: self.view.bounds, style: .plain)
val.dataSource = self
val.delegate = self
val.estimatedRowHeight = 44
val.register(MyCell.self, forCellReuseIdentifier: "MyCell")
return val
}()
let data: [String] = [
"LayoutDemo",
"LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo LayoutDemo",
"LayoutDemoLayoutDemoLayoutDemoLayoutDemo"
]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as? MyCell else { fatalError() }
cell.dataText = data[indexPath.row]
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
class MyCell: UITableViewCell {
var dataText: String? {
didSet {
label.text = dataText
}
}
lazy var label: UILabel = {
let val = UILabel(frame: .zero)
val.translatesAutoresizingMaskIntoConstraints = false
val.numberOfLines = 0
val.backgroundColor = .gray
return val
}()
lazy var button: UIButton = {
let val = UIButton(type: .custom)
val.translatesAutoresizingMaskIntoConstraints = false
val.backgroundColor = .red
return val
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
contentView.addSubview(button)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5),
button.heightAnchor.constraint(equalToConstant: 20),
button.widthAnchor.constraint(equalToConstant: 20),
label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
label.trailingAnchor.constraint(lessThanOrEqualTo: button.leadingAnchor, constant: -10)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

how to add custom view on UITableViewCell programatically?

UPD Resolved - see edited question below
Trying to add a custom view (button, to be more specific) to a custom subclass of UITableViewCell but unable to see any layout results on iOS 10.1 on the device. Did not see any changes on the layout, and tried to just fill the cell with a custom view with the red background but failed to achieve this result as well.
import UIKit
class Save: UITableViewCell {
let containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
self.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerView)
containerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
containerView.topAnchor.constraint(equalTo: topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
}
What I tried besides that: using plain addSubiew and constraint to the self anchors of the cell, and add to contentView in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) but both approaches did nothing, the cell appears but it is empty.
UPD
attaching the TableView initiation code for the reference
class SettingsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let settings = [
"setting1",
"setting2",
"setting3",
"setting4",
"setting5",
"setting6",
"setting7"
]
let overlay: UIView = {
var view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .gray
view.alpha = 0.3
view.isHidden = true
return view
}()
let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "commonCell")
tableView.register(Save.self, forCellReuseIdentifier: "btnCell")
tableView.register(Switcher.self, forCellReuseIdentifier: "switcherCell")
tableView.headerView(forSection: 0)?.textLabel?.text = "settings of app"
tableView.tableFooterView = UIView()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension
tableView.allowsSelection = false
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
view.backgroundColor = .white
view.addSubview(tableView)
tableView.dataSource = self
tableView.delegate = self
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
tabBarController?.navigationItem.rightBarButtonItems = []
tabBarController?.navigationItem.title = "settings"
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settings.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "settings"
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "commonCell", for: indexPath)
cell.textLabel?.text = settings[indexPath.row]
if(indexPath.row == 6) {
cell.textLabel?.text = ""
cell = tableView.dequeueReusableCell(withIdentifier: "btnCell", for: indexPath)
}
return cell
}
}
RESOLVED
in addition to Sh_Khan answer, it was required to set
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 65
on the table
It doesn't look like it's anything wrong with the code in your cell class in my eyes. Can you please add your tableviewcode?
You should either implement
heightForRowAt
Or add
containerView.heightAnchor.constraint(equalToConstant:200).isActive = true
Also add any subview to contentView
contentView.addSubview(containerView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
containerView.heightAnchor.constraint(equalToConstant:200)
])

Resources