How to create a slow and continuous scroll with CollectionView Swift - ios

So I have some info that I would like to display in a horizontal CollectionView that scrolls automatically without user interaction. Like those bars under news channels that display info.
I have the CollectionView set up with the cells renewing once the datasource runs out, so I can scroll infinitely with the data being recycled.
I found some functions that use timers but they snap to a new index path and its not a continuous slow scroll. I also found some cocoa pods but I can imagine there is a simpler way to do this?
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return Int.max
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = CollectionView.dequeueReusableCell(withReuseIdentifier: "OverViewCell", for: indexPath) as? OverViewCollectionCellCollectionViewCell
let itemToShow = testlist[indexPath.item % testlist.count]
cell!.symbolLabelCollection.text = itemToShow
return cell!
}
Any idea?

Here's a very basic example using a UIViewPropertyAnimator.
First, a simple cell with a single label:
class MyLabelCell: UICollectionViewCell {
let label: UILabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
label.textColor = .black
label.textAlignment = .center
label.font = .systemFont(ofSize: 16.0)
label.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
let g = contentView
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
label.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
and an example view controller (everything added via code - no #IBOutlet or #IBAction connections):
class MarqueeCVViewController: UIViewController {
var collectionView: UICollectionView!
var myData: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// some sample data
myData.append("This is cell 1")
myData.append("This is the text of the second cell")
myData.append("Cell 3 has a long enough string that it will exceed the width of the visible portion of the collection view")
myData.append("This is the fourth cell")
myData.append("This is the last cell for this example")
let fl: UICollectionViewFlowLayout = {
let f = UICollectionViewFlowLayout()
f.scrollDirection = .horizontal
f.estimatedItemSize = CGSize(width: 100, height: 30)
return f
}()
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.register(MyLabelCell.self, forCellWithReuseIdentifier: "MyLabelCell")
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// extend the collection view wider than the view
// otherwise, cells tend to be removed when only partially
// scrolled off-screen
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: -100.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 100.0),
collectionView.heightAnchor.constraint(equalToConstant: 32.0),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
// inset the content so we start with the first cell visible
collectionView.contentInset.left = 100
// so we can see the collectionView frame
collectionView.backgroundColor = .cyan
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// start the animation
runAnim()
}
func runAnim() {
// adjust these values to suit
// animation will re-start every 5 seconds
let dur: Double = 5
// "scroll speed" -- number of points per second
let ptsPerSecond: Double = 25
let animator = UIViewPropertyAnimator(duration: dur, curve: .linear) {
self.collectionView.contentOffset.x += ptsPerSecond * dur
}
animator.addCompletion({_ in
self.runAnim()
})
animator.startAnimation()
}
}
extension MarqueeCVViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100_000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "MyLabelCell", for: indexPath) as! MyLabelCell
c.label.text = myData[indexPath.item % myData.count]
// so we can see the cell frames
c.contentView.backgroundColor = .yellow
return c
}
}

Related

How to image setup in UICollectionView in UICollectionViewCell Swift

