Some tableView rows don't show unless I scroll but then disappear - ios

I'm following (I did make some minor changes in how I do the constraints of the tableViewCell, I'm using a stackView instead) this RayWenderlich tutorial. It's about dynamic self sizing tableViewcells. My problem is that my tableView initially loads with 3 rows (on iPhone 6s) or starts with 2 rows on iPhone 7 Plus. FYI when I debug, my dataSource's count shows that it's 4 rows. If I scroll back and forth a few times then maybe one other row would show up, or maybe both or maybe one would show but one other would just vanish!
In the gif below it loads the screen initially with 3 rows, then when I scroll it shows only 2 rows then again it shows 3 rows, but the 3rd is different this time. It should always be showing 4 rows!
My Models are a Work struct
struct Work {
let title: String
let image: UIImage
let info: String
var isExpanded: Bool
}
and an Artist struct:
struct Artist {
let name: String
let bio: String
let image: UIImage
var works: [Work]
init(name: String, bio: String, image: UIImage, works: [Work]) {
self.name = name
self.bio = bio
self.image = image
self.works = works
}
}
My WorkTableViewCell is as below:
import UIKit
class WorkTableViewCell: UITableViewCell {
static let textViewText = "select for more info >"
var work : Work! {
didSet{
titleLabel.text = work.title
workImageView.image = work.image
}
}
lazy var titleLabel : UILabel = {
let label = UILabel()
label.backgroundColor = .cyan
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var workImageView : UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
// imageView.setContentHuggingPriority(1000, for: .vertical)
// imageView.setContentHuggingPriority(2, for: .vertical)
return imageView
}()
lazy var moreInfoTextView : UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.textAlignment = .center
textView.isScrollEnabled = false
return textView
}()
//
// override func prepareForReuse() {
// super.prepareForReuse()
//
// imageView?.image = nil
//
// }
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addConstraints()
}
private func addConstraints(){
let stackView = UIStackView(arrangedSubviews: [workImageView, titleLabel, moreInfoTextView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.alignment = .center
contentView.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
}
}
And this is the cellForRowAt method of my tableView:
extension ArtistDetailViewController: UITableViewDataSource{
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! WorkTableViewCell
cell.work = works[indexPath.row]
cell.moreInfoTextView.text = works[indexPath.row].isExpanded ? works[indexPath.row].info : WorkTableViewCell.textViewText
cell.moreInfoTextView.textAlignment = works[indexPath.row].isExpanded ? .left : .center
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return works.count
}
}
I also did add tableView.reloadData() inside the viewWillAppear and viewDidAppear but nothing changed.

So changing the distribution of the stackView was the fix.
I just had to change:
stackView.distribution = .fillProportionally
to:
stackView.distribution = .fill
I'm not sure I understand why that's necessary though.

Related

Stackview inside collectionview or uitableview

