UIImageView view in UICollectionView cell is not resizing to cell width - ios

I'm creating simple UICollectionView with 3 column per row cells.
My cell is simple - just UIImageView stretched to 4 sides:
class FollowerCell: UICollectionViewCell {
static let reuseID = "FollowerCell"
var avatarImageView:UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
avatarImageView = UIImageView()
addSubview(avatarImageView)
contentView.layer.borderColor = UIColor.red.cgColor
contentView.layer.cornerRadius = 5
contentView.layer.borderWidth = 5
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
avatarImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
avatarImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
avatarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
avatarImageView.image = UIImage(named: "avatar-placeholder")
}
}
In ViewController I define width on cells by implementing UICollectionViewDelegateFlowLayout's method sizeForItemAt:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = self.view.bounds.width
let itemWidth = width / 3
return CGSize(width: itemWidth, height: itemWidth)
}
And i have strange result - my UIImageView size ignores cell size (which is red rectangle):
Github: https://github.com/afirthes/TryUIViewCollection
When I try to do the same in storyboard, it seems to work well:
What i'm missing when creating UICollectionView programmatically ?
Im following tutorial from 2021 where it works just fine, seems something was changed in XCode 11.

Main change
contentView.addSubview(avatarImageView)
Full class
import UIKit
class FollowerCell: UICollectionViewCell {
static let reuseID = "FollowerCell"
var avatarImageView:UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
avatarImageView = UIImageView()
contentView.addSubview(avatarImageView)
contentView.layer.borderColor = UIColor.red.cgColor
contentView.layer.cornerRadius = 5
contentView.layer.borderWidth = 5
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
avatarImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
avatarImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
avatarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
avatarImageView.image = UIImage(named: "avatar-placeholder")
}
}

Related

UITableViewCell shadows overlay