I am trying to set up images in the horizontal alignment in collectionView, but it's not working properly. Please check the attached screenshot.
View Controller Code:-
import UIKit
class ViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout {
#IBOutlet weak var texting1: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.texting1.register(UINib(nibName:"CollectionViewCell1", bundle: nil), forCellWithReuseIdentifier: "CollectionViewCell1")
self.texting1.delegate = self
self.texting1.dataSource = self
texting1.backgroundColor = .red
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell1", for: indexPath) as! CollectionViewCell1
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: self.view.frame.width, height: 120)
}
}
Output:- Image
CollectionViewCell1 Code:-
import UIKit
class CollectionViewCell1: UICollectionViewCell,UICollectionViewDataSource, UICollectionViewDelegate,UICollectionViewDelegateFlowLayout {
#IBOutlet weak var userImageColectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
self.userImageColectionView.register(UINib(nibName:"CollectionViewCell2", bundle: nil), forCellWithReuseIdentifier: "CollectionViewCell2")
userImageColectionView.delegate = self
userImageColectionView.dataSource = self
userImageColectionView.backgroundColor = .black
}
var items = ["1", "2", "3", "4"]
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell2", for: indexPath as IndexPath) as! CollectionViewCell2
return cell
}
// MARK: - UICollectionViewDelegate protocol
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("You selected cell #\(indexPath.item)!")
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 70, height: 60);
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return -10
}
}
Xib ScreenShot:- Image
CollectionViewCell2 Code:-
class CollectionViewCell2: UICollectionViewCell {
#IBOutlet weak var imguserView: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
imguserView.image = UIImage(systemName: "person.crop.circle")
}
}
Xib ScreenShot:- Image
My Goal:- Image
Can someone please explain to me how to show images under in collection view in the horizontal alignment and setup dynamic width according to cell images count, I've tried to implement by above but no results yet.
Any help would be greatly appreciated.
Thanks in advance.
One way to do this without using a collection view is to create a "horizontal chain" of constraints for the "cell views."
For example, if we have 3 image views - red, green, and blue - with these constraints:
NSLayoutConstraint.activate([
red.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0),
green.leadingAnchor.constraint(equalTo: red.trailingAnchor, constant: 10.0),
blue.leadingAnchor.constraint(equalTo: green.trailingAnchor, constant: 10.0),
])
it will look like this:
If we instead use negative constants:
NSLayoutConstraint.activate([
red.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0),
green.leadingAnchor.constraint(equalTo: red.trailingAnchor, constant: -20.0),
blue.leadingAnchor.constraint(equalTo: green.trailingAnchor, constant: -20.0),
])
it can look like this:
So we can create a custom UIView subclass that handles all of that for us -- such as this:
class OverlapView: UIView {
public var overlap: CGFloat = -30 { didSet { setNeedsLayout() } }
public var cellViews: [UIView] = [] {
didSet {
// clear out any existing subviews
subviews.forEach { v in
v.removeFromSuperview()
}
cellViews.forEach { v in
// in case it wasn't set by the caller
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
// center it vertically
v.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
// we want the overlap to go from left-to-right
sendSubviewToBack(v)
}
setNeedsLayout()
}
}
private var horizontalConstraints: [NSLayoutConstraint] = []
override func layoutSubviews() {
super.layoutSubviews()
guard cellViews.count > 0 else { return }
// de-activate any existing horizontal constraints
NSLayoutConstraint.deactivate(horizontalConstraints)
var prevView: UIView!
cellViews.forEach { v in
// if it's the first one
if prevView == nil {
horizontalConstraints.append(v.leadingAnchor.constraint(equalTo: leadingAnchor))
} else {
horizontalConstraints.append(v.leadingAnchor.constraint(equalTo: prevView.trailingAnchor, constant: overlap))
}
prevView = v
}
// constrain last view's trailing
horizontalConstraints.append(prevView.trailingAnchor.constraint(equalTo: trailingAnchor))
// activate the udpated constraints
NSLayoutConstraint.activate(horizontalConstraints)
}
}
Here's a sample controller demonstrating that:
class OverlapTestVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testView = OverlapView()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
testView.heightAnchor.constraint(equalToConstant: 70.0),
// no trailing or width constraint
// width will be determined by the number of "cells"
])
// let's add 6 "cells" -- one for each color
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.systemYellow, .systemCyan, .magenta,
]
var cellViews: [UIView] = []
colors.forEach { c in
if let img = UIImage(systemName: "person.crop.circle") {
let imgView = UIImageView(image: img)
imgView.tintColor = c
imgView.backgroundColor = .white
imgView.translatesAutoresizingMaskIntoConstraints = false
// let's use a 60x60 image view
imgView.widthAnchor.constraint(equalToConstant: 60.0).isActive = true
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor).isActive = true
// we want "round" views
imgView.layer.cornerRadius = 30.0
imgView.layer.masksToBounds = true
cellViews.append(imgView)
}
}
testView.cellViews = cellViews
// we can change the overlap value here if we don't want to use
// our custom view's default
// for example:
//testView.overlap = -40.0
// so we can see the framing
testView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
}
}
Output looks like this:
(We gave the custom view itself a light gray background so we can see its frame.)

Collection view selected cell background view color not changing in IOS 15