I am trying to achieve a layout of a text followed by an image (image height calculated based on aspect ratio) then followed by text and so on. The issue is that the stackview that I am adding the views into randomly squash the views sometimes the imageviews disappear some time the text, it doesn't have a consistent behaviour.
i tried it on both uitableview and uicolletion view and the result is the same. is the combination of the mentioned views considered as a best practice for such usecase or not ? and if not what might be the best practice for such thing ?
class MyStackyView: UIStackView {
// Main variables
weak var videoPlayerDelegate: AVPlayerViewDelegate?
private var avVideoPlayersVC: [AVPlayerViewController] = []
var content: [Content]! {
didSet {
contentCombined = Utility.shared.combineToNew(contents: content)
}
}
private var contentCombined: [Content] = [] {
didSet {
populatePostContent()
}
}
var contentViews: [UIView] = [] // Holds the views created
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("DiaryPostView:: Deinitalized")
}
private func configureView() {
axis = .vertical
distribution = .fill
alignment = .fill
spacing = 0
}
}
// Extension to populate post content
extension MyStackyView {
private func populatePostContent() {
for content in contentCombined {
if content.isMedia {
addMedia(content)
} else {
addText(content.text)
}
}
}
}
// Extension to add the required views
extension MyStackyView {
private func addText(_ text: String?, place: MediaPlace = .center) {
let textView = generateDefaultTextView()
//let parsedText = HTMLParser.shared.parseHTMLToAttributed(string: text ?? "") // fix font issue
switch place {
case .center:
append(textView)
contentViews.append(textView)
}
textView.text = text
// لما استخدم ال parsedtext مرة النص بطلع مع الfont و مرة لا
}
private func addMedia(_ content: Content) {
let avPlayerVC = getAVPlayerViewController()
let mediaView = generateDefaultMediaView()
switch content.getRawPlace() {
case .center:
append(mediaView)
contentViews.append(mediaView)
addText(content.text)
NetworkManager().downloadMedia(content.img!, into: mediaView, avPlayerViewController: avPlayerVC) {
}
}
}
}
extension MyStackyView {
private func generateDefaultTextView() -> UILabel {
let textView = UILabel()
textView.backgroundColor = .clear
textView.numberOfLines = 0
textView.font = UIFont.customFont(.openSans, .regular, .title1, 17)
return textView
}
private func generateDefaultHorizontalStack() -> UIStackView {
let horizontalStack = UIStackView()
horizontalStack.axis = .horizontal
horizontalStack.distribution = .fill
horizontalStack.alignment = .fill
return horizontalStack
}
private func generateDefaultMediaView() -> MediaSliderView {
let mediaSliderView = MediaSliderView()
return mediaSliderView
}
private func getAVPlayerViewController() -> AVPlayerViewController? {
videoPlayerDelegate?.getAVPlayerVC?()
}
func deallocateAVPlayers() {
for player in avVideoPlayersVC {
player.removeFromParent()
}
avVideoPlayersVC.removeAll()
}
}
i initalize a variable of the class in my uitableviewcell and then add these constraints
contentView.addSubview(MyStackyView)
MyStackyView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8).isActive = true
MyStackyView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8).isActive = true
MyStackyView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16).isActive = true
MyStackyView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16).isActive = true
please if possible, i need some guidance about this issue.
thank you, appreciate the help
Here is a (fairly) basic example.
We'll use a data structure like this:
struct VarDevStruct {
var first: String = ""
var second: String = ""
var imageName: String = ""
}
The cell class has a vertical stack view containing:
multiline label
horizontal stack view
with an 80x80 image view and a label
multiline label
If any of the elements in the data struct are empty strings, we'll set the corresponding element in the cell to hidden.
First, the result:
after scrolling down to a few rows with different data:
and rotated:
Here's the complete code... plenty of comments in it, so it should be clear what the code is doing.
Data Structure
struct VarDevStruct {
var first: String = ""
var second: String = ""
var imageName: String = ""
}
Cell class
class VarDevCell: UITableViewCell {
let firstLabel = UILabel()
let secondLabel = UILabel()
let imgView = UIImageView()
let imgNameLabel = UILabel()
let vStack = UIStackView()
let hStack = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// stack view properties
vStack.axis = .vertical
vStack.alignment = .fill
vStack.distribution = .fill
vStack.spacing = 8
vStack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(vStack)
// let's use the default cell margins
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
// constrain stack view to all 4 sides
vStack.topAnchor.constraint(equalTo: g.topAnchor),
vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
vStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
// subview properties
// background colors to make it easy to see the frames
firstLabel.backgroundColor = .yellow
secondLabel.backgroundColor = .green
imgView.backgroundColor = .red
imgNameLabel.backgroundColor = .cyan
// multi-line labels
firstLabel.numberOfLines = 0
secondLabel.numberOfLines = 0
imgNameLabel.textAlignment = .center
// image view defaults to scaleToFill
// let's set it to scaleAspectFit
imgView.contentMode = .scaleAspectFit
// horizontal stack view
hStack.axis = .horizontal
hStack.alignment = .center
hStack.distribution = .fill
hStack.spacing = 8
// add subviews to horizontal stack view
hStack.addArrangedSubview(imgView)
hStack.addArrangedSubview(imgNameLabel)
// let's fill the vertical stack view with
// label
// hStack with 80x80 imageview and label with image name
// label
vStack.addArrangedSubview(firstLabel)
vStack.addArrangedSubview(hStack)
vStack.addArrangedSubview(secondLabel)
// set image view width and height
imgView.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 1.0).isActive = true
}
func fillData(_ vdStruct: VarDevStruct) -> Void {
firstLabel.text = vdStruct.first
secondLabel.text = vdStruct.second
imgNameLabel.text = vdStruct.imageName
// does our data have an image name?
if !vdStruct.imageName.isEmpty {
if #available(iOS 13.0, *) {
if let img = UIImage(systemName: vdStruct.imageName) {
imgView.image = img
}
} else {
// Fallback on earlier versions
if let img = UIImage(named: vdStruct.imageName) {
imgView.image = img
}
}
}
// hide elements that we don't need in this cell
firstLabel.isHidden = vdStruct.first.isEmpty
secondLabel.isHidden = vdStruct.second.isEmpty
hStack.isHidden = vdStruct.imageName.isEmpty
}
}
Controller class
class VarDevTableViewController: UITableViewController {
var myData: [VarDevStruct] = []
override func viewDidLoad() {
super.viewDidLoad()
// register cell class for reuse
tableView.register(VarDevCell.self, forCellReuseIdentifier: "cell")
// generate some sample data
myData = makeSampleData()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: {
_ in
// make sure table re-calculates row heights
UIView.setAnimationsEnabled(false)
self.tableView.performBatchUpdates(nil, completion: nil)
UIView.setAnimationsEnabled(true)
})
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! VarDevCell
cell.fillData(myData[indexPath.row])
return cell
}
func makeSampleData() -> [VarDevStruct] {
var a: [VarDevStruct] = []
// 15 sample data elements
for i in 1...15 {
let d = VarDevStruct(first: "This is the text for the first label in row: \(i).",
second: "This will be a longer string to be used as the text for the second label in row \(i) (long enough to make sure we're getting some word wrapping).",
imageName: "\(i).square.fill")
a.append(d)
}
// change some of the sample data for variations
// (arrays are zero-based)
// fifth row: no first label
a[4].first = ""
a[4].second = "This row has no First label text."
// sixth row: no image
a[5].first = "This row has no image."
a[5].imageName = ""
// seventh row: no second label
a[6].first = "This row has no second label."
a[6].second = ""
// eigth row: no image or second label
a[7].first = "This row has no image, and has no second label. The next row (9) has image only."
a[7].imageName = ""
a[7].second = ""
// ninth row: image only
a[8].first = ""
a[8].second = ""
// tenth row: first label with mutliple lines
a[9].first = "One\nTwo\nThree\nFour"
a[9].second = "This row has embedded newline chars in the text of the first label."
return a
}
}