I have a table view with cells.
Overlaying shadows is done, but that looks not like I wanted.
My shadow white round rectangles should stay white. And shadows should overlay below white rectangles. Any suggestions on how to achieve expected behavior?
I added shadow as a separate subview
class ShadowView: UIView {
override var bounds: CGRect {
didSet {
setupShadow()
}
}
private func setupShadow() {
layer.shadowColor = UIColor.red.cgColor
layer.shadowOpacity = 1
layer.shadowRadius = 40
layer.shadowOffset = CGSize(width: 1, height: 10)
layer.masksToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 5).cgPath
}
}
and then
let shadowView = ShadowView()
addSubview(shadowView)
I wanted something like this. White rectangles are completely white.
The problem, as you are seeing, is that rows (cells) are separate views. If you allow an element to extend outside the cell, it will either overlap or underlap the adjacent views.
Here's a simple example to clarify...
Each cell has a systemYellow view that extends outside its frame on the top and bottom:
If we use Debug View Hierarchy to inspect the layout, it looks something like this:
As we can see, because of the initial z-order, each cell is covering the part of the systemYellow view that is extending up and the part that is extending down overlaps the next cell.
As we scroll a bit, cells are re-drawn at different z-order positions (based on how the tableView re-uses them):
Now we see that some of the systemYellow views overlap the row above, some overlap the row below, and some overlap both.
Inspecting the layout shows us the cells' z-order positions:
If we want to maintain the z-order so that none of the systemYellow views overlap the cell below it, we can add a func to manipulate the z-order positions:
func updateLayout() -> Void {
for c in tableView.visibleCells {
tableView.bringSubviewToFront(c)
}
}
and we need to call that whenever the tableView scrolls (and when the layout changes):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateLayout()
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateLayout()
}
So, the same thing is happening with your layout... the shadows are extending outside the frame of the cell, and either over- or under-lapping the adjacent cells.
If we start by using the same approach to manage the z-order of the cells, we can get this:
So, we're keeping the white rounded-rect views on top of the "shadow above." Of course, now we have the shadows overlapping the bottom of the view.
We can change the rectangle for the .shadowPath to avoid that:
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
r.origin.y += 40
layer.shadowPath = UIBezierPath(roundedRect: r, cornerRadius: 5).cgPath
}
and we get this output:
One more issue though -- if we use the default cell .selectionStyle, we get this:
which is probably not acceptable.
So, we can set the .selectionStyle to .none, and implement setSelected in our cell class. Here, I change the rounded-rect background and the text colors to make it extremely obvious:
Here is some example code -- no #IBOutlet or #IBAction connections needed, so just assign the class of a new table view controller to ShadowTableViewController :
class ShadowView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.shadowColor = UIColor.red.cgColor
layer.shadowOpacity = 1
layer.shadowRadius = 40
layer.masksToBounds = false
layer.cornerRadius = 12
layer.shouldRasterize = true
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
r.origin.y += 40
layer.shadowPath = UIBezierPath(roundedRect: r, cornerRadius: 5).cgPath
}
}
class ShadowCell: UITableViewCell {
let shadowView = ShadowView()
let topLabel = UILabel()
let bottomLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shadowView.backgroundColor = .white
topLabel.font = .boldSystemFont(ofSize: 24.0)
bottomLabel.font = .italicSystemFont(ofSize: 20.0)
bottomLabel.numberOfLines = 0
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 8
stack.addArrangedSubview(topLabel)
stack.addArrangedSubview(bottomLabel)
shadowView.translatesAutoresizingMaskIntoConstraints = false
stack.translatesAutoresizingMaskIntoConstraints = false
shadowView.addSubview(stack)
contentView.addSubview(shadowView)
let mg = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
shadowView.topAnchor.constraint(equalTo: mg.topAnchor),
shadowView.leadingAnchor.constraint(equalTo: mg.leadingAnchor),
shadowView.trailingAnchor.constraint(equalTo: mg.trailingAnchor),
shadowView.bottomAnchor.constraint(equalTo: mg.bottomAnchor),
stack.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: 12.0),
stack.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: 12.0),
stack.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -12.0),
stack.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -12.0),
])
contentView.clipsToBounds = false
self.clipsToBounds = false
self.backgroundColor = .clear
selectionStyle = .none
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
shadowView.backgroundColor = selected ? .systemBlue : .white
topLabel.textColor = selected ? .white : .black
bottomLabel.textColor = selected ? .white : .black
}
}
class ShadowTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.separatorStyle = .none
tableView.register(ShadowCell.self, forCellReuseIdentifier: "shadowCell")
}
func updateLayout() -> Void {
for c in tableView.visibleCells {
tableView.bringSubviewToFront(c)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateLayout()
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateLayout()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "shadowCell", for: indexPath) as! ShadowCell
c.topLabel.text = "Row: \(indexPath.row)"
var s = "Description for row \(indexPath.row)"
if indexPath.row % 3 == 1 {
s += "\nSecond Line"
}
if indexPath.row % 3 == 2 {
s += "\nSecond Line\nThirdLine"
}
c.bottomLabel.text = s
return c
}
}
Note: this is Example Code Only and should not be considered Production Ready.

Swift add items in scrollable list