When the collection view cell is selected, cell background view and the other labels in that cell should change color. The below code is working for below ios 15. If i change didSet to willSet in the below code, it is working for ios 15 but its not working below ios 15. Is there a solution to change the color for the selected custom cell? I am adding the collection view delegate and datasource methods code as well.
override var isSelected: Bool{
didSet{
if self.isSelected
{
super.isSelected = true
lblName.textColor = .white
cellBGView.backgroundColor = .themeColor
cellInfoBtn.tintColor = .white
}
else
{
super.isSelected = false
lblName.textColor = .themeColor
cellBGView.backgroundColor = .white
cellInfoBtn.tintColor = .themeColor
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
selectIndex = indexPath.row
let cell = collectionView.cellForItem(at: indexPath) as! CustomCollectionCell
selectedIndexPath = indexPath
selectIndexSec = indexPath.section
collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCollectionCell", for: indexPath) as! CustomCollectionCell cell.cellInfoBtn.tag = indexPath.row
cell.cellInfoBtn.addTarget(self, action: #selector(infoBtnTapped(_:)), for: .touchUpInside)
if selectIndex == indexPath.row { cell.isSelected=true }
else { cell.isSelected=false }
return cell
}
I tried the above code and i need to find a common solution for old version as well as version above 15. If there is already an answer exists, please redirect me to it.
You are doing a lot of extra work.
A UICollectionView tracks its own selections with .indexPathsForSelectedItems, so there is no need for the additional tracking with your selectedIndexPath and selectIndexSec.
Also, if you're overriding isSelected, there's no need to call .reloadData().
Here's a complete example -- I added the lblName and cellBGView but not the button:
class AutoHighlightCell: UICollectionViewCell {
let lblName = UILabel()
let cellBGView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
lblName.textAlignment = .center
[cellBGView, lblName].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
cellBGView.addSubview(lblName)
contentView.addSubview(cellBGView)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
cellBGView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
cellBGView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
cellBGView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
cellBGView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
lblName.topAnchor.constraint(equalTo: cellBGView.topAnchor, constant: 16.0),
lblName.leadingAnchor.constraint(equalTo: cellBGView.leadingAnchor, constant: 24.0),
lblName.trailingAnchor.constraint(equalTo: cellBGView.trailingAnchor, constant: -24.0),
lblName.bottomAnchor.constraint(equalTo: cellBGView.bottomAnchor, constant: -16.0),
])
contentView.layer.borderWidth = 1.0
contentView.layer.borderColor = UIColor.black.cgColor
// set default non-selected properties
lblName.textColor = .blue
cellBGView.backgroundColor = .yellow
}
override var isSelected: Bool {
didSet {
lblName.textColor = isSelected ? .white : .blue
cellBGView.backgroundColor = isSelected ? .systemGreen : .yellow
}
}
}
class AutoHighlightCollectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var collectionView: UICollectionView!
let instructionLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.text = "Tap Here"
v.numberOfLines = 0
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
let fl = UICollectionViewFlowLayout()
fl.estimatedItemSize = CGSize(width: 80, height: 50)
fl.scrollDirection = .horizontal
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
instructionLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(instructionLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
collectionView.heightAnchor.constraint(equalToConstant: 80.0),
instructionLabel.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 60.0),
instructionLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
instructionLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
collectionView.register(AutoHighlightCell.self, forCellWithReuseIdentifier: "cell")
collectionView.dataSource = self
collectionView.delegate = self
// so we can see the collectionView frame
collectionView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
instructionLabel.addGestureRecognizer(t)
instructionLabel.isUserInteractionEnabled = true
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! AutoHighlightCell
c.lblName.text = "\(indexPath)"
return c
}
#objc func gotTap(_ g: UITapGestureRecognizer) {
var s = "Tap Here\n\n"
if let pth = collectionView.indexPathsForSelectedItems?.first {
s += "Selected Path: \(pth)"
} else {
s += "No Item Selected"
}
instructionLabel.text = s
}
}
When run, it will look like this:
If you tap "Tap Here" before selecting a cell, you'll see:
after selecting a cell:
Notice that when you scroll the cells in and out of view, the "Selected" state is maintained by the collection view, and the cell's UI is updated in override var isSelected ... no need to worry about any of that in cellForItemAt or in didSelectItemAt

UICollectionViewCell is not being correctly called from the UIViewController

