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

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

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

UICollectionCellView delegate not being fired

A UICollectionView I have created doesn't want to fire a custom cell delegate.
The cells are displaying, and the test button accepts presses.
I have debugged the code and the cell.delegate is being assigned self.
I have researched this on google and I have come up with nothing. I think my code is ok but I must be missing something?
I would really appreciate any help.
//HomeControllerDelegateTesting.swift
import UIKit
class HomeControllerDelegateTesting: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, PostCellOptionsDelegate {
var collectionView: UICollectionView! = nil
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.createLayout())
self.collectionView.register(PostCell.self, forCellWithReuseIdentifier: PostCell.reuseIdentifier)
self.collectionView.backgroundColor = .systemBackground
self.view.addSubview(collectionView)
self.collectionView.dataSource = self
self.collectionView.delegate = self
}
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(300))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostCell.reuseIdentifier, for: indexPath) as? PostCell else { fatalError("Cannot create cell") }
cell.postTextLabel.text = "Test"
cell.delegate = self
//Test to eliminiate the button accepts events
//cell.optionsButtons.addTarget(self, action: #selector(test), for: .touchUpInside)
return cell
}
#objc func handlePostOptions(cell: PostCell) {
print("123")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
}
// PostCell.swift
import UIKit
protocol PostCellOptionsDelegate: class {
func handlePostOptions(cell: PostCell)
}
class PostCell: UICollectionViewCell {
static let reuseIdentifier = "list-cell-reuse-identifier"
var delegate: PostCellOptionsDelegate?
let usernameLabel: UILabel = {
let label = UILabel()
label.text = "Username"
label.font = .boldSystemFont(ofSize: 15)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let postImageView: UIImageView = {
let control = UIImageView()
control.translatesAutoresizingMaskIntoConstraints = false
control.contentMode = .scaleAspectFill
control.clipsToBounds = true
return control
}()
let postTextLabel: UILabel = {
let label = UILabel()
label.text = "Post text spanning multiple lines"
label.font = .systemFont(ofSize: 15)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// private let optionsButtons: UIButton = {
// let control = UIButton.systemButton(with: #imageLiteral(resourceName: "post_options"), target: self, action: #selector(handleOptions))
// control.translatesAutoresizingMaskIntoConstraints = false
// return control
// }()
let optionsButtons: UIButton = {
let control = UIButton(type: .system)
control.setTitle("Test", for: .normal)
control.addTarget(self, action: #selector(handleOptions), for: .touchUpInside)
control.translatesAutoresizingMaskIntoConstraints = false
return control
}()
#objc fileprivate func handleOptions() {
print("Handle options button")
delegate?.handlePostOptions(cell: self)
}
override init(frame: CGRect) {
super.init(frame: frame)
setupComponents()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupComponents() {
self.optionsButtons.heightAnchor.constraint(equalToConstant: 50).isActive = true
let labelStack = UIStackView(arrangedSubviews: [optionsButtons])
labelStack.translatesAutoresizingMaskIntoConstraints = false
labelStack.axis = .horizontal
labelStack.alignment = .fill
labelStack.distribution = .fill
labelStack.isLayoutMarginsRelativeArrangement = true
labelStack.layoutMargins.left = 16
labelStack.layoutMargins.right = 16
labelStack.layoutMargins.top = 16
labelStack.layoutMargins.bottom = 16
let postTextLabelStack = UIStackView(arrangedSubviews: [postTextLabel])
postTextLabelStack.translatesAutoresizingMaskIntoConstraints = false
postTextLabelStack.axis = .vertical
postTextLabelStack.alignment = .fill
postTextLabelStack.distribution = .fill
postTextLabelStack.isLayoutMarginsRelativeArrangement = true
postTextLabelStack.layoutMargins.left = 16
postTextLabelStack.layoutMargins.right = 16
postTextLabelStack.layoutMargins.top = 16
postTextLabelStack.layoutMargins.bottom = 16
let stack = UIStackView(arrangedSubviews: [labelStack, postImageView, postTextLabelStack])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.alignment = .fill
stack.distribution = .fill
stack.backgroundColor = .blue
self.addSubview(stack)
//postImageView.heightAnchor.constraint(equalTo: postImageView.widthAnchor).isActive = true
stack.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
stack.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
}
Change your button declaration to this:
// make this a lazy var
lazy var optionsButtons: UIButton = {
let control = UIButton(type: .system)
control.setTitle("Test", for: .normal)
// add self. to the selector
control.addTarget(self, action: #selector(self.handleOptions), for: .touchUpInside)
control.translatesAutoresizingMaskIntoConstraints = false
return control
}()
The issue is that when the optionsButtons is created the self is still not initialized. Hence you need to make the button lazy var so that it could be loaded lazily when the optionsButtons is called and the UICollectionViewCell is initialized. Adding the target before self is initialized doesn't work.
To fix your issue, modify your optionsButtons declaration to lazy var, like this:
lazy var optionsButtons: UIButton = {
OR
Add the target after the UICollectionViewCell is initialized, like this:
override init(frame: CGRect) {
super.init(frame: frame)
optionsButtons.addTarget(self, action: #selector(handleOptions), for: .touchUpInside)
setupComponents()
}

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
}

UITableView with custom cells lags while scrolling

My problem is that UITableView lags quite a lot while scrolling.
This is what I am trying to achieve
Starting from the top I have a simple section header with only one checkbox and one UILabel. Under this header, you can see a custom cell with only one UILabel aligned to the center. This custom cell works like another header for the data that would be shown below (Basically a 3D array). Under these "headers" are custom cells that contain one multiline UILabel and under this label is a container for a variable amount of lines containing a checkbox and an UILabel. On the right side of the cell is also a button (blue/white arrow).
So this means the content is shown like this:
Section header (containing day and date)
Custom UITableViewCell = header (containing some header information)
Custom UITableViewCell (containing data to be shown)
Here is my code:
cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let (isHeader, headerNumber, semiResult) = checkIfIsHeader(section: indexPath.section, row: indexPath.row)
let row = indexPath.row
if isHeader {
let chod = objednavkaDny[indexPath.section].chody[headerNumber+1]
let cell = tableView.dequeueReusableCell(withIdentifier: cellHeaderReuseIdentifier, for: indexPath) as! ObjednavkyHeaderTableViewCell
cell.titleLabel.text = chod.popisPoradiJidla
cell.selectionStyle = .none
return cell
}else{
let chod = objednavkaDny[indexPath.section].chody[headerNumber]
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! ObjednavkyTableViewCell
cell.updateData(objednavka: chod.objednavky[row-semiResult], canSetAmount: self.typDialogu == 3)
return cell
}
}
checkIfIsHeader:
func checkIfIsHeader(section: Int, row: Int) -> (Bool, Int, Int){
if let cachedResult = checkIfHeaderCache[section]?[row] {
return (cachedResult[0] == 1, cachedResult[1], cachedResult[2])
}
var isHeader = false
var semiResult = 0
var headerNumber = -1
for (index, chod) in objednavkaDny[section].chody.enumerated() {
let sum = chod.objednavky.count
if row == semiResult {
isHeader = true
break
}else if row < semiResult {
semiResult -= objednavkaDny[section].chody[index-1].objednavky.count
break
}else {
headerNumber += 1
semiResult += 1
if index != objednavkaDny[section].chody.count - 1 {
semiResult += sum
}
}
}
checkIfHeaderCache[section] = [Int:[Int]]()
checkIfHeaderCache[section]![row] = [isHeader ? 1 : 0, headerNumber, semiResult]
return (isHeader, headerNumber, semiResult)
}
and the main cell that shows the data:
class ObjednavkyTableViewCell: UITableViewCell {
lazy var numberTextField: ObjednavkyTextField = {
let textField = ObjednavkyTextField()
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
let mealLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .black
label.textAlignment = .left
label.font = UIFont(name: ".SFUIText", size: 15)
label.numberOfLines = 0
label.backgroundColor = .white
label.isOpaque = true
return label
}()
lazy var detailsButton: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "arrow-right")?.withRenderingMode(.alwaysTemplate), for: .normal)
button.imageView?.tintColor = UIColor.custom.blue.classicBlue
button.imageView?.contentMode = .scaleAspectFit
button.contentHorizontalAlignment = .right
button.imageEdgeInsets = UIEdgeInsetsMake(10, 0, 10, 0)
button.addTarget(self, action: #selector(detailsButtonPressed), for: .touchUpInside)
button.backgroundColor = .white
button.isOpaque = true
return button
}()
let pricesContainerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.isOpaque = true
return view
}()
var canSetAmount = false {
didSet {
canSetAmount ? showNumberTextField() : hideNumberTextField()
}
}
var shouldShowPrices = false {
didSet {
shouldShowPrices ? showPricesContainerView() : hidePricesContainerView()
}
}
var pricesContainerHeight: CGFloat = 0
private let priceViewHeight: CGFloat = 30
var mealLabelLeadingConstraint: NSLayoutConstraint?
var mealLabelBottomConstraint: NSLayoutConstraint?
var pricesContainerViewHeightConstraint: NSLayoutConstraint?
var pricesContainerViewBottomConstraint: NSLayoutConstraint?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func detailsButtonPressed() {
}
func updateData(objednavka: Objednavka, canSetAmount: Bool) {
self.canSetAmount = canSetAmount
if let popisJidla = objednavka.popisJidla, popisJidla != "", popisJidla != " " {
self.mealLabel.text = popisJidla
}else{
self.mealLabel.text = objednavka.nazevJidelnicku
}
if objednavka.objects.count > 1 {
shouldShowPrices = true
setPricesStackView(with: objednavka.objects)
checkIfSelected(objects: objednavka.objects)
}else{
shouldShowPrices = false
self.numberTextField.text = String(objednavka.objects[0].pocet)
//setSelected(objednavka.objects[0].pocet > 0, animated: false)
objednavka.objects[0].pocet > 0 ? setSelectedStyle() : setDeselectedStyle()
}
}
//---------------
func checkIfSelected(objects: [ObjednavkaObject]) {
var didChangeSelection = false
for object in objects { // Checks wether cell should be selected or not
if object.pocet > 0 {
setSelected(true, animated: false)
setSelectedStyle()
didChangeSelection = true
break
}
}
if !didChangeSelection {
setSelected(false, animated: false)
setDeselectedStyle()
}
}
//--------------
func showNumberTextField() {
numberTextField.isHidden = false
mealLabelLeadingConstraint?.isActive = false
mealLabelLeadingConstraint = mealLabel.leadingAnchor.constraint(equalTo: numberTextField.trailingAnchor, constant: 10)
mealLabelLeadingConstraint?.isActive = true
}
func hideNumberTextField() {
numberTextField.isHidden = true
mealLabelLeadingConstraint?.isActive = false
mealLabelLeadingConstraint = mealLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor, constant: 0)
mealLabelLeadingConstraint?.isActive = true
}
func showPricesContainerView() {
hideNumberTextField()
pricesContainerView.isHidden = false
mealLabelBottomConstraint?.isActive = false
pricesContainerViewBottomConstraint?.isActive = true
}
func hidePricesContainerView() {
pricesContainerView.isHidden = true
pricesContainerViewBottomConstraint?.isActive = false
mealLabelBottomConstraint?.isActive = true
}
//--------------
func setSelectedStyle() {
self.backgroundColor = UIColor.custom.blue.classicBlue
mealLabel.textColor = .white
mealLabel.backgroundColor = UIColor.custom.blue.classicBlue
for subview in pricesContainerView.subviews where subview is ObjednavkyPriceView {
let priceView = (subview as! ObjednavkyPriceView)
priceView.titleLabel.textColor = .white
priceView.checkBox.backgroundColor = UIColor.custom.blue.classicBlue
priceView.titleLabel.backgroundColor = UIColor.custom.blue.classicBlue
priceView.backgroundColor = UIColor.custom.blue.classicBlue
}
pricesContainerView.backgroundColor = UIColor.custom.blue.classicBlue
detailsButton.imageView?.tintColor = .white
detailsButton.backgroundColor = UIColor.custom.blue.classicBlue
}
func setDeselectedStyle() {
self.backgroundColor = .white
mealLabel.textColor = .black
mealLabel.backgroundColor = .white
for subview in pricesContainerView.subviews where subview is ObjednavkyPriceView {
let priceView = (subview as! ObjednavkyPriceView)
priceView.titleLabel.textColor = .black
priceView.checkBox.backgroundColor = .white
priceView.titleLabel.backgroundColor = .white
priceView.backgroundColor = .white
}
pricesContainerView.backgroundColor = .white
detailsButton.imageView?.tintColor = UIColor.custom.blue.classicBlue
detailsButton.backgroundColor = .white
}
//-----------------
func setPricesStackView(with objects: [ObjednavkaObject]) {
let subviews = pricesContainerView.subviews
var subviewsToDelete = subviews.count
for (index, object) in objects.enumerated() {
subviewsToDelete -= 1
if subviews.count - 1 >= index {
let priceView = subviews[index] as! ObjednavkyPriceView
priceView.titleLabel.text = object.popisProduktu // + " " + NSNumber(value: object.cena).getFormattedString(currencySymbol: "Kč") // TODO: currencySymbol
priceView.canSetAmount = canSetAmount
priceView.count = object.pocet
priceView.canOrder = (object.nelzeObj == nil || object.nelzeObj == "")
}else {
let priceView = ObjednavkyPriceView(frame: CGRect(x: 0, y: CGFloat(index) * priceViewHeight + CGFloat(index * 5), width: pricesContainerView.frame.width, height: priceViewHeight))
pricesContainerView.addSubview(priceView)
priceView.titleLabel.text = object.popisProduktu // + " " + NSNumber(value: object.cena).getFormattedString(currencySymbol: "Kč") // TODO: currencySymbol
priceView.numberTextField.delegate = self
priceView.canSetAmount = canSetAmount
priceView.canOrder = (object.nelzeObj == nil || object.nelzeObj == "")
priceView.count = object.pocet
pricesContainerHeight += ((index == 0) ? 30 : 35)
}
}
if subviewsToDelete > 0 { // Deletes unwanted subviews
for _ in 0..<subviewsToDelete {
pricesContainerView.subviews.last?.removeFromSuperview()
pricesContainerHeight -= pricesContainerHeight + 5
}
}
if pricesContainerHeight < 0 {
pricesContainerHeight = 0
}
pricesContainerViewHeightConstraint?.constant = pricesContainerHeight
}
func setupView() {
self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale
self.backgroundColor = .white
contentView.addSubview(numberTextField)
contentView.addSubview(mealLabel)
contentView.addSubview(detailsButton)
contentView.addSubview(pricesContainerView)
setupConstraints()
}
func setupConstraints() {
numberTextField.anchor(leading: readableContentGuide.leadingAnchor, size: CGSize(width: 30, height: 30))
numberTextField.centerYAnchor.constraint(equalTo: mealLabel.centerYAnchor).isActive = true
detailsButton.anchor(trailing: readableContentGuide.trailingAnchor, size: CGSize(width: 30, height: 30))
detailsButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
mealLabel.anchor(top: contentView.topAnchor, trailing: detailsButton.leadingAnchor, padding: .init(top: 10, left: 0, bottom: 0, right: -10))
mealLabelBottomConstraint = mealLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
mealLabelBottomConstraint?.priority = UILayoutPriority(rawValue: 999)
pricesContainerView.anchor(top: mealLabel.bottomAnchor, leading: readableContentGuide.leadingAnchor, trailing: detailsButton.leadingAnchor, padding: .init(top: 10, left: 0, bottom: 0, right: -10))
pricesContainerViewBottomConstraint = pricesContainerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
pricesContainerViewBottomConstraint?.priority = UILayoutPriority(rawValue: 999)
pricesContainerViewHeightConstraint = pricesContainerView.heightAnchor.constraint(equalToConstant: 0)
pricesContainerViewHeightConstraint?.priority = UILayoutPriority(rawValue: 999)
pricesContainerViewHeightConstraint?.isActive = true
}
}
To conclude how it is done:
tableView.rowHeight is set to UITableViewAutomaticDymension
inside cellForRowAt I get the data from an array and give it to the
cell
all the cells are set up in code using constraints
all the views have set isOpaque = true
heights of the cells are cached
cells are set to rasterize
I also noticed that it lags at certain scroll levels and that sometimes it works just fine and sometimes it lags a lot.
Despite all of the optimization I have done, the tableView still lags while scrolling.
Here is a screenshot from Instruments
Any tip how to improve the scrolling performance is highly appreciated!
(I might have forgotten to include some code/information so feel free to ask me in the comments.)
I can't tell you where the lag happens exactly but when we are talking about lagging during scrolling, it's related to your cellForRowAt delegate method. What happends is that too many things are going on within this method and it's called for every cells that are displaying & going to display. I see that your are trying to cache the result by checkIfHeaderCache but still, there is a for loop at the very beginning to determine header cell.
Suggestions:
I don't know where you get data (objednavkaDny) from but right after you get the data, do a full loop through and determin cell type one by one, and save the result some where base on your design. During this loading time, you can show some loading message on the screen. Then, within the cellForRow method, you should be just simply using things like
if (isHeader) {
render header cell
} else {
render other cell
}
Bottom line:
cellForRow method is not designed to handle heavy calculations, and it will slow down the scrolling if you do so. This method is for assigning values to the cached table view cell only and that's the only thing it is good at.

Delegate Function Not Being Called

So I am trying to use protocols and delegates to connect two functions so I can perform some operation on a variable a collectionView in this case in a different file.
import Foundation
import UIKit
protocol EventCollectionCellDelegate: NSObjectProtocol {
func setupCollectionView(for eventCollectionView: UICollectionView?)
}
class EventCollectionCell:UICollectionViewCell {
weak var delegate: EventCollectionCellDelegate?
var eventArray = [EventDetails](){
didSet{
self.eventCollectionView.reloadData()
}
}
var enentDetails:Friend?{
didSet{
var name = "N/A"
var total = 0
seperator.isHidden = true
if let value = enentDetails?.friendName{
name = value
}
if let value = enentDetails?.events{
total = value.count
self.eventArray = value
seperator.isHidden = false
}
if let value = enentDetails?.imageUrl{
profileImageView.loadImage(urlString: value)
}else{
profileImageView.image = #imageLiteral(resourceName: "Tokyo")
}
self.eventCollectionView.reloadData()
setLabel(name: name, totalEvents: total)
}
}
let container:UIView={
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 16
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.3
return view
}()
//profile image view for the user
var profileImageView:CustomImageView={
let iv = CustomImageView()
iv.layer.masksToBounds = true
iv.layer.borderColor = UIColor.lightGray.cgColor
iv.layer.borderWidth = 0.3
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
//will show the name of the user as well as the total number of events he is attending
let labelNameAndTotalEvents:UILabel={
let label = UILabel()
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
let seperator:UIView={
let view = UIView()
view.backgroundColor = .lightGray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
//collectionview that contains all of the events a specific user will be attensing
let flow = UICollectionViewFlowLayout()
lazy var eventCollectionView = UICollectionView(frame: .zero, collectionViewLayout: flow)
// var eventCollectionView:UICollectionView?
override init(frame: CGRect) {
super.init(frame: frame)
self.setUpCell()
self.setupCollectionView(for: eventCollectionView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupCollectionView(for eventCollectionView: UICollectionView?){
delegate?.setupCollectionView(for: eventCollectionView)
}
}
This is the file that creates a collectionViewCell with a collectionView in it. I am trying to perform some operation on that collectionView using the delegate pattern. My problem is that the delegate function is never called in the accompanying viewController. I feel like I have done everything right but nothing happens in the accompanying vc. Anyone notice what could possibly be wrong.
I have shown the code for the VC below
class FriendsEventsView: UIViewController,UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout,EventCollectionCellDelegate {
var friends = [Friend]()
var followingUsers = [String]()
var height:CGFloat = 0
var notExpandedHeight : CGFloat = 50
var isExpanded = [Bool]()
//so this is the main collectonview that encompasses the entire view
lazy var mainCollectionView:UICollectionView={
// the flow layout which is needed when you create any collection view
let flow = UICollectionViewFlowLayout()
//setting the scroll direction
flow.scrollDirection = .vertical
//setting space between elements
let spacingbw:CGFloat = 5
flow.minimumLineSpacing = spacingbw
flow.minimumInteritemSpacing = 0
//actually creating collectionview
let cv = UICollectionView(frame: .zero, collectionViewLayout: flow)
//register a cell for that collectionview
cv.register(EventCollectionCell.self, forCellWithReuseIdentifier: "events")
cv.translatesAutoresizingMaskIntoConstraints = false
//changing background color
cv.backgroundColor = .white
//sets the delegate of the collectionView to self. By doing this all messages in regards to the collectionView will be sent to the collectionView or you.
//"Delegates send messages"
cv.delegate = self
//sets the datsource of the collectionView to you so you can control where the data gets pulled from
cv.dataSource = self
//sets positon of collectionview in regards to the regular view
cv.contentInset = UIEdgeInsetsMake(spacingbw, 0, spacingbw, 0)
return cv
}()
lazy var eventCollectionView:UICollectionView={
let flow = UICollectionViewFlowLayout()
flow.scrollDirection = .vertical
let spacingbw:CGFloat = 5
flow.minimumLineSpacing = 0
flow.minimumInteritemSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: flow)
//will register the eventdetailcell
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .white
cv.register(EventDetailsCell.self, forCellWithReuseIdentifier: "eventDetails")
cv.delegate = self
cv.dataSource = self
cv.contentInset = UIEdgeInsetsMake(spacingbw, 0, spacingbw, 0)
cv.showsVerticalScrollIndicator = false
cv.bounces = false
return cv
}()
func setupCollectionView(for eventCollectionView: UICollectionView?) {
print("Attempting to create collectonView")
eventCollectionView?.backgroundColor = .blue
}
//label that will be displayed if there are no events
let labelNotEvents:UILabel={
let label = UILabel()
label.textColor = .lightGray
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = UIFont.italicSystemFont(ofSize: 14)
label.text = "No events found"
label.isHidden = true
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
//will set up all the views in the screen
self.setUpViews()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "close_black").withRenderingMode(.alwaysOriginal), style: .done, target: self, action: #selector(self.goBack))
}
func setUpViews(){
//well set the navbar title to Friends Events
self.title = "Friends Events"
view.backgroundColor = .white
//adds the main collection view to the view and adds proper constraints for positioning
view.addSubview(mainCollectionView)
mainCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
mainCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
mainCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
mainCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
//adds the label to alert someone that there are no events to the collectionview and adds proper constrains for positioning
mainCollectionView.addSubview(labelNotEvents)
labelNotEvents.centerYAnchor.constraint(equalTo: mainCollectionView.centerYAnchor, constant: 0).isActive = true
labelNotEvents.centerXAnchor.constraint(equalTo: mainCollectionView.centerXAnchor, constant: 0).isActive = true
//will fetch events from server
self.fetchEventsFromServer()
}
// MARK: CollectionView Datasource for maincollection view
//woll let us know how many cells are being displayed
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print(friends.count)
isExpanded = Array(repeating: false, count: friends.count)
return friends.count
}
//will control the size of the cell that is displayed in the containerview
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
height = 100
let event = friends[indexPath.item]
if let count = event.events?.count,count != 0{
height += (CGFloat(count*40)+10)
}
return CGSize(width: collectionView.frame.width, height: height)
}
//will do the job of effieicently creating cells for the eventcollectioncell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "events", for: indexPath) as! EventCollectionCell
cell.delegate = self
cell.enentDetails = friends[indexPath.item]
cell.eventCollectionView = eventCollectionView
return cell
}
}
I have cut the code down to what I believe is needed to answer the question for simplicity. Any help is appreciated
You set delegate after setupCollectionView. In your case, you can't call setupCollectionView before you set delegate, because setupCollectionView called in init

Resources