Stackview inside collectionview or uitableview - ios

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

Related

How to change collectionview cells color based on device theme (following my color scheme)

Overview:
I'm building a keyboard Extension using collectionviews. I want the cells to change color based on the device theme (light/dark). At the moment, when I set the color scheme for my collectionview cells they don't work. I'm marking the problematic parts of my code with a "///" comment.
Resources:
I found this RayWenderlich project and I liked how they handled the color changing stuff so I copied it.
My code:
I have 3 classes:
KeyboardViewController
Custom View containing keyboard buttons
Custom collectionview cells
CollectionView cell
class KeyboardKeys: UICollectionViewCell {
var defaultColor = UIColor.white
var highlighColor = UIColor.lightGray.withAlphaComponent(0.6)
let label: UILabel = {
let iv = UILabel()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.contentMode = .scaleAspectFit
iv.font = UIFont.systemFont(ofSize: 20)
iv.clipsToBounds = true
iv.numberOfLines = 1
iv.textAlignment = .center
return iv
}()
override init(frame: CGRect) {
super.init(frame: .zero)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
contentView.addSubview(label)
label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
label.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = isHighlighted ? highlighColor : defaultColor
}
}
Custom View
class lettersKeyboard: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
var keyView: UICollectionView!
let letters = ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit(){
//If you find some errors it's because this is way different in my code. This is just a regulare collection view anyway
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
keyView = UICollectionView(frame: CGRect(x: 0.0, y: 0.0 , width: frame.width, height: 280), collectionViewLayout: layout)
keyView.setCollectionViewLayout(layout, animated: true)
keyView.isScrollEnabled = false
keyView.register(KeyboardKeys.self, forCellWithReuseIdentifier: "collectionCellId")
keyView.delegate = self
keyView.dataSource = self
addSubview(keyView)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = keyView.dequeueReusableCell(withReuseIdentifier: "collectionCellId", for: indexPath) as! KeyboardKeys
cell.label.text = letters[indexPath.row]
return cell
}
///I guess something is wrong here
func setColorScheme(_ colorScheme: ColorScheme) {
let colorScheme = CColors(colorScheme: colorScheme)
for view in subviews {
if let cell = view as? KeyboardKeys {
cell.tintColor = colorScheme.buttonTextColor
cell.defaultColor = colorScheme.keysDefaultColor
cell.highlighColor = colorScheme.keysHighlightColor
}
}
}
}
Color scheme struct
enum ColorScheme {
case dark
case light
}
struct CColors {
let keysDefaultColor: UIColor
let keysHighlightColor: UIColor
let buttonTextColor: UIColor
init(colorScheme: ColorScheme) {
switch colorScheme {
case .light:
keysDefaultColor = .systemRed
//UIColor.white
keysHighlightColor = UIColor.lightGray.withAlphaComponent(0.6)
buttonTextColor = .black
case .dark:
keysDefaultColor = .systemBlue
// UIColor.gray.withAlphaComponent(0.5)
keysHighlightColor = UIColor.lightGray.withAlphaComponent(0.5)
buttonTextColor = .white
}
}
}
KeyboardViewController
class KeyboardViewController: UIInputViewController {
var letters : lettersKeyboard = {
let m = lettersKeyboard(frame: .zero)
m.translatesAutoresizingMaskIntoConstraints = false
m.backgroundColor = .clear
return m
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(letters)
letters.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
letters.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
letters.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
letters.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
//The rest is the default inputvc stuff
///Or here
override func textDidChange(_ textInput: UITextInput?) {
// The app has just changed the document's contents, the document context has been updated.
let colorScheme: ColorScheme
let proxy = self.textDocumentProxy
if proxy.keyboardAppearance == UIKeyboardAppearance.dark {
colorScheme = .dark
} else {
colorScheme = .light
}
letters.setColorScheme(colorScheme)
}
}
Question:
I don't know what I'm doing wrong since my code works with everything except for collectionview cells. I guess another way of doing this stuff exists. So how do I change my collectionView cells' color based on the device's theme following my color scheme?
You should really be reloading the collection view, rather than trying to find the subviews that are the keys, and updating those.
Pass in the colorScheme model to each cell and have the colors be set as a result of a reload.
A very kind guy helped me out and found this solution. The problem here is that I forgot the view's hierarchy.
CollectionView cell
override func layoutSubviews() {
super.layoutSubviews()
setupBackGround()
}
func setupBackGround(){
backgroundColor = isHighlighted ? highlighColor : defaultColor
}
KeyboardViewController
func setColorScheme(_ colorScheme: ColorScheme) {
let colorScheme = CColors(colorScheme: colorScheme)
for view in subviews {
func setToRootView(view: UIView) {
if let cell = view as? KeyboardKeys {
cell.tintColor = colorScheme.buttonTextColor
cell.defaultColor = colorScheme.keysDefaultColor
cell.highlighColor = colorScheme.keysHighlightColor
cell.setBackground()
return
}
guard view.subviews.count > 0 else {
return
}
view.subviews.forEach(setToRootView(view:))
}
setToRootView(view: self)
}

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
}

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

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.

Problems with complex UITableViewCell

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

Resources