I am trying to create a custom overlay for a UICollectionViewCell that when a user selects an image it puts a gray overlay with a number (ie. order) that the user selected the image in. When I run my code I do not get any errors but it also appears to do nothing. I added some print statements to help debug and when I run the code I get "Count :0" printed 15 times. That is the number of images I have in the library. When I select the first image in the first row I still get "Count: 0" as I would expect, but when I select the next image I get the print out that you see below. It appears that the count is not working but I am not sure why. What am I doing wrong? I can't figure out why the count is wrong, but my primary issue/concern I want to resolve is why the overlay wont display properly?
Print Statement
Cell selected: [0, 0]
Count :0
Count :0
Count :0
Cell selected: [0, 4]
Count :0
View Controller
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.setupView()
print("Cell selected: \(indexPath)")
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.backgroundColor = nil
cell.imageView.alpha = 1
}
}
Custom Overlay
lazy var circleView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.layer.cornerRadius = self.countSize.width / 2
view.alpha = 0.4
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var 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
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private func setup(){addSubview(circleView)
addSubview(circleView)
addSubview(countLabel)
NSLayoutConstraint.activate([
circleView.leadingAnchor.constraint(equalTo: leadingAnchor),
circleView.trailingAnchor.constraint(equalTo: trailingAnchor),
circleView.topAnchor.constraint(equalTo: topAnchor),
circleView.bottomAnchor.constraint(equalTo: bottomAnchor),
countLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
countLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
countLabel.topAnchor.constraint(equalTo: topAnchor),
countLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
TestCVCell: UICollectionViewCell
override var isSelected: Bool {
didSet { overlay.isHidden = !isSelected }
}
var imageView: UIImageView = {
let view = UIImageView()
view.clipsToBounds = true
view.contentMode = .scaleAspectFill
view.backgroundColor = UIColor.gray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var count: Int = 0 {
didSet { overlay.countLabel.text = "\(count)" }
}
let overlay: CustomAssetCellOverlay = {
let view = CustomAssetCellOverlay()
view.isHidden = true
return view
}()
func setupView() {
addSubview(imageView)
addSubview(overlay)
print("Count :\(count)")
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),
])
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = self.bounds
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
fatalError("init(coder:) has not been implemented")
}
Based on your other question, I'm guessing you are trying to do something like this...
Display images from device Photos, and allow multiple selections in order:
and, when you de-select a cell - for example, de-selecting my 2nd selection - you want to re-number the remaining selections:
To accomplish this, you need to keep track of the cell selections in an array - as they are made - so you can maintain the numbering.
Couple ways to approach this... here is one.
First, I'd suggest re-naming your count property to index, and, when setting the value, show or hide the overlay:
var index: Int = 0 {
didSet {
overlay.countLabel.text = "\(index)"
// hide if count is Zero, show if not
overlay.isHidden = index == 0
}
}
When you dequeue a cell from cellForItemAt, see if the indexPath is in our "tracking" array and set the cell's .index property appropriately (which will also show/hide the overlay).
Next, when you select a cell:
add the indexPath to our tracking array
we can set the .index property - with the count of our tracking array - directly to update the cell's appearance, because it won't affect any other cells
When you de-select a cell, we have to do additional work:
remove the indexPath from our tracking array
reload the cells so they are re-numbered
Here is a complete example - lots of comments in the code.
CircleView
class CircleView: UIView {
// simple view subclass that keeps itself "round"
// (assuming it has a 1:1 ratio)
override func layoutSubviews() {
layer.cornerRadius = bounds.width * 0.5
}
}
CustomAssetCellOverlay
class CustomAssetCellOverlay: UIView {
lazy var circleView: CircleView = {
let view = CircleView()
view.backgroundColor = UIColor(red: 0.0, green: 0.5, blue: 1.0, alpha: 1.0)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var 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
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private func setup(){addSubview(circleView)
addSubview(circleView)
addSubview(countLabel)
NSLayoutConstraint.activate([
// circle view at top-left
circleView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4.0),
circleView.topAnchor.constraint(equalTo: topAnchor, constant: 4.0),
// circle view Width: 28 Height: 1:1 ratio
circleView.widthAnchor.constraint(equalToConstant: 28.0),
circleView.heightAnchor.constraint(equalTo: circleView.widthAnchor),
// count label constrained ot circle view
countLabel.leadingAnchor.constraint(equalTo: circleView.leadingAnchor),
countLabel.trailingAnchor.constraint(equalTo: circleView.trailingAnchor),
countLabel.topAnchor.constraint(equalTo: circleView.topAnchor),
countLabel.bottomAnchor.constraint(equalTo: circleView.bottomAnchor),
])
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
}
TestCVCell
class TestCVCell: UICollectionViewCell {
var imageView = UIImageView()
var index: Int = 0 {
didSet {
overlay.countLabel.text = "\(index)"
// hide if count is Zero, show if not
overlay.isHidden = index == 0
}
}
let overlay: CustomAssetCellOverlay = {
let view = CustomAssetCellOverlay()
view.backgroundColor = UIColor.black.withAlphaComponent(0.4)
view.isHidden = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
contentView.addSubview(imageView)
contentView.addSubview(overlay)
imageView.translatesAutoresizingMaskIntoConstraints = false
overlay.translatesAutoresizingMaskIntoConstraints = false
// constrain both image view and overlay to full contentView
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
overlay.topAnchor.constraint(equalTo: imageView.topAnchor),
overlay.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
overlay.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
overlay.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
TrackSelectionsViewController
class TrackSelectionsViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate {
var myCollectionView: UICollectionView!
// array to track selected cells in the order they are selected
var selectedCells: [IndexPath] = []
// to load assests when needed
let imgManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
// will be used to get photos data
var fetchResult: PHFetchResult<PHAsset>!
override func viewDidLoad() {
super.viewDidLoad()
// set main view background color to a nice medium blue
view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
// request Options to be used in cellForItemAt
requestOptions.isSynchronous = false
requestOptions.deliveryMode = .opportunistic
// vertical stack view for the full screen (safe area)
let mainStack = UIStackView()
mainStack.axis = .vertical
mainStack.spacing = 0
mainStack.translatesAutoresizingMaskIntoConstraints = false
// add it to the view
view.addSubview(mainStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: g.topAnchor, constant:0.0),
mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
// create a label
let label = UILabel()
// add the label to the main stack view
mainStack.addArrangedSubview(label)
// label properties
label.textColor = .white
label.textAlignment = .center
label.text = "Select Photos"
label.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
// setup the collection view
setupCollection()
// add it to the main stack view
mainStack.addArrangedSubview(myCollectionView)
// start the async call to get the assets
grabPhotos()
}
func setupCollection() {
let layout = UICollectionViewFlowLayout()
myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
myCollectionView.delegate = self
myCollectionView.dataSource = self
myCollectionView.backgroundColor = UIColor.white
myCollectionView.allowsMultipleSelection = true
myCollectionView.register(TestCVCell.self, forCellWithReuseIdentifier: "cvCell")
}
//MARK: CollectionView
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
// add newly selected cell (index path) to our tracking array
selectedCells.append(indexPath)
// when selecting a cell,
// we can update the appearance of the newly selected cell
// directly, because it won't affect any other cells
cell.index = selectedCells.count
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
// when de-selecting a cell,
// we can't update the appearance of the cell directly
// because if it's not the last cell selected, the other
// selected cells need to be re-numbered
// get the index of the deselected cell from our tracking array
guard let idx = selectedCells.firstIndex(of: indexPath) else { return }
// remove from our tracking array
selectedCells.remove(at: idx)
// reloadData() clears the collection view's selected cells, so
// get a copy of currently selected cells
let curSelected: [IndexPath] = collectionView.indexPathsForSelectedItems ?? []
// reload collection view
// we do this to update all cells' appearance,
// including re-numbering the currently selected cells
collectionView.reloadData()
// save current Y scroll offset
let saveY = collectionView.contentOffset.y
collectionView.performBatchUpdates({
// re-select previously selected cells
curSelected.forEach { pth in
collectionView.selectItem(at: pth, animated: false, scrollPosition: .centeredVertically)
}
}, completion: { _ in
// reset Y offset
collectionView.contentOffset.y = saveY
})
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard fetchResult != nil else { return 0 }
return fetchResult.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cvCell", for: indexPath) as! TestCVCell
imgManager.requestImage(for: fetchResult.object(at: indexPath.item) as PHAsset, targetSize: CGSize(width:120, height: 120),contentMode: .aspectFill, options: requestOptions, resultHandler: { (image, error) in
cell.imageView.image = image
})
// get the index of this indexPath from our tracking array
// if it's not there (nil), set it to -1
let idx = selectedCells.firstIndex(of: indexPath) ?? -1
// set .count property to index + 1 (arrays are zero-based)
cell.index = idx + 1
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width
return CGSize(width: width/4 - 1, height: width/4 - 1)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
myCollectionView.collectionViewLayout.invalidateLayout()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
//MARK: grab photos
func grabPhotos(){
DispatchQueue.global(qos: .background).async {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
self.fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
if self.fetchResult.count == 0 {
print("No photos found.")
}
DispatchQueue.main.async {
self.myCollectionView.reloadData()
}
}
}
}
Note: This is example code only!!! It should not be considered "production ready."
Shouldn't your var count: Int = 0 be set at your CollectionView delegate?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.setupView()
cell.count = indexPath.item
print("Cell selected: \(indexPath)")
}
}

UICollectionView weird cells animation when dismiss mode of the keyboard is interactive

I'm trying to achieve a chat system using UICollectionView -The collectionview is turned upside down and so are the cells using CGAffineTransform(scaleX: -1, y: 1)- and so far everything is going well, I'm calculating cell sizes manually using NSString.boundingRect and it's working as expected, then I enabled the collectionview's interactive keyboard dismissal since then whenever I drag the keyboard a little bit then I let go it results into a really weird animation which I can't seem to figure out what's triggering it.
I also use Typist which is a small utility class that facilitates keyboard handling https://github.com/totocaster/Typist, I'm not really suspecting it since it is a simple wrapper UIKeyboard notifications.
I've tried several stuff including but not limited to
1-Disabling animations when showing cell using
UIView.setAnimationsEnabled(false) then enabling it again just before returning the cell which did not work.
2-Removing the upside-down approach altogether, and again nope even when it's in a normal transform the bug still occurs
3-Rewrote the entire UICollectionView implementation to UITableView implementation which -surprisingly- yielded a better result the weird animation isn't as strong as it's on the UICollectionView but it's still happening to some extent
4-Removing Typist altogether and reverting back to NotificationCenter and manual handling and nope, does not work.
UICollectionView initialization
lazy var chatCollectionView: UICollectionView = {
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.scrollDirection = .vertical
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .white
collectionView.showsHorizontalScrollIndicator = false
collectionView.showsVerticalScrollIndicator = false
collectionView.register(OutgoingTextCell.self, forCellWithReuseIdentifier: OutgoingTextCell.className)
collectionView.register(IncomingTextCell.self, forCellWithReuseIdentifier: IncomingTextCell.className)
collectionView.register(OutgoingImageCell.self, forCellWithReuseIdentifier: OutgoingImageCell.className)
collectionView.register(IncomingImageCell.self, forCellWithReuseIdentifier: IncomingImageCell.className)
collectionView.transform = CGAffineTransform(scaleX: 1, y: -1)
collectionView.semanticContentAttribute = .forceLeftToRight
collectionView.keyboardDismissMode = .interactive
return collectionView
}()
UICollectionView DataSource / Delete implementation
extension ChatViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return presenter.numberOfRows
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if presenter.isSender(at: indexPath) {
switch presenter.messageType(at: indexPath) {
case .text:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OutgoingTextCell.className, for: indexPath) as! OutgoingTextCell
presenter.configure(cell: AnyConfigurableCell(cell), at: indexPath)
return cell
case .photo:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OutgoingImageCell.className, for: indexPath) as! OutgoingImageCell
presenter.configure(cell: AnyConfigurableCell(cell), at: indexPath)
return cell
}
} else {
switch presenter.messageType(at: indexPath) {
case .text:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: IncomingTextCell.className, for: indexPath) as! IncomingTextCell
presenter.configure(cell: AnyConfigurableCell(cell), at: indexPath)
return cell
case .photo:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: IncomingImageCell.className, for: indexPath) as! IncomingImageCell
presenter.configure(cell: AnyConfigurableCell(cell), at: indexPath)
return cell
}
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if presenter.messageType(at: indexPath) == .photo {
return CGSize(width: view.frame.width, height: 250)
} else {
if let height = cachedSizes[presenter.uuidForMessage(at: indexPath)] {
return CGSize(width: view.frame.width, height: height)
} else {
let height = calculateTextHeight(at: indexPath)
cachedSizes[presenter.uuidForMessage(at: indexPath)] = height
return CGSize(width: view.frame.width, height: height)
}
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == presenter.numberOfRows - 1 && !isPaginating {
presenter.loadNextPage()
}
}
private func calculateTextHeight(at indexPath: IndexPath) -> CGFloat {
let approximateSize = CGSize(width: view.frame.width - 88, height: .greatestFiniteMagnitude)
let estimatedHeight = NSString(string: presenter.contentForMessage(at: indexPath)).boundingRect(with: approximateSize, options: .usesLineFragmentOrigin, attributes: [.font: DinNextFont.regular.getFont(ofSize: 14)], context: nil).height
return estimatedHeight + 40
}
}
Cell implementation
class OutgoingTextCell: UICollectionViewCell, ConfigurableCell {
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .coral
view.layer.cornerRadius = 10
return view
}()
lazy var contentLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .white
label.numberOfLines = 0
label.font = DinNextFont.regular.getFont(ofSize: 14)
return label
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [timeLabel, checkMarkImageView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.alignment = .center
stackView.spacing = 4
return stackView
}()
private lazy var checkMarkImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "ic_check")?.withRenderingMode(.alwaysTemplate))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.tintColor = .white
imageView.heightAnchor.constraint(equalToConstant: 8).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 10).isActive = true
return imageView
}()
lazy var timeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .white
label.font = DinNextFont.regular.getFont(ofSize: 10)
label.text = "12:14"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
layoutUI()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func addSubviews() {
addSubview(containerView)
containerView.addSubview(contentLabel)
containerView.addSubview(stackView)
}
func setupContainerViewConstraints() {
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 0),
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 100),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 34),
containerView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 64)
])
}
private func setupContentLabelConstraints() {
NSLayoutConstraint.activate([
contentLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 4),
contentLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
contentLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
])
}
private func setupStackViewConstraints() {
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 0),
stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -6),
stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4),
stackView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor, constant: 6),
stackView.heightAnchor.constraint(equalToConstant: 20),
])
}
private func layoutUI() {
addSubviews()
setupContainerViewConstraints()
setupContentLabelConstraints()
setupStackViewConstraints()
}
func configure(model: MessageViewModel) {
containerView.transform = CGAffineTransform(scaleX: 1, y: -1)
contentLabel.text = model.content
timeLabel.text = model.time
checkMarkImageView.isHidden = !model.isSent
}
}
The output of UICollectionView is as the following
https://www.youtube.com/watch?v=RajfZk5lGCQ
The output of the UITableview is as the following
https://www.youtube.com/watch?v=6ayG7WhYKXo
It might not be really clear but if you look closely you can see the cells are "briefly" animating and with longer messages it looks kind of "sliding animation" which does not leak appealing at all.

