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.)
Related
I had everything working perfectly in test.
In production, a user saved a few images, two are ok but for some reason, two are doubling up on top of another image.
When tapping on the image (didSelectItemAt)
collectionView.reloadData()
Gets called and each tap, changes the image to clear it up into just one image.
I've worked back from this point but I'm stuck.
Images loaded in viewDidLoad
db.collection("SAVED IMAGE IDS").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)")
}
else
{
for document in querySnapshot!.documents
{
let id = document.documentID
let Ref = Storage.storage().reference(forURL: "SavedUserImages/\(id)")
Ref.getData(maxSize: 1 * 1024 * 1024)
{
data, error in
if error != nil
{
print("Error: Image could not download!")
}
else
{
let image = UIImage(data: data!)
self.picArray.append(image!)
self.imageID.append(id)
self.collectionView.reloadData()
}
}
}
}
}
Image loads in cell for row
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath as IndexPath)
let data = picArray[indexPath.row]
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
let iv = UIImageView()
cell.contentView.addSubview(iv)
iv.frame = cell.contentView.frame
iv.image = data
collectionView.selectItem(at: IndexPath(item: 0, section: 0) as IndexPath, animated: false, scrollPosition: .init())
return cell
}
Thanks in advance for any help
With this code:
you are adding another image view every time you reload the cell.
Instead, you need to design your cell to already have the image view and change your code to:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath as IndexPath)
let data = picArray[indexPath.row]
cell.iv.image = data
return cell
}
Edit - further explanation and examples...
Based on the code you've shown, you are using a default UICollectionViewCell instead of a custom subclassed cell.
So, if we do a complete example, using SF Symbol images from 0 to 14 for the picArray, using your approach:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
let cellID: String = "cell"
var collectionView: UICollectionView!
var cvWidth: CGFloat = 0
// let's use an array of images for this example
var picArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
// create images 0 through 14
for i in 0..<15 {
if let img = UIImage(systemName: "\(i).circle") {
picArray.append(img)
}
}
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .vertical
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
collectionView.dataSource = self
collectionView.delegate = self
// it appears you're using a default collection view cell class
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellID)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only do this is the collection view frame has changed
if cvWidth != collectionView.frame.width {
cvWidth = collectionView.frame.width
if let fl = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
fl.itemSize = CGSize(width: cvWidth, height: 200.0)
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return picArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath as IndexPath)
let data = picArray[indexPath.row]
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
// this is wrong... we're Adding ANOTHER image view every time
let iv = UIImageView()
cell.contentView.addSubview(iv)
iv.frame = cell.contentView.frame
iv.image = data
// this makes no sense, but I'll leave it here
collectionView.selectItem(at: IndexPath(item: 0, section: 0) as IndexPath, animated: false, scrollPosition: .init())
return cell
}
}
It looks like this at the start:
if we scroll all the way down - to where the "14" image should be the bottom cell, it looks like this:
If we scroll back to the top:
and after scrolling up and down several times:
As we can see, as the cells are reused we keep adding more and more image views on top of each other.
So, instead, let's create a simple custom cell subclass that creates and adds an image view when it is created:
class SimpleImageCell: UICollectionViewCell {
let imgView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
imgView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(imgView)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
imgView.topAnchor.constraint(equalTo: g.topAnchor),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
}
}
and we'll use an almost identical view controller - the only differences are registering our SimpleImageCell class, and using a correct cellForItemAt func:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
let cellID: String = "cell"
var collectionView: UICollectionView!
var cvWidth: CGFloat = 0
// let's use an array of images for this example
var picArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
// create images 0 through 14
for i in 0..<15 {
if let img = UIImage(systemName: "\(i).circle") {
picArray.append(img)
}
}
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .vertical
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
collectionView.dataSource = self
collectionView.delegate = self
// register cell class that already has an image view
collectionView.register(SimpleImageCell.self, forCellWithReuseIdentifier: cellID)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only do this is the collection view frame has changed
if cvWidth != collectionView.frame.width {
cvWidth = collectionView.frame.width
if let fl = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
fl.itemSize = CGSize(width: cvWidth, height: 200.0)
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return picArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! SimpleImageCell
let data = picArray[indexPath.item]
cell.imgView.image = data
return cell
}
}
The results:
We can scroll up and down all we want, and we will never have images "on top of" each other.
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
}
}
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)
}
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)
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.