right now this is all I have in my project:
In the end it should look and function pretty like this:
1. How do I add items into the ScrollView (in a 2 x X View)
2. How do I make the ScrollView actually be able to scroll (and refresh like in the 3 pictures below) or is this maybe solvable with just a list?
UPDATE
The final view should look like this:
The "MainWishList" cell and the "neue Liste erstellen" (= add new cell) should be there from the beginning. When the user clicks the "add-Cell" he should be able to choose a name and image for the list.
Part of the built-in functionality of a UICollectionView is automatic scrolling when you have more items (cells) than will fit in the frame. So there is no need to embed a collection view in a scroll view.
Here is a basic example. Everything is done via code (no #IBOutlet, #IBAction or prototype cells). Create a new UIViewController and assign its class to ExampleViewController as found below:
//
// ExampleViewController.swift
// CollectionAddItem
//
// Created by Don Mag on 10/22/19.
//
import UIKit
// simple cell with label
class ContentCell: UICollectionViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.backgroundColor = .yellow
contentView.addSubview(theLabel)
// constrain label to all 4 sides
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
theLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
theLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
theLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
}
}
// simple cell with button
class AddItemCell: UICollectionViewCell {
let btn: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("+", for: .normal)
v.setTitleColor(.systemBlue, for: .normal)
v.titleLabel?.font = UIFont.systemFont(ofSize: 40.0)
return v
}()
// this will be used as a "callback closure" in collection view controller
var tapCallback: (() -> ())?
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.backgroundColor = .green
contentView.addSubview(btn)
// constrain button to all 4 sides
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: contentView.topAnchor),
btn.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
btn.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
btn.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
#objc func didTap(_ sender: Any) {
// tell the collection view controller we got a button tap
tapCallback?()
}
}
class ExampleViewController: UIViewController, UICollectionViewDataSource {
let theCollectionView: UICollectionView = {
let v = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
v.contentInsetAdjustmentBehavior = .always
return v
}()
let columnLayout = FlowLayout(
itemSize: CGSize(width: 100, height: 100),
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)
// track collection view frame change
var colViewWidth: CGFloat = 0.0
// example data --- this will be filled with simple number strings
var theData: [String] = [String]()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
view.addSubview(theCollectionView)
// constrain collection view
// 100-pts from top
// 60-pts from bottom
// 40-pts from leading
// 40-pts from trailing
NSLayoutConstraint.activate([
theCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100.0),
theCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -60.0),
theCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
theCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
])
// register the two cell classes for reuse
theCollectionView.register(ContentCell.self, forCellWithReuseIdentifier: "ContentCell")
theCollectionView.register(AddItemCell.self, forCellWithReuseIdentifier: "AddItemCell")
// set collection view dataSource
theCollectionView.dataSource = self
// use custom flow layout
theCollectionView.collectionViewLayout = columnLayout
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only want to call this when collection view frame changes
// to set the item size
if theCollectionView.frame.width != colViewWidth {
let w = theCollectionView.frame.width / 2 - 15
columnLayout.itemSize = CGSize(width: w, height: w)
colViewWidth = theCollectionView.frame.width
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// return 1 more than our data array (the extra one will be the "add item" cell
return theData.count + 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// if item is less that data count, return a "Content" cell
if indexPath.item < theData.count {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ContentCell", for: indexPath) as! ContentCell
cell.theLabel.text = theData[indexPath.item]
return cell
}
// past the end of the data count, so return an "Add Item" cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AddItemCell", for: indexPath) as! AddItemCell
// set the closure
cell.tapCallback = {
// add item button was tapped, so append an item to the data array
self.theData.append("\(self.theData.count + 1)")
// reload the collection view
collectionView.reloadData()
collectionView.performBatchUpdates(nil, completion: {
(result) in
// scroll to make newly added row visible (if needed)
let i = collectionView.numberOfItems(inSection: 0) - 1
let idx = IndexPath(item: i, section: 0)
collectionView.scrollToItem(at: idx, at: .bottom, animated: true)
})
}
return cell
}
}
// custom FlowLayout class to left-align collection view cells
// found here: https://stackoverflow.com/a/49717759/6257435
class FlowLayout: UICollectionViewFlowLayout {
required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()
self.itemSize = itemSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }
// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Set the initial left inset
var leftInset = sectionInset.left
// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}
return layoutAttributes
}
}
When you run this, the data array will be empty, so the first thing you'll see is:
Each time you tap the "+" cell, a new item will be added to the data array (in this example, a numeric string), reloadData() will be called, and a new cell will appear.
Once we have enough items in our data array so they won't all fit in the collection view frame, the collection view will become scrollable:

UIScrollView not showing up in the view