How to add constraints to a collection view cell once the cell is selected?

I am trying to create a feature programmatically so that when a user selects a cell in the collection view the app keeps a count of the image selected and adds it as an overlay. I am also wanting to add the video duration to the bottom of the image if the selection is a video. I know my problem is in my constraints. You can see in the image example below that I am trying to add the count to the top left of the collection view cell, but also when the user deselects a cell the count adjusts so for example if the number 2 in the image below was deselected the number 3 would become 2. For the most part I think I have the code working but I cannot get the constraints to work. With the current configuration I am getting an error (see below) but I do not even know where to begin with this problem.
"Unable to activate constraint with anchors because they have
no common ancestor. Does the constraint or its anchors reference
items in different view hierarchies? That's illegal."
CollectionView:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.commonInit()
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
//Not sure what to put here
}
}
Overlay
class CustomAssetCellOverlay: UIView {
let countSize = CGSize(width: 40, height: 40)
lazy var circleView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.layer.cornerRadius = self.countSize.width / 2
view.alpha = 0.4
return view
}()
let countLabel: UILabel = {
let label = UILabel()
let font = UIFont.preferredFont(forTextStyle: .headline)
label.font = UIFont.systemFont(ofSize: font.pointSize, weight: UIFont.Weight.bold)
label.textAlignment = .center
label.textColor = .white
label.adjustsFontSizeToFitWidth = true
return label
}()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
private func commonInit() {
addSubview(circleView)
addSubview(countLabel)
//***** START - UPDATED BASED ON SUGGESTION IN COMMENTS******
countLabel.translatesAutoresizingMaskIntoConstraints = false
//***** END - UPDATED BASED ON SUGGESTION IN COMMENTS******
countLabel.centerXAnchor.constraint(equalTo: circleView.centerXAnchor).isActive = true
countLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor).isActive = true
}
}
Collection View Cell
var img = UIImageView()
var overlayView = UIView()
var asset: PHAsset? {
didSet {}
}
var isVideo: Bool = false {
didSet {
durationLabel.isHidden = !isVideo
}
}
override var isSelected: Bool {
didSet { overlay.isHidden = !isSelected }
}
var imageView: UIImageView = {
let view = UIImageView()
view.clipsToBounds = true
view.contentMode = .scaleAspectFill
view.backgroundColor = UIColor.gray
return view
}()
var count: Int = 0 {
didSet { overlay.countLabel.text = "\(count)" }
}
var duration: TimeInterval = 0 {
didSet {
let hour = Int(duration / 3600)
let min = Int((duration / 60).truncatingRemainder(dividingBy: 60))
let sec = Int(duration.truncatingRemainder(dividingBy: 60))
var durationString = hour > 0 ? "\(hour)" : ""
durationString.append(min > 0 ? "\(min):" : ":")
durationString.append(String(format: "%02d", sec))
durationLabel.text = durationString
}
}
let overlay: CustomAssetCellOverlay = {
let view = CustomAssetCellOverlay()
view.isHidden = true
return view
}()
let durationLabel: UILabel = {
let label = UILabel()
label.preferredMaxLayoutWidth = 80
label.backgroundColor = .gray
label.textColor = .white
label.textAlignment = .right
label.font = UIFont.boldSystemFont(ofSize: 20)
return label
}()
func commonInit() {
addSubview(imageView)
imageView.addSubview(overlay)
imageView.addSubview(durationLabel)
imageView.translatesAutoresizingMaskIntoConstraints = false
//***** START - UPDATED BASED ON SUGGESTION IN COMMENTS******
overlay.translatesAutoresizingMaskIntoConstraints = false
overlayView.translatesAutoresizingMaskIntoConstraints = false
//***** END - UPDATED BASED ON SUGGESTION IN COMMENTS******
NSLayoutConstraint.activate([
overlay.topAnchor.constraint(equalTo: imageView.topAnchor),
overlay.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
overlay.leftAnchor.constraint(equalTo: imageView.leftAnchor),
overlay.rightAnchor.constraint(equalTo: imageView.rightAnchor),
overlayView.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
overlayView.centerYAnchor.constraint(equalTo: overlay.centerYAnchor),
overlayView.widthAnchor.constraint(equalToConstant: 80.0),
overlayView.heightAnchor.constraint(equalToConstant: 80.0),
]
)
}
//Some other stuff

