In https://stackoverflow.com/a/51231881/72437 , it show how to achieve a full width, dynamic height in vertical UICollectionView
's cell, by using UICollectionViewCompositionalLayout
We would like to achieve the same on a horizontal UICollectionView
, with requirements
Here's how our solution looks like
class MenuTabsView: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
lazy var collView: UICollectionView = {
let itemSize = NSCollectionLayoutSize(
widthDimension: NSCollectionLayoutDimension.estimated(44),
heightDimension: NSCollectionLayoutDimension.fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(
layoutSize: itemSize
)
item.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 0,
bottom: 0,
trailing: 1
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: itemSize,
subitem: item,
count: 1
)
let section = NSCollectionLayoutSection(group: group)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal
let layout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)
let cv = UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout)
cv.showsHorizontalScrollIndicator = false
cv.backgroundColor = .white
cv.delegate = self
cv.dataSource = self
return cv
}()
We expect by using widthDimension: NSCollectionLayoutDimension.estimated(44)
, that's the key to make the cell width grow dynamically. However, that doesn't work as expected. It looks like
May I know, how can we solve this problem by using UICollectionViewCompositionalLayout
? The complete workable project is located at https://github.com/yccheok/PageViewControllerWithTabs/tree/UICollectionViewCompositionalLayout
p/s
We want to avoid from using UICollectionViewDelegateFlowLayout method collectionView(_:layout:sizeForItemAt:)
. As, our cell can grow complex, and it will contain other views besides UILabel
. Having to calculate the content size manually, will make the solution inflexible and error prone.
I have created what you need. Follow the following steps:
Once this is done you need to create your layout. I had the following function (I have height 50 just change to 44 in your case):
func layoutConfig() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { (sectionNumber, env) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(44), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(44), heightDimension: .absolute(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
return section
}
}
Then in your viewDidLoad() function before you call the delegate or dataSource methods you need to call the following (assuming you keep the same function name):
collectionView.collectionViewLayout = layoutConfig()
You should end up with the following. It scrolls horizontally:
Here is the source code to the example:
You seem to be asking the same question over and over?
You may find it helpful if you ask your complete question first.
You say "our cell content can grow complex" but you don't provide any information about what "complex" might be.
Here's an example that might get you headed in the right direction.
First, the output... Each "tab" has an Image View, a Label and a Button, or a combination of elements as follows:
// 1st tab is Image + Label + Button
// 2nd tab is Label + Button
// 3rd tab is Image + Label
// 4th tab is Image + Button
// 5th tab is Label Only
// 6th tab is Button Only
// 7th tab is Image Only
With different color tabs:
With the same color tabs, except for the "active" tab:
With background colors on the tab elements to see the frames:
I used this as the struct for the tab info:
struct TabInfo {
var name: String? = ""
var color: Int = 0
var imageName: String? = ""
var buttonTitle: String? = ""
}
and I used this from your GitHub repo:
class Utils {
static func intToUIColor(argbValue: Int) -> UIColor {
// & binary AND operator to zero out other color values
// >> bitwise right shift operator
// Divide by 0xFF because UIColor takes CGFloats between 0.0 and 1.0
let red = CGFloat((argbValue & 0xFF0000) >> 16) / 0xFF
let green = CGFloat((argbValue & 0x00FF00) >> 8) / 0xFF
let blue = CGFloat(argbValue & 0x0000FF) / 0xFF
let alpha = CGFloat((argbValue & 0xFF000000) >> 24) / 0xFF
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
Sample code for the view controller:
class TabsTestViewController: UIViewController {
// 1st tab is Image + Label + Button
// 2nd tab is Label + Button
// 3rd tab is Image + Label
// 4th tab is Image + Button
// 5th tab is Label Only
// 6th tab is Button Only
// 7th tab is Image Only
var tabs = [
TabInfo(name: "All", color: 0xff5481e6, imageName: "swiftBlue64x64", buttonTitle: "One"),
TabInfo(name: "Calendar", color: 0xff7cb342, imageName: nil, buttonTitle: "Two"),
TabInfo(name: "Home", color: 0xffe53935, imageName: "swiftBlue64x64", buttonTitle: nil),
TabInfo(name: nil, color: 0xfffb8c00, imageName: "swiftBlue64x64", buttonTitle: "Work"),
TabInfo(name: "Label Only", color: 0xffe00000, imageName: nil, buttonTitle: nil),
TabInfo(name: nil, color: 0xff008000, imageName: nil, buttonTitle: "Button Only"),
TabInfo(name: nil, color: 0xff000080, imageName: "swiftBlue64x64", buttonTitle: nil),
]
let menuTabsView: MenuTabsView = {
let v = MenuTabsView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let otherView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(menuTabsView)
view.addSubview(otherView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain menuTabsView Top / Leading / Trailing
menuTabsView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
menuTabsView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
menuTabsView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// constrain otherView Leading / Trailing / Bottom
otherView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
otherView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
otherView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constrain otherView Top to menuTabsView Bottom
otherView.topAnchor.constraint(equalTo: menuTabsView.bottomAnchor, constant: 0.0),
])
// un-comment to set all tab colors to green - 0xff7cb342
// except first tab
//for i in 1..<tabs.count {
// tabs[i].color = 0xff7cb342
//}
menuTabsView.dataArray = tabs
// set background color of "bottom bar" to first tab's background color
guard let tab = tabs.first else {
return
}
menuTabsView.bottomBar.backgroundColor = Utils.intToUIColor(argbValue: tab.color)
}
}
Sample code for the MenuTabsView
:
class MenuTabsView: UIView {
var tabsHeight: CGFloat = 44
var dataArray: [TabInfo] = [] {
didSet{
self.collView.reloadData()
}
}
lazy var collView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 1
// needed to prevent last cell being clipped
layout.minimumInteritemSpacing = 1
layout.estimatedItemSize = CGSize(width: 100, height: self.tabsHeight)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
return cv
}()
let bottomBar: UIView = {
let v = UIView()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
collView.translatesAutoresizingMaskIntoConstraints = false
bottomBar.translatesAutoresizingMaskIntoConstraints = false
addSubview(collView)
addSubview(bottomBar)
NSLayoutConstraint.activate([
// collection view constrained Top / Leading / Trailing
collView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
collView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
collView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// collection view Height constrained to 44
collView.heightAnchor.constraint(equalToConstant: tabsHeight),
// "bottom bar" constrained Leading / Trailing / Bottom
bottomBar.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
bottomBar.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
bottomBar.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
// "bottom bar" Height constrained to 4-pts
bottomBar.heightAnchor.constraint(equalToConstant: 4.0),
// collection view Bottom constrained to "bottom bar" Top
collView.bottomAnchor.constraint(equalTo: bottomBar.topAnchor),
])
collView.register(MyStackCell.self, forCellWithReuseIdentifier: "cell")
collView.dataSource = self
collView.delegate = self
collView.backgroundColor = .clear
backgroundColor = .white
}
}
extension MenuTabsView: UICollectionViewDelegate, UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyStackCell
let t = dataArray[indexPath.item]
cell.configure(with: t.name, imageName: t.imageName, buttonTitle: t.buttonTitle, bkgColor: t.color)
//cell.configure(with: t.name, or: nil, or: "My Button")
return cell
}
}
and finally, the collection view cell:
class MyStackCell: UICollectionViewCell {
// can be set by caller to change default cell height
public var stackHeight: CGFloat = 36.0
private var stackHeightConstraint: NSLayoutConstraint!
private let label: UILabel = {
let v = UILabel()
v.textAlignment = .center
return v
}()
private let imageView: UIImageView = {
let v = UIImageView()
return v
}()
private let button: UIButton = {
let v = UIButton()
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
return v
}()
private let stack: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.alignment = .center
v.spacing = 8
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(stack)
// stack views in cells get cranky
// so we set priorities on desired element constraints to 999 (1 less than required)
// to avoid console warnings
var c = imageView.heightAnchor.constraint(equalToConstant: 32.0)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// image view has 1:1 ratio
c = imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// minimum width for label if desired
c = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// minimum width for button if desired
c = button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// height for stack view
stackHeightConstraint = stack.heightAnchor.constraint(equalToConstant: stackHeight)
stackHeightConstraint.priority = UILayoutPriority(rawValue: 999)
stackHeightConstraint.isActive = true
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4.0),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0),
])
stack.addArrangedSubview(imageView)
stack.addArrangedSubview(label)
stack.addArrangedSubview(button)
// during development, so we can see the frames
// delete or comment-out when satisfied with layout
//imageView.backgroundColor = .yellow
//label.backgroundColor = .green
//button.backgroundColor = .blue
}
func customTabHeight(_ h: CGFloat) -> Void {
stackHeightConstraint.constant = h
}
func configure(with name: String?, imageName: String?, buttonTitle: String?, bkgColor: Int?) -> Void {
// set and show elements
// or hide if nil
if let s = imageName, s.count > 0 {
if let img = UIImage(named: s) {
imageView.image = img
imageView.isHidden = false
}
} else {
imageView.isHidden = true
}
if let s = name, s.count > 0 {
label.text = s
label.isHidden = false
} else {
label.isHidden = true
}
if let s = buttonTitle, s.count > 0 {
button.setTitle(s, for: [])
button.isHidden = false
} else {
button.isHidden = true
}
if let c = bkgColor {
backgroundColor = Utils.intToUIColor(argbValue: c)
}
}
override func layoutSubviews() {
super.layoutSubviews()
// set the mask in layoutSubviews
let maskPath = UIBezierPath(roundedRect: bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 12.0, height: 12.0))
let shape = CAShapeLayer()
shape.path = maskPath.cgPath
layer.mask = shape
}
}
Note that this is Example Code Only!!!
It is not intended to be production-ready --- it's just to help you on your way.
Implement UICollectionViewDelegateFlowLayout
method collectionView(_:layout:sizeForItemAt:)
method to return the cell size according to your text.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let text = "This is your text"
let size = text.size(withAttributes:[.font: UIFont.systemFont(ofSize:18.0)])
return CGSize(width: size.width + 10.0, height: 44.0)
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.