I am implementing a UIScrollView in a CollectionViewCell. I have a custom view which the scroll view should display, hence I am performing the following program in the CollectionViewCell. I have created everything programmatically and below is my code :
struct ShotsCollections {
let title: String?
}
class ShotsMainView: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
containerScrollView.contentSize.width = frame.width * CGFloat(shotsData.count)
shotsData = [ShotsCollections.init(title: "squad"), ShotsCollections.init(title: "genral")]
var i = 0
for data in shotsData {
let customview = ShotsMediaView(frame: CGRect(x: containerScrollView.frame.width * CGFloat(i), y: 0, width: containerScrollView.frame.width, height: containerScrollView.frame.height))
containerScrollView.addSubview(customview)
i += 1
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var shotsData = [ShotsCollections]()
var containerScrollView: UIScrollView = {
let instance = UIScrollView()
instance.isScrollEnabled = true
instance.bounces = true
instance.backgroundColor = blueColor
return instance
}()
private func setupViews() { //These are constraints by using TinyConstraints
addSubview(containerScrollView)
containerScrollView.topToSuperview()
containerScrollView.bottomToSuperview()
containerScrollView.rightToSuperview()
containerScrollView.leftToSuperview()
}
}
Now the issue is, while the scrollview is displayed, the content in it is not. I on printing the contentSize and frame of the scrollview, it displays 0. But if I check the Debug View Hierarchy, scrollview containes 2 views with specific frames.
I am not sure whats going wrongs. Any help is appreciated.
When you are adding customView in your containerScrollView, you are not setting up the constraints between customView and containerScrollView.
Add those constraints and you will be able to see your customViews given that your customView has some height. Also, when you add more view, you would need to remove the bottom constraint of the last added view and create a bottom constraint to the containerScrollView with the latest added view.
I created a sample app for your use case. I am pasting the code and the resultant screen shot below. Hope this is the functionality you are looking for. I suggest you paste this in a new project and tweak the code until you are satisfied. I have added comments to make it clear.
ViewController
import UIKit
class ViewController: UIViewController {
// Initialize dummy data array with numbers 0 to 9
var data: [Int] = Array(0..<10)
override func loadView() {
super.loadView()
// Add collection view programmatically
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(ShotsMainView.self, forCellWithReuseIdentifier: ShotsMainView.identifier)
self.view.addSubview(collectionView)
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: collectionView.topAnchor),
self.view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
self.view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
self.view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
])
collectionView.delegate = self
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = UIColor.white
self.view.addSubview(collectionView)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = UIColor.white
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ShotsMainView.identifier, for: indexPath) as! ShotsMainView
return cell
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// The cell dimensions are set from here
return CGSize(width: collectionView.frame.size.width, height: 100.0)
}
}
ShotsMainView
This is the collection view cell
import UIKit
class ShotsMainView: UICollectionViewCell {
static var identifier = "Cell"
weak var textLabel: UILabel!
override init(frame: CGRect) {
// Initialize with zero frame
super.init(frame: frame)
// Add the scrollview and the corresponding constraints
let containerScrollView = UIScrollView(frame: .zero)
containerScrollView.isScrollEnabled = true
containerScrollView.bounces = true
containerScrollView.backgroundColor = UIColor.blue
containerScrollView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(containerScrollView)
NSLayoutConstraint.activate([
self.topAnchor.constraint(equalTo: containerScrollView.topAnchor),
self.bottomAnchor.constraint(equalTo: containerScrollView.bottomAnchor),
self.leadingAnchor.constraint(equalTo: containerScrollView.leadingAnchor),
self.trailingAnchor.constraint(equalTo: containerScrollView.trailingAnchor)
])
// Add the stack view that will hold the individual items that
// in each row that need to be scrolled horrizontally
let stackView = UIStackView(frame: .zero)
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
containerScrollView.addSubview(stackView)
stackView.backgroundColor = UIColor.magenta
NSLayoutConstraint.activate([
containerScrollView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
containerScrollView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
containerScrollView.topAnchor.constraint(equalTo: stackView.topAnchor),
containerScrollView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor)
])
// Add individual items (Labels in this case).
for i in 0..<10 {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(label)
label.text = "\(i)"
label.font = UIFont(name: "System", size: 20.0)
label.textColor = UIColor.white
label.backgroundColor = UIColor.purple
label.layer.masksToBounds = false
label.layer.borderColor = UIColor.white.cgColor
label.layer.borderWidth = 1.0
label.textAlignment = .center
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1.0, constant: 0.0),
label.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.2, constant: 0.0)
])
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Screenshot

Make collection view height dynamic depending on content

I have a collection view that contains a cell with varying width (it has a label inside it):
public class TagView: UIView {
let textLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 15)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupLabel()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
backgroundColor = #colorLiteral(red: 0.03529411765, green: 0.6862745098, blue: 0.003921568627, alpha: 1)
backgroundColor = backgroundColor
layer.cornerRadius = 3
layer.masksToBounds = true
}
private func setupLabel() {
addSubview(textLabel)
textLabel.fillToSuperview(constant: 3)
}
}
How do I make the collection view's height dynamic? The problem is that at init time I don't know what frame I should give to the collection view, so I just give zero:
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
How do I make the collection view height dynamic?
I have also looked into the sizeForItem method:
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let view = TagView()
let data = tags[indexPath.row]
view.textLabel.text = data
view.layoutSubviews()
return view.frame.size
}
but I think this returns a size of zero width and heigth.
First here set an assumption height , but the width should be known
let collectionView = UICollectionView(frame:CGRect(x:0,y:20,width:self.view.frame.width,height:300), collectionViewLayout: layout)
Then in sizeForItemAt
let fixedWidth = (collectionView.frame.width - 40 ) / 5 // say here you need 5 items / row
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 15)
let si = label.sizeThatFits(CGSize(width:fixedWidth, height: CGFloat.greatestFiniteMagnitude))
// si.height is the needed height with 6 padding from top and bottom according to your constant in tagView
return CGSize(width:fixedWidth + 6.0 ,height:si.height)
For a total height , create a function from above and call it with all your items then add the heights and set them to collectionView's height

