UICollectionView inside UITableViewCell with UITableViewAutoDimesion - uitableview

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.

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

How to create a slow and continuous scroll with CollectionView Swift

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

How to connect UIPageControl and CollectionView?

I have collection view with Custom View Cell. There are scroll view and three image view in a custom view cell.
My ViewController has UIPageControll, but I don't know how to connect UIPageControll and scroll view.
My code
ViewController:
class MainScrenenViewController: UIViewController {
let data = [
CustomData(title: "A", backgroundImage: #imageLiteral(resourceName: "Onboard")),
CustomData(title: "B", backgroundImage: #imageLiteral(resourceName: "Onboard")),
CustomData(title: "B", backgroundImage: #imageLiteral(resourceName: "Onboard")),
]
//UIPage Controller
lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.numberOfPages = data.count
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.addTarget(self, action: #selector(pageControlTapHandler(sender:)), for: .touchUpInside)
return pageControl
}()
var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(DayWeatherCell.self, forCellWithReuseIdentifier: "sliderCell")
collectionView.layer.cornerRadius = 5
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = UIColor(red: 0.125, green: 0.306, blue: 0.78, alpha: 1)
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .brown
view.addSubview(collectionView)
view.addSubview(pageControl)
collectionView.dataSource = self
collectionView.delegate = self
setupConstraints()
}
//MARK: ~FUNCTIONS
func setupConstraints() {
let constraints = [
collectionView.widthAnchor.constraint(equalToConstant: 344),
collectionView.heightAnchor.constraint(equalToConstant: 212),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 112),
pageControl.topAnchor.constraint(equalTo: cityLabel.bottomAnchor, constant: 10),
pageControl.widthAnchor.constraint(equalToConstant: 100),
pageControl.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
]
NSLayoutConstraint.activate(constraints)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
//Selector for UIPage Controller
#objc func pageControlTapHandler(sender: UIPageControl) {
//I don't know what I need to do here
}
extension MainScrenenViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "sliderCell", for: indexPath) as! DayWeatherCell
cell.backgroundColor = .red
return cell
}
}
extension MainScrenenViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("User tapped on item \(indexPath.row)")
}
}
extension MainScrenenViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
}
}
extension MainScrenenViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
}
My CollectionViewCell:
class DayWeatherCell: UICollectionViewCell, UIScrollViewDelegate {
weak var mainScreenViewController: MainScrenenViewController?
var data: CustomData? {
didSet {
guard let data = data else { return }
imageView.image = data.backgroundImage
}
}
var imageView: UIImageView = {
let imageView = UIImageView()
imageView.image = #imageLiteral(resourceName: "Onboard")
imageView.layer.cornerRadius = 5
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.showsHorizontalScrollIndicator = false
scrollView.isPagingEnabled = true
scrollView.delegate = self
return scrollView
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(scrollView)
contentView.addSubview(imageView)
self.contentView.layer.cornerRadius = 10
let constraints = [
scrollView.topAnchor.constraint(equalTo: contentView.topAnchor),
scrollView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
scrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
]
NSLayoutConstraint.activate(constraints)
}
required init?( coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
enter image description here
You can use this. In this code block, collection view cell size equal to collection view and collection view scroll horizontally. I hope, this helps you.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offSet = scrollView.contentOffset.x
let width = scrollView.frame.width
let horizontalCenter = width / 2
pageControl.currentPage = Int(offSet + horizontalCenter) / Int(width)
}

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.

Why Xcode simulator view is different from view-hierarchy view

I am developing a UIImageView on top of UICollectionView using autolayout constraints:
I have anchored the UIImageView to take the top part of screen with a fixed H:W ratio of 4:3, then let UICollectionView to take whatever space is left at the bottom.
Strangely, when I ran it, the view in xcode simulator is quite different from the view-hierarchy debugger:
View Hierarchy Debugger:
iPhone 8 Plus simulator:
In the UICollectionView, each cell will show a face photo, I have configured the itemSize of UICollectionView to be a square. It should show then entire face (so the debugger view is correct, the simulator is not).
// MARK: - UICollectionViewDelegateFlowLayout
extension ViewController : UICollectionViewDelegateFlowLayout {
// set item size
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// gw: to force one row, height need to be smaller than flow height
return CGSize(width: collectionView.bounds.height, height: collectionView.bounds.height)
}
}
What could be the cause of this difference?
I am using xcode 10.1, ios 12.1
Full code is here (not very long):
import UIKit
class ViewController: UICollectionViewController{
let collectionViewCellIdentifier = "MyCollectionViewCellIdentifier"
let canvas:Canvas = {
let canvas = Canvas()
canvas.backgroundColor = UIColor.black
canvas.translatesAutoresizingMaskIntoConstraints=false
canvas.alpha = 0.2
return canvas
} ()
let photoView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(imageLiteralResourceName: "hongjinbao")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
return imageView
} ()
private let myArray: NSArray = ["First","Second","Third"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// stack views
view.addSubview(photoView)
view.addSubview(canvas)
collectionView?.backgroundColor = UIColor.white
collectionView?.translatesAutoresizingMaskIntoConstraints = false
collectionView?.register(PersonCollectionViewCell.self, forCellWithReuseIdentifier: collectionViewCellIdentifier)
setupLayout()
}
private func setupLayout() {
photoView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
photoView.heightAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1.333).isActive = true
photoView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
photoView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
canvas.topAnchor.constraint(equalTo: photoView.topAnchor).isActive = true
canvas.bottomAnchor.constraint(equalTo: photoView.bottomAnchor).isActive = true
canvas.leadingAnchor.constraint(equalTo: photoView.leadingAnchor).isActive = true
canvas.trailingAnchor.constraint(equalTo: photoView.trailingAnchor).isActive = true
collectionView?.topAnchor.constraint(equalTo: photoView.bottomAnchor).isActive = true
collectionView?.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
collectionView?.leadingAnchor.constraint(equalTo: photoView.leadingAnchor).isActive = true
collectionView?.trailingAnchor.constraint(equalTo: photoView.trailingAnchor).isActive = true
}
}
// MARK: - UICollectionViewDataSource
extension ViewController {
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.collectionViewCellIdentifier, for: indexPath)
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 13
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension ViewController : UICollectionViewDelegateFlowLayout {
// set item size
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// gw: to force one row, height need to be smaller than flow height
return CGSize(width: collectionView.bounds.height, height: collectionView.bounds.height)
}
}
import UIKit
class PersonCollectionViewCell: UICollectionViewCell {
let face: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(imageLiteralResourceName: "mary")
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
} ()
let name: UILabel = {
let textLabel = UILabel()
textLabel.translatesAutoresizingMaskIntoConstraints = false
return textLabel
} ()
let desc: UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
} ()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
// gw: needed by compiler
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
addSubview(face)
addSubview(name)
addSubview(desc)
backgroundColor = UIColor.red
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-16-[v0]|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["v0":face]))
}
}
I found the cause, it is due to the stacking order of subviews in my ViewController. The photoView was add on top of the collectionView, and it blocked part of collectionView.
Solution: in viewDidLoad add below statement to bring the later one to front:
// stack views
view.addSubview(photoView)
// little trick to bring inherent collectionView to front
view.bringSubviewToFront(self.collectionView)

Resources