Reputation: 1114
I am building a list that contains hashtags using the UICompositionalLayout
, I have managed to achieve dynamic-sized cells in my sections using the estimated
metrics. Which looks like this
However, I want to remove the empty spaces between the tags and have a continuous stream spanning over two rows. Here is how I am building my layout section
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(120), heightDimension: .absolute(44))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.43), heightDimension: .fractionalWidth(0.35))
let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: layoutGroupSize, subitems: [layoutItem])
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.orthogonalScrollingBehavior = .continuous
return layoutSection
Thanks a lot for your help!
Upvotes: 2
Views: 981
Reputation: 59
I think it is related to the cell.xib constraints (trailing ). check that and reply back to me.
good luck.
Upvotes: -1
Reputation: 77672
UI components are great -- until they're not.
Something such as a UICollectionView
is designed to layout and manage a large number of items (cells).
When the layout doesn't match the desired layout, though, it can become a fight against the built-in design of the component. That's the case here.
As you know, by default, a horizontal scrolling collection view aligns the cells like this:
Trying to get the cells left-aligned with equal spacing from row-to-row is not easy, whether using Flow or Compositional layout.
You might want to take a different approach -- here's one idea...
We'll start with a simple "self-selectable Tag View":
class MyTagView: UIView {
public var selectChanged: ((MyTagView) ->())?
public var text: String = "" {
didSet {
theLabel.text = text
}
}
public var normalForegroundColor: UIColor = UIColor(red: 78.0 / 255.0, green: 164.0 / 255.0, blue: 145.0 / 255.0, alpha: 1.0)
public var normalBackgroundColor: UIColor = .white
public var highlightForegroundColor: UIColor = .lightGray
public var highlightBackgroundColor: UIColor = UIColor(red: 78.0 / 255.0, green: 164.0 / 255.0, blue: 145.0 / 255.0, alpha: 1.0)
private let theLabel: UILabel = {
let v = UILabel()
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 {
backgroundColor = normalBackgroundColor
theLabel.textColor = normalForegroundColor
theLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(theLabel)
let g = self
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0),
theLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 40.0),
])
layer.cornerRadius = 8
layer.borderWidth = 1
layer.borderColor = normalForegroundColor.cgColor
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
addGestureRecognizer(t)
}
@objc func gotTap(_ sender: Any?) {
selected.toggle()
selectChanged?(self)
}
var selected: Bool = false {
didSet {
if selected {
backgroundColor = normalForegroundColor
theLabel.textColor = normalBackgroundColor
layer.borderColor = normalBackgroundColor.cgColor
} else {
backgroundColor = normalBackgroundColor
theLabel.textColor = normalForegroundColor
layer.borderColor = normalForegroundColor.cgColor
}
}
}
}
which gives us this:
It also has a closure so we can inform its parent / controller when the selected state has changed.
Next we'll create a "Tags View" that will use a vertical stack view with horizontal "row" stack views to layout the tags:
class MyTagsView: UIView {
public var delegate: MyTagsViewDelegate?
public var tagViews: [MyTagView] = []
var theTags: [String] = [] {
didSet {
// clear existing (in case we're setting the tags multiple times)
vStack.arrangedSubviews.forEach { v in
v.removeFromSuperview()
}
tagViews = []
var totalWidth: CGFloat = 0
// create individual tag views and get the total width
theTags.forEach { str in
let t = MyTagView()
t.text = "#" + str
let sz = t.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
totalWidth += sz.width
tagViews.append(t)
}
let rowWidth: CGFloat = totalWidth / CGFloat(numRows)
var iTag: Int = 0
while iTag < tagViews.count {
// create a new "row" horizontal stack view
let v = UIStackView()
v.spacing = 8
vStack.addArrangedSubview(v)
var cw: CGFloat = 0
// add tag views
while cw < rowWidth, iTag < tagViews.count {
let t = tagViews[iTag]
let sz = t.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
v.addArrangedSubview(t)
cw += sz.width
iTag += 1
}
}
// set closure so we can track selections
tagViews.forEach { tv in
tv.selectChanged = { [weak self] theTagView in
guard let self = self,
let idx = self.tagViews.firstIndex(of: theTagView)
else { return }
if theTagView.selected {
self.delegate?.myTagsView(self, didSelectItemAt: idx)
} else {
self.delegate?.myTagsView(self, didDeSelectItemAt: idx)
}
}
}
}
}
// number of rows of tagViews
public var numRows: Int = 2
// vertical stack view to hold the rows
private let vStack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
v.alignment = .leading
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(vStack)
addSubview(scrollView)
let g = self
let cg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
vStack.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
vStack.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
vStack.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
vStack.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
scrollView.heightAnchor.constraint(equalTo: vStack.heightAnchor, constant: 16.0),
])
}
}
It will use a protocol to allow our controller to respond to selection / deselection:
// protocol so we can tell the controller about selections
protocol MyTagsViewDelegate {
func myTagsView(_ myTagsView: MyTagsView, didSelectItemAt index: Int)
func myTagsView(_ myTagsView: MyTagsView, didDeSelectItemAt index: Int)
}
Our example controller will create 3 instances of MyTagsView
, with 2, 3 and 4 "rows":
class MyTagsVC: UIViewController, MyTagsViewDelegate {
let theTags: [String] = [
"streetphotograhy", "portraits", "wild", "india", "landscape", "portrait",
"These", "Are", "Tags", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L",
]
let stack = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 16.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// let the view determine its own height
])
// let's create 3 tag views
// with 2, 3, 4 rows
for i in 2...4 {
let tv = MyTagsView()
tv.backgroundColor = .white
tv.numRows = i
tv.theTags = self.theTags
tv.delegate = self
stack.addArrangedSubview(tv)
}
}
func myTagsView(_ myTagsView: MyTagsView, didSelectItemAt index: Int) {
guard let tvIDX = stack.arrangedSubviews.firstIndex(of: myTagsView) else { return }
print("Selected: \(index) / \"\(theTags[index])\" in tags view \(tvIDX)")
}
func myTagsView(_ myTagsView: MyTagsView, didDeSelectItemAt index: Int) {
guard let tvIDX = stack.arrangedSubviews.firstIndex(of: myTagsView) else { return }
print("Deselected: \(index) / \"\(theTags[index])\" in tags view \(tvIDX)")
}
}
and it looks like this:
This is just an idea of one other approach ... and is just starter code to play with to see if it would be a workable alternative.
Upvotes: 0