How can I change my UICollectionView's Flow Layout to a vertical List with Horizontal Scrolling

Basically what I am trying to create is a table with three cells stacked on top of one another. But, if there are more than three cells, I want to be able to swipe left on the Collection View to show more cells. Here is a picture to illustrate.
Right now I have the cells arranged in a list but I cannot seem to change the scroll direction for some reason. - They still scroll vertically
Here is my current code for the Flow Layout:
Note: I'm not going to include the Collection View code that is in my view controller as I do not think it is relevant.
import Foundation
import UIKit
class HorizontalListCollectionViewFlowLayout: UICollectionViewFlowLayout {
let itemHeight: CGFloat = 35
func itemWidth() -> CGFloat {
return collectionView!.frame.width
}
override var itemSize: CGSize {
set {
self.itemSize = CGSize(width: itemWidth(), height: itemHeight)
}
get {
return CGSize(width: itemWidth(), height: itemHeight)
}
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return collectionView!.contentOffset
}
override var scrollDirection: UICollectionViewScrollDirection {
set {
self.scrollDirection = .horizontal
} get {
return self.scrollDirection
}
}
}
If you have your cells sized correctly, Horizontal Flow Layout will do exactly what you want... fill down and across.
Here is a simple example (just set a view controller to this class - no IBOutlets needed):
//
// ThreeRowCViewViewController.swift
//
// Created by Don Mag on 6/20/17.
//
import UIKit
private let reuseIdentifier = "LabelItemCell"
class LabelItemCell: UICollectionViewCell {
// simple CollectionViewCell with a label
#IBOutlet weak var theLabel: UILabel!
let testLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 14)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addViews()
}
func addViews(){
addSubview(testLabel)
testLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8.0).isActive = true
testLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0.0).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ThreeRowCViewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
// 3 gray colors for the rows
let cellColors = [
UIColor.init(white: 0.9, alpha: 1.0),
UIColor.init(white: 0.8, alpha: 1.0),
UIColor.init(white: 0.7, alpha: 1.0)
]
var theCodeCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
// height we'll use for the rows
let rowHeight = 30
// just picked a random width of 240
let rowWidth = 240
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
// horizontal collection view direction
layout.scrollDirection = .horizontal
// each cell will be the width of the collection view and our pre-defined height
layout.itemSize = CGSize(width: rowWidth - 1, height: rowHeight)
// no item spacing
layout.minimumInteritemSpacing = 0.0
// 1-pt line spacing so we have a visual "edge" (with horizontal layout, the "lines" are vertical blocks of cells
layout.minimumLineSpacing = 1.0
theCodeCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
theCodeCollectionView.dataSource = self
theCodeCollectionView.delegate = self
theCodeCollectionView.register(LabelItemCell.self, forCellWithReuseIdentifier: reuseIdentifier)
theCodeCollectionView.showsVerticalScrollIndicator = false
// set background to orange, just to make it obvious
theCodeCollectionView.backgroundColor = .orange
theCodeCollectionView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(theCodeCollectionView)
// set collection view width x height to rowWidth x (rowHeight * 3)
theCodeCollectionView.widthAnchor.constraint(equalToConstant: CGFloat(rowWidth)).isActive = true
theCodeCollectionView.heightAnchor.constraint(equalToConstant: CGFloat(rowHeight * 3)).isActive = true
// center the collection view
theCodeCollectionView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0).isActive = true
theCodeCollectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0).isActive = true
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 12
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! LabelItemCell
cell.backgroundColor = cellColors[indexPath.row % 3]
cell.testLabel.text = "\(indexPath)"
return cell
}
}
I'll leave the "enable paging" part up to you :)

Resources