Calculating Size of Cell for CollectionView Mosaic Layout

I'm trying to make a mosaic collection view layout similar to Google's Keep app. I've subclassed UICollectionViewLayout similar to the many tutorials found online. In order to properly layout the collection view cells, the calling class must implement a delegate, HeightForCellAtIndexPath method to get the cell's height. In my case, I also get the cell's width to create 1, 2 or 3 column layouts.
In all of the tutorials, the height of the cell's content is known and does not need to be computed. In my case, the size of content is not known and needs to be computed. I've tried many different ways of calculating this but none work perfectly. My latest attempt entails creating a CardContent class and adding that to a cell's contentView in cellForItemAt and also instantiate a CardContent instance in HeightForCellAtIndexPath to calculate the size of the content that is passed to the layout class.
I'm sure there are many problems with my methodology, but from what I can gather, the issue appears to be with the multi-line labels not laid out correctly in HeightForCellAtIndexPath in that the labels are not wrapping to multi line and remain as a single line thus giving me an incorrect height of the contentView.
CardContentCell.swift
import UIKit
class CardContentCell: UICollectionViewCell {
var todoList: TodoList! {
didSet {
self.backgroundColor = UIColor(todoList.color)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 5.0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
CardContent.swift
Edit: Added createLineItem method. See answer below.
class CardContent: UIStackView {
var todoList: TodoList!
var verticalItemSpacing: CGFloat = 10.0
var cellWidth: CGFloat!
init(todoList: TodoList, cellWidth: CGFloat = 0.0) {
self.todoList = todoList
self.cellWidth = cellWidth
super.init(frame: CGRect(x: 0, y: 0, width: cellWidth, height: 0))
self.axis = .vertical
self.alignment = .fill
self.distribution = .fill
self.contentMode = .scaleToFill
self.spacing = 10.0
layoutContent()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createTitleLabel(title: String) -> UILabel {
let label = UILabel()
label.text = title
label.font = label.font.withSize(20.0)
label.numberOfLines = 2
label.lineBreakMode = .byTruncatingTail
label.translatesAutoresizingMaskIntoConstraints = false
return label
}
func createItemLabel(text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = label.font.withSize(17.0)
label.numberOfLines = 3
label.lineBreakMode = .byTruncatingTail
label.translatesAutoresizingMaskIntoConstraints = false
label.sizeToFit()
return label
}
func createLineItem(text: String) -> UIStackView {
let hstack = UIStackView()
hstack.axis = .horizontal
hstack.alignment = .fill
hstack.distribution = .fillProportionally
let imgView = createImgView(withFont: lineItemFont)
let textLabel = createItemLabel(text: text)
hstack.addArrangedSubview(imgView)
hstack.addArrangedSubview(textLabel)
return hstack
}
func layoutContent() {
self.addArrangedSubview(createTitleLabel(title: todoList.title))
for todo in todoList.todos.prefix(6) {
let lineItem = createLineItem(text: todo.text)
self.addArrangedSubview(lineItem)
}
}
}
MyCollectionView.swift
extension MyCollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! CardContentCell
cell.todoList = todoLists[indexPath.row]
let content = CardContent(todoList: cell.todoList)
cell.contentView.addSubview(content)
content.pinTopAndSides(to: cell.contentView) // See extension below
return cell
}
}
extension MyCollectionView: CardLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, HeightForCellAtIndexPath indexPath: IndexPath, cellWidth: CGFloat) -> CGFloat {
let todoList = todoLists[indexPath.row]
let stackView = CardContent(todoList: todoList, cellWidth: cellWidth)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.setNeedsLayout()
stackView.layoutIfNeeded()
let size = stackView.frame.size
return size.height
}
}
extension UIView {
func pinTopAndSides(to other: UIView) {
translatesAutoresizingMaskIntoConstraints = false
leadingAnchor.constraint(equalTo: other.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
topAnchor.constraint(equalTo: other.topAnchor).isActive = true
}
}
The result is, if there are always 6 line items, then the computed height is always 230 (in a 2 column layout). In the screen shot below, the cell is colored while the rest of the content overflows.
Barring a better solution, the answer for me involved not using a nested horizontal UIStackview. That was fraught with unknowns and hard to diagnose auto layout issues. Instead, I used a UIView and added my own constraints.
Here's the method that creates said view. It's interesting that no one took a close enough look at my question that in my hurry to copy and past, I omitted this most crucial method in the original post. I will update the question with the original implementation of this method for reference.
func createLineItem(text: String) -> UIView {
let view = UIView()
let imgView = createImgView(withFont: lineItemFont)
imgView.translatesAutoresizingMaskIntoConstraints = false
let textLabel = createItemLabel(text: text)
textLabel.translatesAutoresizingMaskIntoConstraints = false
imgView.tintColor = self.textColor
view.addSubview(imgView)
view.addSubview(textLabel)
imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
imgView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
textLabel.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 5.0).isActive = true
textLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
textLabel.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
textLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
return view
}
And,as for the HeightForCellAtIndexPath delegate function, setting the widthAnchor to the cell width provided the correct height of the cell:
func collectionView(_ collectionView: UICollectionView, HeightForCellAtIndexPath indexPath: IndexPath, cellWidth: CGFloat) -> CGFloat {
let stackView = CardContent(todoList: todoList)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.widthAnchor.constraint(equalToConstant: cellWidth).isActive = true
stackView.setNeedsLayout()
stackView.layoutIfNeeded()
let size = stackView.frame.size
return size.height
}

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