UICollectionView inside UITableViewCell with UITableViewAutoDimesion

I am working on a project needs to add a UICollectionView(horizontal direction) inside UITableViewCell. The UITableViewCell height is using UITableViewAutoDimension and each UITableViewCell I am having a UIView(with a border for design requirements) as a base view, and in the UIView, I have a UIStackView added in as the containerView to proportionally fill the UICollectionView with two other buttons vertically. And then for UICollectionViewCell, I have added in a UIStackView to fill five labels.
Now, the auto-layout works if the UITableViewDelegate assigns a fixed height. But it doesn't work with the UITableViewAutoDimension. My guessing is that UICollectionView frame is not ready while UITableView is rendering its' cells. So the UITableViewAutoDimension calculates the UITableViewCell height with a default height of the UICollectionView which is zero.
So, of course, I have been searching before I throw a question out on the Internet but no solutions worked for me. Here are some links I have tried.
UICollectionView inside a UITableViewCell — dynamic height?
Making UITableView with embedded UICollectionView using UITableViewAutomaticDimension
UICollectionView inside UITableViewCell does NOT dynamically size correctly
Does anyone have the same issue? If the links above did work, please feel free to let me know in case I did it wrong. Thank you
--- Updated Sep 23th, 2018:
Layout Visualization:
There are some UI modifications, but it does not change the issue that I am facing. Hope the picture can help.
Code:
The current code I have is actually not using the UIStackView in UITableViewCell and the UITableView heightForRowAtIndex I return a fixed height with 250.0. However, UITableView won't configure the cell height properly if I return UITableViewAutoDimension as I mentioned in my question.
1. UITableViewController
class ViewController: UIViewController {
private let tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(ViewControllerTableViewCell.self, forCellReuseIdentifier: ViewControllerTableViewCell.identifier)
return tableView
}()
private lazy var viewModel: ViewControllerViewModel = {
return ViewControllerViewModel(models: [
Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 100, summary: "1% up"),
Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 200, summary: "2% up"),
Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 300, summary: "3% up"),
])
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.numberOfSections
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.numberOfRowsInSection
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ViewControllerTableViewCell.identifier, for: indexPath) as! ViewControllerTableViewCell
cell.configure(viewModel: viewModel.cellViewModel)
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 250.0
}
}
2. UITableViewCell
class ViewControllerTableViewCell: UITableViewCell {
static let identifier = "ViewControllerTableViewCell"
private var viewModel: ViewControllerTableViewCellViewModel!
private let borderView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.black.cgColor
view.layer.borderWidth = 1
return view
}()
private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillProportionally
return stackView
}()
private let seperator: UIView = {
let seperator = UIView()
seperator.translatesAutoresizingMaskIntoConstraints = false
seperator.backgroundColor = .lightGray
return seperator
}()
private let actionButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Show business insight", for: .normal)
button.setTitleColor(.black, for: .normal)
return button
}()
private let pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.pageIndicatorTintColor = .lightGray
pageControl.currentPageIndicatorTintColor = .black
pageControl.hidesForSinglePage = true
return pageControl
}()
private let collectionView: UICollectionView = {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: 200, height: 200), collectionViewLayout: layout)
collectionView.isPagingEnabled = true
collectionView.backgroundColor = .white
collectionView.showsHorizontalScrollIndicator = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(ViewControllerCollectionViewCell.self, forCellWithReuseIdentifier: ViewControllerCollectionViewCell.identifier)
return collectionView
}()
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpConstraints()
setUpUserInterface()
}
func configure(viewModel: ViewControllerTableViewCellViewModel) {
self.viewModel = viewModel
pageControl.numberOfPages = viewModel.items.count
collectionView.reloadData()
}
#objc func pageControlValueChanged() {
let indexPath = IndexPath(item: pageControl.currentPage, section: 0)
collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
}
private func setUpConstraints() {
contentView.addSubview(borderView)
borderView.addSubview(actionButton)
borderView.addSubview(seperator)
borderView.addSubview(pageControl)
borderView.addSubview(collectionView)
NSLayoutConstraint.activate([
borderView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
borderView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
borderView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
borderView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
])
NSLayoutConstraint.activate([
actionButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
actionButton.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
actionButton.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
actionButton.bottomAnchor.constraint(equalTo: borderView.bottomAnchor)
])
NSLayoutConstraint.activate([
seperator.heightAnchor.constraint(equalToConstant: 1),
seperator.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
seperator.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
seperator.bottomAnchor.constraint(equalTo: actionButton.topAnchor)
])
NSLayoutConstraint.activate([
pageControl.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
pageControl.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
pageControl.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
pageControl.bottomAnchor.constraint(equalTo: seperator.topAnchor)
])
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: borderView.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: pageControl.topAnchor)
])
}
private func setUpUserInterface() {
selectionStyle = .none
collectionView.delegate = self
collectionView.dataSource = self
pageControl.addTarget(self, action: #selector(pageControlValueChanged), for: .valueChanged)
}
}
extension ViewControllerTableViewCell: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return viewModel!.numberOfSections
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel!.numberOfRowsInSection
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ViewControllerCollectionViewCell.identifier, for: indexPath) as! ViewControllerCollectionViewCell
let collectionCellViewModel = viewModel!.collectionCellViewModel(at: indexPath)
cell.configure(viewModel: collectionCellViewModel)
return cell
}
}
extension ViewControllerTableViewCell: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
debugPrint("did select \(indexPath.row)")
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
pageControl.currentPage = indexPath.row
}
}
extension ViewControllerTableViewCell: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width - 40.0, height: collectionView.frame.height)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 20.0, bottom: 0, right: 20.0)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 40.0
}
}
3. UICollectionViewCell
class ViewControllerCollectionViewCell: UICollectionViewCell {
override class var requiresConstraintBasedLayout: Bool {
return true
}
static let identifier = "ViewControllerCollectionViewCell"
private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillEqually
return stackView
}()
private let titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.textColor = .black
titleLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
return titleLabel
}()
private let descriptionLabel: UILabel = {
let descriptionLabel = UILabel()
descriptionLabel.textAlignment = .right
descriptionLabel.textColor = .black
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
return descriptionLabel
}()
private let amountLabel: UILabel = {
let amountLabel = UILabel()
amountLabel.textColor = .black
amountLabel.textAlignment = .right
amountLabel.translatesAutoresizingMaskIntoConstraints = false
return amountLabel
}()
private let summaryLabel: UILabel = {
let summaryLabel = UILabel()
summaryLabel.textColor = .black
summaryLabel.textAlignment = .right
summaryLabel.translatesAutoresizingMaskIntoConstraints = false
return summaryLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(stackView)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(descriptionLabel)
stackView.addArrangedSubview(amountLabel)
stackView.addArrangedSubview(summaryLabel)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(viewModel: CollectionCellViewModel) {
titleLabel.text = viewModel.title
descriptionLabel.text = viewModel.description
amountLabel.text = viewModel.amount.localizedCurrencyString(with: viewModel.currency)
summaryLabel.text = viewModel.summary
}
}
To enable self-sizing table view cells, you must set the table view’s rowHeight property to UITableViewAutomaticDimension. You must also assign a value to the estimatedRowHeight property, you also need an unbroken chain of constraints and views to fill the content view of the cell.
More: https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html#//apple_ref/doc/uid/TP40010853-CH25-SW1
So in this case, the UIView with border determines the table view cell height dynamically, you have to tell the system how tall this UIView is, you can constraint this view to the UIStackView edges (top, bottom, leading, trailing), and use the UIStackView intrinsic content height as the UIView(with border) height.
The trouble is in the UIStackView intrinsic content height and with the distribution property. The UIStackView can estimate a height automatically for the two UIButtons (based on the size of text, the font style, and the font size), but the UICollectionView has no intrinsic content height, and since your UIStackview is filled proportionally the UIStackView sets a height of 0.0 for the UICollectionView, so it looks like something like this:
Fill Proportionally UIStackView
But if you change the UIStackView distribution property to fill equally you will have something like this:
Fill Equally UIStackView
If you want the UICollectionView to determine its own size and fill UIStackView proportionally, you need to set a height constraint for the UICollectionView. And then update the constraint based on the UICollectionViewCell content height.
Your code looks good, you only need to make minor changes,
Here is a solution (updated 09/25/2018):
private func setUpConstraints() {
contentView.addSubview(borderView)
borderView.addSubview(actionButton)
borderView.addSubview(seperator)
borderView.addSubview(pageControl)
borderView.addSubview(collectionView)
NSLayoutConstraint.activate([
borderView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), // --- >. Use cell content view anchors
borderView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), // --- >. Use cell content view anchors
borderView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), // --- >. Use cell content view anchors
borderView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), // --- >. Use cell content view anchors
])
//other constraints....
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: borderView.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: pageControl.topAnchor),
collectionView.heightAnchor.constraint(equalToConstant: 200) // ---> System needs to know height, add this constraint to collection view.
])
}
Also remember to set the tableView rowHeight to UITableView.automaticDimension & to give the system an estimated row height with: tableView.estimatedRowHeight.

Resources