In my project (UIKit, programmatic UI) I have a UITableView with sections. The cells use a custom class. On load all cells just show 3 lines of info (2 labels). On tap, all contents will be displayed. Therefor I've setup my custom cell class to have two containers, one for the 3 line preview and one for the full contents. These containers are added/removed from the cell's content view when needed when the user taps the cell by calling a method (toggleFullView) on the custom cell class. This method is called from the view controller in didSelectRowAt:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let annotation = annotationsController.getAnnotationFor(indexPath)
//Expandable cell
guard let cell = tableView.cellForRow(at: indexPath) as? AnnotationCell else { return }
cell.toggleFullView()
tableView.reloadRows(at: [indexPath], with: .none)
// tableView.reloadData()
}
Basically it works, but there are some issues:
I have to double tap the cell for it to expand and again to make it collapse again. The first tap will perform the row animation of tableView.reloadRows(at: [indexPath], with: .none) and the second tap will perform the expanding. If I substitute reloadRows with tableView.reloadData() the expanding and collapsing will happen after a single tap! But that is disabling any animations obviously, it just snaps into place. How Do I get it to work with one tap?
When the cell expands, some other random cells are also expanded. I guess this has something to do with reusable cells, but I have not been able to remedy this. See the attached Video (https://www.youtube.com/watch?v=rOkuqMnArEU).
I want to be the expanded cell to collapse once I tap another cell to expand, how do I perceive that?
My custom cell class:
import UIKit
class AnnotationCell: UITableViewCell, SelfConfiguringAnnotationCell {
//MARK: - Properties
private let titleLabelPreview = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .headline))
private let titleLabelDetails = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .headline))
private let detailsLabelShort = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .subheadline), numberOfLines: 2)
private let detailsLabelLong = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .subheadline), numberOfLines: 0)
private let mapImageLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .footnote), andColor: .tertiarySystemGroupedBackground)
private let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .footnote), andColor: .tertiarySystemGroupedBackground)
private let checkmarkImageView = UIImageView()
private var checkmarkView = UIView()
private var previewDetailsView = UIStackView()
private var fullDetailsView = UIStackView()
private var showFullDetails = false
//MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureContents()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutIfNeeded() {
super.layoutIfNeeded()
let padding: CGFloat = 5
if contentView.subviews.contains(previewDetailsView) {
//Constrain the preview view
NSLayoutConstraint.activate([
previewDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
previewDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
previewDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
previewDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
])
} else {
//Constrain the full view
NSLayoutConstraint.activate([
fullDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
fullDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
fullDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
fullDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
])
}
}
//MARK: - Actions
///Expand and collapse the cell
func toggleFullView() {
showFullDetails.toggle()
if showFullDetails {
//show the full version
if contentView.subviews.contains(previewDetailsView) {
previewDetailsView.removeFromSuperview()
}
if !contentView.subviews.contains(fullDetailsView) {
contentView.addSubview(fullDetailsView)
}
} else {
//show the preview version
if contentView.subviews.contains(fullDetailsView) {
fullDetailsView.removeFromSuperview()
}
if !contentView.subviews.contains(previewDetailsView) {
contentView.addSubview(previewDetailsView)
}
}
UIView.animate(withDuration: 1.2) {
self.layoutIfNeeded()
}
}
//MARK: - Layout
private func configureContents() {
backgroundColor = .clear
separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
selectionStyle = .none
detailsLabelShort.adjustsFontSizeToFitWidth = false
detailsLabelLong.adjustsFontSizeToFitWidth = false
checkmarkView.translatesAutoresizingMaskIntoConstraints = false
checkmarkView.addSubview(checkmarkImageView)
checkmarkImageView.tintColor = .systemOrange
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
previewDetailsView = UIStackView(arrangedSubviews: [titleLabelPreview, detailsLabelShort])
previewDetailsView.axis = .vertical
previewDetailsView.translatesAutoresizingMaskIntoConstraints = false
previewDetailsView.addBackground(.blue)
fullDetailsView = UIStackView(arrangedSubviews: [titleLabelDetails, detailsLabelLong, mapImageLabel, lastEditedLabel])
fullDetailsView.axis = .vertical
fullDetailsView.translatesAutoresizingMaskIntoConstraints = false
fullDetailsView.addBackground(.green)
//By default only add the preview View
contentView.addSubviews(checkmarkView, previewDetailsView)
let padding: CGFloat = 5
NSLayoutConstraint.activate([
//Constrain the checkmark image view to the top left with a fixed height and width
checkmarkImageView.widthAnchor.constraint(equalToConstant: 24),
checkmarkImageView.heightAnchor.constraint(equalTo: checkmarkImageView.widthAnchor),
checkmarkImageView.centerYAnchor.constraint(equalTo: checkmarkView.centerYAnchor),
checkmarkImageView.centerXAnchor.constraint(equalTo: checkmarkView.centerXAnchor),
checkmarkView.widthAnchor.constraint(equalToConstant: 30),
checkmarkView.heightAnchor.constraint(equalTo: checkmarkView.widthAnchor),
checkmarkView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
checkmarkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding)
])
self.layoutIfNeeded()
}
//MARK: - Configure cell with data
func configure(with annotation: AnnotationsController.Annotation) {
titleLabelPreview.text = annotation.title
titleLabelDetails.text = annotation.title
detailsLabelShort.text = annotation.details
detailsLabelLong.text = annotation.details
checkmarkImageView.image = annotation.complete ? ProjectImages.Annotation.checkmark : nil
lastEditedLabel.text = annotation.lastEdited.customMediumToString
mapImageLabel.text = annotation.mapImage?.title ?? "No map image attached"
}
}
Ok, got it fixed, a fully expanding tableview cell. Key things are invalidating the layout in the custom cell class and calling beginUpdates() and endUpdates() on the tableView!
In my viewController:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Expandable cell
guard let cell = tableView.cellForRow(at: indexPath) as? AnnotationCell else { return }
cell.toggleFullView()
tableView.beginUpdates()
tableView.endUpdates()
}
and my custom cell class with the toggleFullView() method:
class AnnotationCell: UITableViewCell, SelfConfiguringAnnotationCell {
//MARK: - Properties
private let titleLabelPreview = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .headline))
private let titleLabelDetails = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .headline))
private let detailsLabelShort = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .subheadline), numberOfLines: 2)
private let detailsLabelLong = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .subheadline), numberOfLines: 0)
private let mapImageLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .footnote), andColor: .tertiarySystemGroupedBackground)
private let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .footnote), andColor: .tertiarySystemGroupedBackground)
private let checkmarkImageView = UIImageView()
private var checkmarkView = UIView()
private var previewDetailsView = UIStackView()
private var fullDetailsView = UIStackView()
let padding: CGFloat = 5
var showFullDetails = false
//MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureContents()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Actions
///Expand and collapse the cell
func toggleFullView() {
//Show the full contents
print("ShowFullDetails = \(showFullDetails.description.uppercased())")
if showFullDetails {
print("Show full contents")
if contentView.subviews.contains(previewDetailsView) {
previewDetailsView.removeFromSuperview()
}
if !contentView.subviews.contains(fullDetailsView) {
contentView.addSubview(fullDetailsView)
}
NSLayoutConstraint.activate([
fullDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
fullDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
fullDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
fullDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
])
//Show preview contents
} else {
print("Show preview contents")
if contentView.subviews.contains(fullDetailsView) {
fullDetailsView.removeFromSuperview()
}
if !contentView.subviews.contains(previewDetailsView) {
contentView.addSubview(previewDetailsView)
}
NSLayoutConstraint.activate([
previewDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
previewDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
previewDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
previewDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
showFullDetails.toggle()
//Invalidate current layout &
self.setNeedsLayout()
}
override func prepareForReuse() {
//Make sure reused cells start in the preview mode!
// showFullDetails = false
}
override func layoutIfNeeded() {
super.layoutIfNeeded()
NSLayoutConstraint.activate([
//Constrain the checkmark image view to the top left with a fixed height and width
checkmarkImageView.widthAnchor.constraint(equalToConstant: 24),
checkmarkImageView.heightAnchor.constraint(equalTo: checkmarkImageView.widthAnchor),
checkmarkImageView.centerYAnchor.constraint(equalTo: checkmarkView.centerYAnchor),
checkmarkImageView.centerXAnchor.constraint(equalTo: checkmarkView.centerXAnchor),
checkmarkView.widthAnchor.constraint(equalToConstant: 30),
checkmarkView.heightAnchor.constraint(equalTo: checkmarkView.widthAnchor),
checkmarkView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
checkmarkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding)
])
}
//MARK: - Layout
private func configureContents() {
//Setup Views
backgroundColor = .clear
separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
selectionStyle = .none
detailsLabelShort.adjustsFontSizeToFitWidth = false
detailsLabelLong.adjustsFontSizeToFitWidth = false
checkmarkView.translatesAutoresizingMaskIntoConstraints = false
checkmarkView.addSubview(checkmarkImageView)
checkmarkImageView.tintColor = .systemOrange
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
previewDetailsView = UIStackView(arrangedSubviews: [titleLabelPreview, detailsLabelShort])
previewDetailsView.axis = .vertical
previewDetailsView.translatesAutoresizingMaskIntoConstraints = false
previewDetailsView.addBackground(.blue)
fullDetailsView = UIStackView(arrangedSubviews: [titleLabelDetails, detailsLabelLong, mapImageLabel, lastEditedLabel])
fullDetailsView.axis = .vertical
fullDetailsView.translatesAutoresizingMaskIntoConstraints = false
fullDetailsView.addBackground(.green)
//By default only show the preview View
contentView.addSubviews(checkmarkView)
//Setup preview/DetailView
toggleFullView()
}
//MARK: - Configure cell with data
func configure(with annotation: AnnotationsController.Annotation) {
titleLabelPreview.text = annotation.title
titleLabelDetails.text = annotation.title
detailsLabelShort.text = annotation.details
detailsLabelLong.text = annotation.details
checkmarkImageView.image = annotation.complete ? ProjectImages.Annotation.checkmark : nil
lastEditedLabel.text = annotation.lastEdited.customMediumToString
mapImageLabel.text = annotation.mapImage?.title ?? "No map image attached"
}
}
HTH!
Related
Continuing to resolve the task of UITableViewCell, I could fix all UIButtons side by side but they are not appearing properly in executing time. Sometimes the second row mix with the third.
I tryied with cell.layoutIfNeeded() but still wrong.
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! DetailViewCell
let array = sections[indexPath.section].filters
var hStackView = UIStackView()
var vStackView = UIStackView()
vStackView.axis = .vertical
vStackView.distribution = .fillEqually
vStackView.spacing = 12
vStackView.alignment = .top
for (index, item) in array.enumerated() {
let btn = UIButton()
btn.setTitle(item.title, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 12)
btn.setTitleColor(.black, for: .normal)
btn.backgroundColor = UIColor(red: 220, green: 220, blue: 220)
btn.layer.cornerRadius = 15;
btn.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
btn.tag = index
if index % 2 == 0 {
hStackView = UIStackView()
hStackView.axis = .horizontal
hStackView.spacing = 12
hStackView.alignment = .fill
hStackView.distribution = .fillEqually
hStackView.addArrangedSubview(btn)
if (index + 1) == array.count {
vStackView.addArrangedSubview(hStackView)
hStackView.leadingAnchor.constraint(equalTo: vStackView.leadingAnchor).isActive = true
hStackView.widthAnchor.constraint(equalTo: vStackView.widthAnchor, multiplier: 0.5).isActive = true
}
}
else {
hStackView.addArrangedSubview(btn)
vStackView.addArrangedSubview(hStackView)
hStackView.leadingAnchor.constraint(equalTo: vStackView.leadingAnchor).isActive = true
hStackView.trailingAnchor.constraint(equalTo: vStackView.trailingAnchor).isActive = true
}
}
cell.addSubview(vStackView)
vStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cell.leadingAnchor.constraint(equalTo: vStackView.leadingAnchor, constant: -8),
cell.trailingAnchor.constraint(equalTo: vStackView.trailingAnchor, constant: 8),
cell.topAnchor.constraint(equalTo: vStackView.topAnchor, constant: -8),
cell.bottomAnchor.constraint(equalTo: vStackView.bottomAnchor, constant: 8)
])
cell.layoutIfNeeded()
return cell
You should look up information about dequeueReusableCell
It works somehow like this: when you get a cell in tableView(_: cellForRowAt:) from dequeueReusableCell you are provided with a ready-made cell that was used somewhere. It follows from this that all content that the cells differ in must be placed in the cell only in this method
Note, the button with a label сссс is clearly from the top cell
I would advise you to declare in your UITableViewCell subclass a method in which you put content inside and call it every time inside tableView(_:cellForRowAt:)
You can use a storyboard if you like, but if you want to do it with code, I can suggest custom UITableViewCell subclass template:
class CustomTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
// MARK: - Public methods
func setContent(_ content: <# ContentType #>) {
}
// MARK: - Private setup methods
private func setupView() {
makeConstraints()
}
private func makeConstraints() {
NSLayoutConstraint.activate([
])
}
}
Friends, I need help, I spent 2 days on solving this issue. I need to make my ScrollView dynamically change its height based on the amount of text I add to the tableView. The problem is that if I don't strictly set the scroll.contentSize.height height, then the ability to scroll the screen is disabled.
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.contentSize.height = 4000
scroll.backgroundColor = UIColor.white
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
func setConstraints() {
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
NSLayoutConstraint.activate([
headLabel.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
headLabel.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 45)
])
NSLayoutConstraint.activate([
ellipseBabyImage.topAnchor.constraint(equalTo: stackViewName.bottomAnchor, constant: 75),
ellipseBabyImage.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
ellipseBabyImage.heightAnchor.constraint(equalToConstant: 131.3),
ellipseBabyImage.widthAnchor.constraint(equalToConstant: 145)
])
NSLayoutConstraint.activate([
goButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
goButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40),
goButton.leadingAnchor.constraint(equalTo: view.leadingAnchor,constant: 20),
goButton.trailingAnchor.constraint(equalTo: view.trailingAnchor,constant: -20),
goButton.heightAnchor.constraint(equalToConstant: 48)
])
NSLayoutConstraint.activate([
characterLabel.topAnchor.constraint(equalTo: ellipseBabyImage.bottomAnchor, constant: 68.35),
characterLabel.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
])
NSLayoutConstraint.activate([
stackViewName.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
stackViewName.topAnchor.constraint(equalTo: headLabel.topAnchor,constant: 44),
])
NSLayoutConstraint.activate([
leftBlockStack.topAnchor.constraint(equalTo: fullNameLabel.topAnchor, constant: 60),
leftBlockStack.leftAnchor.constraint(equalTo: view.leftAnchor,constant: 28),
leftBlockStack.rightAnchor.constraint(equalTo: ellipseBabyImage.leftAnchor,constant: -10)
])
NSLayoutConstraint.activate([
rightBlockStack.topAnchor.constraint(equalTo: fullNameLabel.topAnchor, constant: 60),
rightBlockStack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -28),
rightBlockStack.leftAnchor.constraint(equalTo: ellipseBabyImage.rightAnchor,constant: 10)
])
NSLayoutConstraint.activate([
arrayStackCharacteristicOne.topAnchor.constraint(equalTo: characterLabel.bottomAnchor,constant: 16),
arrayStackCharacteristicOne.centerXAnchor.constraint(equalTo: view.centerXAnchor),
arrayStackCharacteristicOne.leftAnchor.constraint(equalTo: scrollView.leftAnchor,constant: 22),
healthProgressView.heightAnchor.constraint(equalToConstant: 5),
healthProgressView.widthAnchor.constraint(equalToConstant: 90),
willpowerProgressView.heightAnchor.constraint(equalToConstant: 5),
willpowerProgressView.widthAnchor.constraint(equalToConstant: 90),
luckProgressView.heightAnchor.constraint(equalToConstant: 5),
luckProgressView.widthAnchor.constraint(equalToConstant: 90),
])
NSLayoutConstraint.activate([
arrayStackCharacteristicTwo.topAnchor.constraint(equalTo: arrayStackCharacteristicOne.bottomAnchor,constant: 16),
arrayStackCharacteristicTwo.centerXAnchor.constraint(equalTo: view.centerXAnchor),
arrayStackCharacteristicTwo.leftAnchor.constraint(equalTo: scrollView.leftAnchor,constant: 22),
talentProgressView.heightAnchor.constraint(equalToConstant: 5),
talentProgressView.widthAnchor.constraint(equalToConstant: 90),
successProgressView.heightAnchor.constraint(equalToConstant: 5),
successProgressView.widthAnchor.constraint(equalToConstant: 90),
creationProgressView.heightAnchor.constraint(equalToConstant: 5),
creationProgressView.widthAnchor.constraint(equalToConstant: 90),
])
NSLayoutConstraint.activate([
descriptionView.topAnchor.constraint(equalTo: arrayStackCharacteristicTwo.topAnchor, constant: 50),
descriptionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
descriptionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
descriptionView.bottomAnchor.constraint(equalTo: view.bottomAnchor,constant: 0),
])
class DescriptionView: UIView {
private let characteristicModel = CharacteristicModel()
private let descriptionIdentifier = "CELL"
private let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.backgroundColor = .none
tableView.bounces = false
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.isScrollEnabled = false
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setDelegates()
setupViews()
setupConstraints()
tableView.sizeToFit()
tableView.register(DescriptionTableViewCell.self, forCellReuseIdentifier: "CELL")
}
required init?(coder: NSCoder) {
fatalError()
}
private func setDelegates() {
tableView.delegate = self
tableView.dataSource = self
}
private func setupViews() {
addSubview(tableView)
translatesAutoresizingMaskIntoConstraints = false
}
}
//MARK: - UI TableViewDataSource
extension DescriptionView: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
characteristicModel.title.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL",for: indexPath) as! DescriptionTableViewCell
let descriptionTitle = characteristicModel.title[indexPath.row]
let descriptionText = characteristicModel.description[indexPath.row]
var content = cell.defaultContentConfiguration()
content.textProperties.color = UIColor(red: 0.729, green: 0.443, blue: 0.443, alpha: 1)
content.textProperties.alignment = .center
content.textProperties.font = UIFont(name: "Lato-SemiBold", size: 22) ?? .systemFont(ofSize: 22)
content.text = descriptionTitle
content.secondaryText = descriptionText
content.secondaryTextProperties.font = UIFont(name: "Lato-SemiBold", size: 16) ?? .systemFont(ofSize: 16)
content.secondaryTextProperties.alignment = .justified
cell.contentConfiguration = content
return cell
}
}
extension DescriptionView {
private func setupConstraints() {
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: topAnchor,constant: 5),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor,constant: 5),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor,constant: -5),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor,constant: -5)
])
}
}
I tried to use different approaches that I found on the Internet, but unfortunately I could not solve it. Either scroll is disabled for me, or the scroll width does not change automatically.
The screenshot shows that I use a tableView for the text that will be automatically added from the API, everything else is uiScrollView. tableView should have scroll disabled and scrollView should scroll to the end of the text
The application should look like this.
Set your scrollview and tableview height to 0 and try adding this to your viewcontroller. you can update the height of the scrollview through viewDidLayoutSubviews.
override func viewDidLayoutSubviews() {
DispatchQueue.main.async {
self.scrollView.contentSize.height = self.descriptionView.tableView.contentSize.height + otherViews.frame.height
}
}
you can also add an observer to the tableview whenever it updates its contentsize
override func viewDidLoad() {
descriptionView.tableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let obj = object as? UITableView {
if obj == descriptionView.tableView && keyPath == "contentSize" {
if let newSize = change?[.newKey] as? CGSize {
descriptionView.tableViewHeightConstraint.constant = newSize.height
view.layoutIfNeeded()
}
}
}
}
I have been unable to figure out how to solve this issue of mine. I tried to follow the answer from this post. How to change UIView height based on elements inside it
Like the post answer says to do, I have:
set autolayout constraints between UIContainerView top to the UITextView top and UIContainerView bottom to the UITextView bottom (#1)
set height constraint on the text view (#2) and change its constant when resizing the text view (#3)
I have to do this all programmatically. I first set the frame for the container view and give it a specified height. I'm not sure if that is okay too. I also add (#1) in viewDidLoad and am unsure if that's correct.
The text view is not able to increase height either with the current constraints (it is able to if I remove the topAnchor constraint but the container view still doesn't change size).
class ChatController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
lazy var containerView: UIView = {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height * 0.075)
return containerView
}()
lazy var textView: UITextView = {
let textView = UITextView()
textView.text = "Enter message..."
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
textView.delegate = self
return textView
}()
override func viewDidLoad() {
super.viewDidLoad()
...
textViewDidChange(self.textView)
addContainerSubViews()
(#1)
containerView.topAnchor.constraint(equalTo: self.textView.topAnchor, constant: -UIScreen.main.bounds.size.height * 0.075 * 0.2).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.textView.bottomAnchor, constant: UIScreen.main.bounds.size.height * 0.075 * 0.2).isActive = true
}
func addContainerSubViews() {
let height = UIScreen.main.bounds.size.height
let width = UIScreen.main.bounds.size.width
let containerHeight = height * 0.075
...//constraints for imageView and sendButton...
containerView.addSubview(self.textView)
self.textView.leftAnchor.constraint(equalTo: imageView.rightAnchor, constant: width/20).isActive = true
self.textView.rightAnchor.constraint(equalTo: sendButton.leftAnchor, constant: -width/20).isActive = true
(#2)
self.textView.heightAnchor.constraint(equalToConstant: containerHeight * 0.6).isActive = true
}
override var inputAccessoryView: UIView? {
get {
return containerView
}
}
(#3)
func textViewDidChange(_ textView: UITextView) {
let size = CGSize(width: view.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
You can do this all with auto-layout / constraints. Because a UITextView with scrolling disabled will "auto-size" its height based on the text, no need to calculate height and change constraint constant.
Here's an example -- it's from a previous answer, modified to include your image view and send button:
class ViewController: UIViewController {
let testLabel: InputLabel = InputLabel()
override func viewDidLoad() {
super.viewDidLoad()
let instructionLabel = UILabel()
instructionLabel.textAlignment = .center
instructionLabel.text = "Tap yellow label to edit..."
let centeringFrameView = UIView()
// label properties
let fnt: UIFont = .systemFont(ofSize: 32.0)
testLabel.isUserInteractionEnabled = true
testLabel.font = fnt
testLabel.adjustsFontSizeToFitWidth = true
testLabel.minimumScaleFactor = 0.25
testLabel.numberOfLines = 2
testLabel.setContentHuggingPriority(.required, for: .vertical)
let minLabelHeight = ceil(fnt.lineHeight)
// so we can see the frames
centeringFrameView.backgroundColor = .red
testLabel.backgroundColor = .yellow
[centeringFrameView, instructionLabel, testLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(instructionLabel)
view.addSubview(centeringFrameView)
centeringFrameView.addSubview(testLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// instruction label centered at top
instructionLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
instructionLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// centeringFrameView 20-pts from instructionLabel bottom
centeringFrameView.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: 20.0),
// Leading / Trailing with 20-pts "padding"
centeringFrameView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
centeringFrameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// test label centered vertically in centeringFrameView
testLabel.centerYAnchor.constraint(equalTo: centeringFrameView.centerYAnchor, constant: 0.0),
// Leading / Trailing with 20-pts "padding"
testLabel.leadingAnchor.constraint(equalTo: centeringFrameView.leadingAnchor, constant: 20.0),
testLabel.trailingAnchor.constraint(equalTo: centeringFrameView.trailingAnchor, constant: -20.0),
// height will be zero if label has no text,
// so give it a min height of one line
testLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: minLabelHeight),
// centeringFrameView height = 3 * minLabelHeight
centeringFrameView.heightAnchor.constraint(equalToConstant: minLabelHeight * 3.0)
])
// to handle user input
testLabel.editCallBack = { [weak self] str in
guard let self = self else { return }
self.testLabel.text = str
}
testLabel.doneCallBack = { [weak self] in
guard let self = self else { return }
// do something when user taps done / enter
}
let t = UITapGestureRecognizer(target: self, action: #selector(self.labelTapped(_:)))
testLabel.addGestureRecognizer(t)
}
#objc func labelTapped(_ g: UITapGestureRecognizer) -> Void {
testLabel.becomeFirstResponder()
testLabel.inputContainerView.theTextView.text = testLabel.text
testLabel.inputContainerView.theTextView.becomeFirstResponder()
}
}
class InputLabel: UILabel {
var editCallBack: ((String) -> ())?
var doneCallBack: (() -> ())?
override var canBecomeFirstResponder: Bool {
return true
}
override var canResignFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
get { return inputContainerView }
}
lazy var inputContainerView: CustomInputAccessoryView = {
let v = CustomInputAccessoryView()
v.editCallBack = { [weak self] str in
guard let self = self else { return }
self.editCallBack?(str)
}
v.doneCallBack = { [weak self] in
guard let self = self else { return }
self.resignFirstResponder()
}
return v
}()
}
class CustomInputAccessoryView: UIView, UITextViewDelegate {
var editCallBack: ((String) -> ())?
var doneCallBack: (() -> ())?
let theTextView: UITextView = {
let tv = UITextView()
tv.isScrollEnabled = false
tv.font = .systemFont(ofSize: 16)
tv.autocorrectionType = .no
tv.returnKeyType = .done
return tv
}()
let imgView: UIImageView = {
let v = UIImageView()
v.contentMode = .scaleAspectFit
v.clipsToBounds = true
return v
}()
let sendButton: UIButton = {
let v = UIButton()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .lightGray
autoresizingMask = [.flexibleHeight, .flexibleWidth]
if let img = UIImage(named: "testImage") {
imgView.image = img
} else {
imgView.backgroundColor = .systemBlue
}
let largeConfig = UIImage.SymbolConfiguration(pointSize: 22, weight: .regular, scale: .large)
let buttonImg = UIImage(systemName: "paperplane.fill", withConfiguration: largeConfig)
sendButton.setImage(buttonImg, for: .normal)
[theTextView, imgView, sendButton].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
}
// if we want to see the image view and button frames
//[imgView, sendButton].forEach { v in
// v.backgroundColor = .systemYellow
//}
NSLayoutConstraint.activate([
// constrain image view 40x40 with 8-pts leading
imgView.widthAnchor.constraint(equalToConstant: 40.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
imgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
// constrain image view 40x40 with 8-pts trailing
sendButton.widthAnchor.constraint(equalToConstant: 40.0),
sendButton.heightAnchor.constraint(equalTo: sendButton.widthAnchor),
sendButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// constrain text view with 10-pts from
// image view trailing
// send button leading
theTextView.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 10),
theTextView.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -10),
// constrain image view and button
// centered vertically
// at least 8-pts top and bottom
imgView.centerYAnchor.constraint(equalTo: centerYAnchor),
sendButton.centerYAnchor.constraint(equalTo: centerYAnchor),
imgView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 8.0),
sendButton.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 8.0),
imgView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
sendButton.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
// constrain text view 8-pts top/bottom
theTextView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
theTextView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
theTextView.delegate = self
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
doneCallBack?()
}
return true
}
func textViewDidChange(_ textView: UITextView) {
editCallBack?(textView.text ?? "")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return .zero
}
}
Output:
I have created a view in my View controller which has UISegmentControl and a UIScrollView
let segmentControl : UISegmentedControl = {
let segmentItems = ["Personal","Statistics","Calendar"]
let segmentControl = UISegmentedControl(items: segmentItems)
segmentControl.selectedSegmentIndex = 0
segmentControl.addTarget(self, action: #selector(selectIn(sender:)), for: .valueChanged)
segmentControl.translatesAutoresizingMaskIntoConstraints = false
return segmentControl
}()
let subView : UIScrollView = {
let view = UIScrollView()
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
In viewDidLoad I added
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.isHidden = true
view.addSubview(segmentControl)
view.addSubview(subView)
setLayout()
}
and added layout as follows
func setLayout(){
NSLayoutConstraint.activate([
segmentControl.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
segmentControl.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
segmentControl.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
segmentControl.heightAnchor.constraint(equalToConstant: 30),
subView.topAnchor.constraint(equalTo: segmentControl.bottomAnchor, constant: 10),
subView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
subView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
subView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
and tried to add PersonalView (UIView) when any segmentControl is pressed
#objc func selectIn(sender: UISegmentedControl){
subView.addSubview(pvc)
}
My personalView is as follows
class PersonalView: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UITableViewDelegate, UITableViewDataSource {
let tempValueForTable : Int = 10
let todayLabel : UILabel = {
let label = UILabel()
label.text = "Today"
label.font = .montserratSemiBold
label.textAlignment = .center
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(todayLabel)
setLayout()
}
private func setLayout(){
NSLayoutConstraint.activate([
todayLabel.topAnchor.constraint(equalTo: self.topAnchor),
todayLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20),
todayLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
todayLabel.heightAnchor.constraint(equalToConstant: 25),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}}
Still I am not able to get view and in further update I need to add more view in same ScrollView
1.when you are using auto layout constraints you must set this false after add.
SomeView.translatesAutoresizingMaskIntoConstraints = false
2.After you add your subView you must set its constraints and call view.layoutIfNeeded()
subView.addSubview(pvc)
pvc.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pvc.trailingAnchor.constraint(equalTo: subView.trailingAnchor),
pvc.leadingAnchor.constraint(equalTo: subView.leadingAnchor),
pvc.topAnchor.constraint(equalTo: subView.topAnchor),
pvc.bottomAnchor.constraint(equalTo: subView.bottomAnchor),
])
view.layoutIfNeeded()
I have a UITableView that displays comments. The last cell always has the same height as the tallest cell in the UITableView and I do not know why. With reference to the image below, it can be seen that the height of the last custom object (LabelWithPadding) that holds the comment is the same height as the tallest custom object in the same table. Please can someone advise?
class CommentOnPostTableView: UITableView, UITableViewDataSource, UITableViewDelegate{
var items: [Comment]?
var post: ResearchPost?
let cellIDB = "commentCellId"
init(frame: CGRect, style: UITableView.Style, sourceData: [Comment], postContent: ResearchPost) {
super.init(frame: frame, style: style)
items = sourceData
post = postContent
self.dataSource = self
self.delegate = self
self.register(CommentTableViewCell.self, forCellReuseIdentifier: cellIDB)
//self.separatorStyle = .none
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
/**Delegate functions*/
extension CommentOnPostTableView{
/***Number of cells*/
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let dataSource = items{
return dataSource.count
}else{
return 0
}
}
/***Create Cell*/
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let dataSource = items else{return UITableViewCell.init()}
let cell = tableView.dequeueReusableCell(withIdentifier: cellIDB, for: indexPath) as! CommentTableViewCell
cell.dataObject = dataSource[indexPath.row]
return cell
}
class CommentTableViewCell: UITableViewCell {
var commentLabel: LabelWithPadding!
var imgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = UIColor.black
view.contentMode = .scaleAspectFit
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var nameLabel: UILabel = {
let label = UILabel()
label.font = .boldSystemFont(ofSize: 11)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var dataObject: Comment?{
didSet{
if let data = dataObject{
guard
let image = data.imageOfCommentor,
let name = data.name,
let comment = data.comment else{return}
imgView.image = image
nameLabel.text = name
let padding = UIEdgeInsets.init(top: 3, left: 5, bottom: 3, right: 5)
commentLabel = LabelWithPadding.init(frame: .zero, with: padding)
commentLabel.translatesAutoresizingMaskIntoConstraints = false
commentLabel.label.text = comment
setupViews()
}
}
}
....
// Auto layout constraints
private func setupViews(){
let c = contentView.safeAreaLayoutGuide
contentView.addSubview(imgView)
contentView.addSubview(nameLabel)
contentView.addSubview(commentLabel)
imgView.topAnchor.constraint(equalTo: c.topAnchor, constant: borderSpace).isActive = true
imgView.leadingAnchor.constraint(equalTo: c.leadingAnchor, constant: borderSpace).isActive = true
imgView.widthAnchor.constraint(equalTo: c.widthAnchor, multiplier: 0.08).isActive = true
imgView.heightAnchor.constraint(equalTo: c.widthAnchor, multiplier: 0.08).isActive = true
nameLabel.topAnchor.constraint(equalTo: c.topAnchor, constant: borderSpace).isActive = true
nameLabel.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 10).isActive = true
nameLabel.trailingAnchor.constraint(equalTo: c.trailingAnchor, constant: -borderSpace).isActive = true
commentLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: viewSpace).isActive = true
commentLabel.widthAnchor.constraint(equalTo: nameLabel.widthAnchor).isActive = true
commentLabel.trailingAnchor.constraint(equalTo: c.trailingAnchor, constant: -borderSpace).isActive = true
commentLabel.bottomAnchor.constraint(equalTo: c.bottomAnchor).isActive = true
commentLabel.label.font = UIFont.systemFont(ofSize: 12)
commentLabel.label.numberOfLines = 0
commentLabel.label.lineBreakMode = .byWordWrapping
commentLabel.layer.cornerRadius = 7
commentLabel.layer.masksToBounds = true
commentLabel.backgroundColor = UIColor.appGrayExtraLightGray
}
}
Custom Class:
class LabelWithPadding: UIView {
var padding: UIEdgeInsets!
let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
init(frame: CGRect, with padding: UIEdgeInsets) {
super.init(frame: frame)
self.padding = padding
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView(){
self.addSubview(label)
label.topAnchor.constraint(equalTo: self.topAnchor, constant: padding.top).isActive = true
label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding.bottom).isActive = true
label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding.left).isActive = true
label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding.bottom).isActive = true
}
}
First of all, I couldn't find where you update constraints.
When you use tableView.dequeueReusableCell, you may get the old cell, so you should update old constraints (or remove old constratints and add new ones). You may read it in docs.
You should add variable to store old constraints:
var oldConstraints: [NSLayoutConstraint] = []
And should deactivate old constraints and add new constraints in setupView():
NSLayoutConstraint.deactivate(oldConstraints)
let constraints = [
imgView.topAnchor.constraint(equalTo: c.topAnchor, constant: borderSpace),
imgView.leadingAnchor.constraint(equalTo: c.leadingAnchor, constant: borderSpace),
imgView.widthAnchor.constraint(equalTo: c.widthAnchor, multiplier: 0.08),
imgView.heightAnchor.constraint(equalTo: c.widthAnchor, multiplier: 0.08),
nameLabel.topAnchor.constraint(equalTo: c.topAnchor, constant: borderSpace),
nameLabel.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 10),
nameLabel.trailingAnchor.constraint(equalTo: c.trailingAnchor, constant: -borderSpace),
commentLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: viewSpace),
commentLabel.widthAnchor.constraint(equalTo: nameLabel.widthAnchor),
commentLabel.trailingAnchor.constraint(equalTo: c.trailingAnchor, constant: -borderSpace),
commentLabel.bottomAnchor.constraint(equalTo: c.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
oldConstraints = constraints
Also you shouldn't add subviews every time you update dataObject in CommentTableViewCell, because they could have already been added.
In conclusion, you should add subview to contentView just once and update the constraints every time you update dataObject. I gave you an example how to solve your issue.
You could also use prepareForReuse() to prepare the cell for reuse:
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:.