Prevent UIStackView from compressing UITableView

I am adding a UITableView into vertical UIStackView. That vertical UIStackView is within a UIScrollView.
However the table is not displaying unless I force an explicit Height constraint on it which I obviously don't want to do.
According to this SO question and answer UITableView not shown inside UIStackView this is because "stackview tries to compress content as much as possible"
If I add a UILabel to the StackView it is displayed fine. There is something specific about the UITableView that means it is not. I am using Xamarin and creating the UITableView in code
this.recentlyOpenedPatientsTable = new UITableView()
{
RowHeight = UITableView.AutomaticDimension,
EstimatedRowHeight = 44.0f,
AllowsMultipleSelectionDuringEditing = false,
TranslatesAutoresizingMaskIntoConstraints = false,
Editing = false,
BackgroundColor = UIColor.Clear,
TableFooterView = new UIView(),
ScrollEnabled = false,
};
The UIScrollView is pinned to the Top, Bottom, Left and Right of the View and works fine. It takes the Height I expect.
I have tried both the suggestions in this SO question and neither have worked. I find it odd that I cannot find others having this issue.
Any other suggestions?
Here is a very basic example, using a UITableView subclass to make it auto-size its height based on its content.
The red buttons (in a horizontal stack view) are the first arranged subView in the vertical stack view.
The table is next (green background for the cells' contentView, yellow background for a multi-line label).
And the last arranged subView is a cyan background UILabel:
Note that the vertical stack view is constrained 40-pts from Top, Leading and Trailing, and at least 40-pts from the Bottom. If you add enough rows to the table to exceed the available height, you'll have to scroll to see the additional rows.
//
// TableInStackViewController.swift
//
// Created by Don Mag on 6/24/19.
//
import UIKit
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
class TableInStackCell: UITableViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .yellow
v.textAlignment = .left
v.numberOfLines = 0
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .green
contentView.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor, constant: 0.0),
theLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor, constant: 0.0),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TableInStackViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
let addButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Add a Row", for: .normal)
v.backgroundColor = .red
return v
}()
let deleteButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete a Row", for: .normal)
v.backgroundColor = .red
return v
}()
let buttonsStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 20
return v
}()
let theTable: ContentSizedTableView = {
let v = ContentSizedTableView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let bottomLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
v.textAlignment = .center
v.numberOfLines = 0
v.text = "This label is the last element in the stack view."
// prevent label from being compressed when the table gets too tall
v.setContentCompressionResistancePriority(.required, for: .vertical)
return v
}()
var theTableData: [String] = [
"Content Sized Table View",
"This row shows that the cell heights will auto-size, based on the cell content (multi-line label in this case).",
"Here is the 3rd default row",
]
var minRows = 1
let reuseID = "TableInStackCell"
override func viewDidLoad() {
super.viewDidLoad()
minRows = theTableData.count
view.addSubview(theStackView)
NSLayoutConstraint.activate([
// constrain stack view 40-pts from top, leading and trailing
theStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
theStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
// constrain stack view *at least* 40-pts from bottom
theStackView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40.0),
])
buttonsStack.addArrangedSubview(addButton)
buttonsStack.addArrangedSubview(deleteButton)
theStackView.addArrangedSubview(buttonsStack)
theStackView.addArrangedSubview(theTable)
theStackView.addArrangedSubview(bottomLabel)
theTable.delegate = self
theTable.dataSource = self
theTable.register(TableInStackCell.self, forCellReuseIdentifier: reuseID)
addButton.addTarget(self, action: #selector(addRow), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteRow), for: .touchUpInside)
}
#objc func addRow() -> Void {
// add a row to our data source
let n = theTableData.count - minRows
theTableData.append("Added Row: \(n + 1)")
theTable.reloadData()
}
#objc func deleteRow() -> Void {
// delete a row from our data source (keeping the original rows intact)
let n = theTableData.count
if n > minRows {
theTableData.remove(at: n - 1)
theTable.reloadData()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theTableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as! TableInStackCell
cell.theLabel.text = theTableData[indexPath.row]
return cell
}
}

Resources