I have a horizontal collection view embedded in the cell of a vertical collection view (a carousel in a feed unit). For some strange reason, the inner scroll view is not receiving scroll view delegate events like scrollViewDidScroll. It does scroll, I just don't get any notifications about it.
The outer scrollview does, however, receive these events. I suspected the gesture recognizers might be interfering with eachother but I'm not sure. Do you have any idea what might be going wrong here or how I could get the inner collection view to behave normally?
EDIT: I determined that the source of the bug is actually a bug / undefined behavior with compositional layouts with orthogonal scrolling behavior:
https://developer.apple.com/forums/thread/127825
Basically they internally create additional horizontal collection views whose delegate events are not propagated upwards. That said, the approach in the accepted answer does work since it's not using this behavior.
Quick testing -- "horizontal collection view embedded in the cell of a vertical collection view" -- no trouble getting scrollViewDidScroll events for both.
Here's a quick example:
// simple collection view cell with centered label
class HorizItemCell: UICollectionViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
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() {
contentView.addSubview(theLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 12.0),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -12.0),
theLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
func fillData(_ str: String, bk: UIColor) {
theLabel.text = str
contentView.backgroundColor = bk
}
}
// collection view cell with horizontal scrolling collection view
class VertItemCell: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegate {
var cv: UICollectionView!
var nItems: Int = 0
var bkColor: UIColor = .white
var vertRow: Int = 0
let theLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 14.0, weight: .light)
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() {
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .horizontal
fl.estimatedItemSize = CGSize(width: 120, height: 36)
cv = UICollectionView(frame: .zero, collectionViewLayout: fl)
cv.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(theLabel)
contentView.addSubview(cv)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
cv.topAnchor.constraint(equalTo: theLabel.bottomAnchor, constant: 4.0),
cv.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0),
cv.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0),
cv.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0),
])
cv.register(HorizItemCell.self, forCellWithReuseIdentifier: "hc")
cv.dataSource = self
cv.delegate = self
contentView.layer.borderWidth = 2
contentView.layer.borderColor = UIColor.lightGray.cgColor
contentView.layer.cornerRadius = 12
}
func fillData(rowNum: Int, numItems: Int, bk: UIColor) {
nItems = numItems
bkColor = bk
vertRow = rowNum
theLabel.text = "Vertical Row: \(vertRow)"
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return nItems
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "hc", for: indexPath) as! HorizItemCell
c.fillData("\(indexPath.item)", bk: bkColor)
return c
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(type(of: self), "Horizontal Scrolling - Row:", vertRow, "contentOffset:", scrollView.contentOffset)
}
}
class TestingVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var cv: UICollectionView!
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.cyan, .magenta, .yellow,
.red, .green, .blue,
.orange, .purple,
]
let counts: [Int] = [
10, 12, 19, 13, 8, 11, 17, 14, 16, 9, 15,
]
override func viewDidLoad() {
super.viewDidLoad()
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .vertical
cv = UICollectionView(frame: .zero, collectionViewLayout: fl)
cv.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(cv)
let safeG = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
cv.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
cv.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
cv.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
cv.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -20.0),
])
cv.register(VertItemCell.self, forCellWithReuseIdentifier: "vc")
cv.dataSource = self
cv.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let fl = cv.collectionViewLayout as? UICollectionViewFlowLayout {
fl.itemSize = CGSize(width: cv.frame.width, height: 80.0)
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 12
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "vc", for: indexPath) as! VertItemCell
c.fillData(rowNum: indexPath.row, numItems: counts[indexPath.item % counts.count], bk: colors[indexPath.item % colors.count])
return c
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(type(of: self), "Vertical Scrolling", scrollView.contentOffset)
}
}
It looks like this when running:
and this is some sample Debug Console output:
TestingVC Vertical Scrolling (0.0, 100.5)
TestingVC Vertical Scrolling (0.0, 107.5)
TestingVC Vertical Scrolling (0.0, 115.5)
VertItemCell Horizontal Scrolling - Row: 3 contentOffset: (314.5, 0.0)
VertItemCell Horizontal Scrolling - Row: 3 contentOffset: (315.0, 0.0)
VertItemCell Horizontal Scrolling - Row: 4 contentOffset: (1.0, 0.0)
VertItemCell Horizontal Scrolling - Row: 4 contentOffset: (22.5, 0.0)
VertItemCell Horizontal Scrolling - Row: 5 contentOffset: (308.0, 0.0)
VertItemCell Horizontal Scrolling - Row: 5 contentOffset: (307.5, 0.0)
VertItemCell Horizontal Scrolling - Row: 6 contentOffset: (12.5, 0.0)
VertItemCell Horizontal Scrolling - Row: 6 contentOffset: (32.5, 0.0)
Related
A common pattern in UI is to maximize the size of the view up to some point and after that fill the rest of its superview with the spaces.
When using AutoLayout, it can be achieved easily with width <= X constraint. But when using this with UICollectionView, the scroll area matches the size of UICollectionView, so the sides are unscrollable which is unwanted for me.
So, the only way I found to achieve the behavior is to use the proper layout inside the cells themselves. I consider this as a not very good design decision (especially when you have multiple cells). But are there any alternatives available?
We can accomplish this by subclassing UICollectionView and implementing hitTest(_:with:).
What we'll do is extend the "touch area" wider than the collection view itself:
class ExtendedCollectionView: UICollectionView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
// touch is inside self.bounds, so
// send it on to super (i.e. "normal" behavior)
// so we can select a cell on tap
return super.hitTest(point, with: event)
}
if self.bounds.insetBy(dx: -self.frame.origin.x, dy: 0).contains(point) {
// touch is outside self.bounds, but
// it IS inside bounds extended left and right, so
// capture the touch for self
return self
}
// touch was outside self.bounds (and outside our extended bounds), so
// send it on to super (i.e. "normal" behavior)
// so the rest of the view hierarchy (buttons, etc)
// can receive the gesture
return super.hitTest(point, with: event)
}
}
You can now use ExtendedCollectionView just as you would use UICollectionView, except you'll be able to scroll it starting from left or right, outside of its bounds.
Here's a complete example:
class ViewController: UIViewController {
var myData: [String] = []
// we'll use these colors for the cell backgrounds
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.systemPink, .systemYellow, .systemTeal,
]
// our "extended collection view"
var collectionView: ExtendedCollectionView!
let cellSize: CGSize = CGSize(width: 80, height: 100)
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Extended CollectionView"
// always respect the safe area
let g = view.safeAreaLayoutGuide
// let's add a "background" image view, sized to fit the view
// so we can easily see the reults
if let img = UIImage(named: "sampleBKG") {
let v = UIImageView()
v.image = img
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
view.sendSubviewToBack(v)
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
}
// fill myData array with 20 strings
// of different lengths to show this
// works with dynamic width cells
let strs: [String] = [
"Short",
"Bit Longer",
"Much Longer String",
]
for i in 0..<20 {
myData.append("C: \(i) \(strs[i % strs.count])")
}
// set the flow layout properties
let fl = UICollectionViewFlowLayout()
fl.estimatedItemSize = CGSize(width: 50, height: 100)
fl.scrollDirection = .horizontal
fl.minimumLineSpacing = 8
fl.minimumInteritemSpacing = 8
// create an instance of ExtendedCollectionView
collectionView = ExtendedCollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .clear
view.addSubview(collectionView)
// let's make the collection view
// 80% of the width of the view's safe area
let cvWidthPercent = 0.8
// let's add a label below our custom view
// the same percentage width, so we can
// easily see the layout
let v = UILabel()
v.backgroundColor = .green
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.text = "\(cvWidthPercent * 100)%"
view.addSubview(v)
NSLayoutConstraint.activate([
// let's put our collection view
// 80-pts from the top
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
// centered Horizontally
collectionView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// height equal to cell Height
collectionView.heightAnchor.constraint(equalToConstant: cellSize.height),
// 80% of the width of the safe area
collectionView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: cvWidthPercent),
// constrain label 8-pts below the collection view
v.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 8.0),
// centered Horizontally
v.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// same percentage width
v.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: cvWidthPercent),
])
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(MyDynamicCVCell.self, forCellWithReuseIdentifier: "cvCell")
}
}
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cvCell", for: indexPath) as! MyDynamicCVCell
cell.contentView.backgroundColor = colors[indexPath.item % colors.count]
cell.label.text = myData[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Did Select Cell At:", indexPath)
}
}
a simple dynamic-width cell
class MyDynamicCVCell: UICollectionViewCell {
let label: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
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(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
])
}
}
The result looks like this:
When you run it, you'll see that you can scroll horizontally, even if you start dragging from the left or right of the cells.
I'm trying to figure out is it possible for UICollectionView to calculate it's own height using autolayout? My custom cells are built on autolayout and the UICollectionViewFlowLayout.automaticSize property for itemSize seems to be working, but the size of UICollectionView itself should be set. I believe that this is normal behavior, since the collection can have bigger size than it's cells, but maybe it is possible to make height of the content view of UICollectionView to be equal to cell with some insets?
Here is the code for test UIViewController with UICollectionView
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var collectionView: UICollectionView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 100)
layout.itemSize = UICollectionViewFlowLayout.automaticSize
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
if let collectionView = collectionView {
collectionView.showsHorizontalScrollIndicator = false
collectionView.isPagingEnabled = true
collectionView.register(CollectionViewCell.self,
forCellWithReuseIdentifier: "CollectionViewCell")
collectionView.backgroundColor = .clear
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.dataSource = self
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor),
// I want to get rid of this constraint
collectionView.heightAnchor.constraint(equalToConstant: 200)
])
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell",
for: indexPath) as? CollectionViewCell else {
return UICollectionViewCell()
}
return cell
}
}
And for custom cell
final class CollectionViewCell: UICollectionViewCell {
var mainView = UIView()
var bigView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
var label: UILabel = {
let label = UILabel()
label.text = "Here is text"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var anotherLabel: UILabel = {
let label = UILabel()
label.text = "Here may be no text"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let inset: CGFloat = 16.0
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
mainView.backgroundColor = .white
addSubview(bigView)
addSubview(label)
addSubview(anotherLabel)
NSLayoutConstraint.activate([
mainView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width),
mainView.topAnchor.constraint(equalTo: topAnchor),
mainView.leftAnchor.constraint(equalTo: leftAnchor),
mainView.rightAnchor.constraint(equalTo: rightAnchor),
mainView.bottomAnchor.constraint(equalTo: bottomAnchor),
bigView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: inset),
bigView.leftAnchor.constraint(equalTo: mainView.leftAnchor, constant: inset),
bigView.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: -inset),
bigView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: -inset),
label.topAnchor.constraint(equalTo: bigView.topAnchor, constant: inset),
label.leftAnchor.constraint(equalTo: bigView.leftAnchor, constant: inset),
label.rightAnchor.constraint(equalTo: bigView.rightAnchor, constant: -inset),
anotherLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: inset),
anotherLabel.leftAnchor.constraint(equalTo: bigView.leftAnchor, constant: inset),
anotherLabel.rightAnchor.constraint(equalTo: bigView.rightAnchor, constant: -inset),
anotherLabel.bottomAnchor.constraint(equalTo: bigView.bottomAnchor, constant: -inset)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And maybe someone, who knows the answer also could tell whether it's possible to later put this paging collection into UITalbleViewCell and make size of the cell change with selected item in the collection? I provide screenshot of the collection I'm trying to make:
Item's without second text label will have smaller height, than items on the picture
Screenshot
Solution 1:
Get collection view content size from collectionViewLayout :
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Set your collection view height here.
let collectionHeight = self.yourCollectionView.collectionViewLayout.collectionViewContentSize.height
}
Solution 2:
Use KVO.
// Register observer
self.yourCollectionView.addObserver(self, forKeyPath: "contentSize", options: [.new, .old, .prior], context: nil)
#objc override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
// content size changed. Set your collection view height here.
let contentSize = change?[NSKeyValueChangeKey.newKey] as? CGSize {
print("contentSize:", contentSize)
}
}
// Remove register observer
deinit {
self.yourCollectionView.removeObserver(self, forKeyPath: "contentSize")
}
Solution 3:
Assign this class to UICollectionView. If you set height constraint from storyboard then set and enable remove at runtime.
class DynamicCollectionView: UICollectionView {
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size, self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
var size = contentSize
size.height += (contentInset.top + contentInset.bottom)
size.width += (contentInset.left + contentInset.right)
return size
}
}
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
Fixed height 44
Minimum width 44
The width should be dynamically enlarge, when the content grows
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:
Create a cell with a label inside.
Add trailing and leading constraints to the label (my example has 10 and 10) (important)
Set the label to align vertically.
Set the width of the label to be >= 44. You can do this by inspecting the constraint and changing from equal to >=.
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:
https://github.com/williamfinn/HorizontalCollectionExample
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)
}
how to implement UITextView inside UITableView inside UITableView ??
(1) If you type text in 'UITextView', the height of 'UITableViewCell2' is automatically increased.
(2) When the height of 'UITableViewCell2' is increased, the height of 'UITableViewCell' is automatically increased accordingly.
I have implemented the case of (1) but not (2).
How should I implement it?
Nesting table views may not be the ideal solution for this, but your UITableViewCell would need to estimate and measure the whole height of the embedded UITableView, and propagate changes up to the parent table.
You might want to give this a try...
It using a single-section table view. Each cell contains a UIStackView that arranges the (variable) UITextViews.
No #IBOutlet or #IBAction or prototype cell connections... just assign a standard UIViewController custom class to TableTextViewsViewController:
//
// TableTextViewsViewController.swift
// Created by Don Mag on 3/10/20.
//
import UIKit
class TextViewsCell: UITableViewCell, UITextViewDelegate {
let frameView: UIView = {
let v = UIView()
v.backgroundColor = .clear
v.layer.borderColor = UIColor(red: 0.0, green: 0.5, blue: 0.0, alpha: 1.0).cgColor
v.layer.borderWidth = 1
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let stackViewPadding: CGFloat = 8.0
var textViewCosure: ((Int, String)->())?
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 {
let g = contentView.layoutMarginsGuide
contentView.addSubview(frameView)
frameView.addSubview(stackView)
// bottom constraint needs to be less than 1000 (required) to avoid auot-layout warnings
let frameViewBottomConstrait = frameView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
frameViewBottomConstrait.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
frameView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
frameView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
frameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
frameViewBottomConstrait,
stackView.topAnchor.constraint(equalTo: frameView.topAnchor, constant: stackViewPadding),
stackView.leadingAnchor.constraint(equalTo: frameView.leadingAnchor, constant: stackViewPadding),
stackView.trailingAnchor.constraint(equalTo: frameView.trailingAnchor, constant: -stackViewPadding),
stackView.bottomAnchor.constraint(equalTo: frameView.bottomAnchor, constant: -stackViewPadding),
])
}
override func prepareForReuse() {
super.prepareForReuse()
stackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
}
func fillData(_ strings: [String]) -> Void {
strings.forEach {
let v = UITextView()
v.font = UIFont.systemFont(ofSize: 16.0)
v.isScrollEnabled = false
// hugging and compression resistance set to required for cell expansion animation
v.setContentHuggingPriority(.required, for: .vertical)
v.setContentCompressionResistancePriority(.required, for: .vertical)
v.text = $0
// frame the text view
v.layer.borderColor = UIColor.blue.cgColor
v.layer.borderWidth = 1
v.delegate = self
stackView.addArrangedSubview(v)
}
}
func textViewDidChange(_ textView: UITextView) {
guard let idx = stackView.arrangedSubviews.firstIndex(of: textView) else {
fatalError("Shouldn't happen, but couldn't find the textView index")
}
textViewCosure?(idx, textView.text)
}
}
class TableTextViewsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let topLabel: UILabel = {
let v = UILabel()
v.text = "Top Label"
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let tableView: UITableView = {
let v = UITableView()
v.layer.borderColor = UIColor.red.cgColor
v.layer.borderWidth = 1
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var myData: [[String]] = [[String]]()
var textViewsInRows: [Int] = [
3, 4, 2, 6, 1, 4, 3,
]
override func viewDidLoad() {
super.viewDidLoad()
// generate some dummy data
var i = 1
textViewsInRows.forEach {
var s: [String] = [String]()
for j in 1...$0 {
s.append("Table Row: \(i) TextView \(j)")
}
myData.append(s)
i += 1
}
view.addSubview(topLabel)
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
topLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
topLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
topLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
tableView.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 8.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
])
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
tableView.keyboardDismissMode = .onDrag
tableView.register(TextViewsCell.self, forCellReuseIdentifier: "TextViewsCell")
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextViewsCell", for: indexPath) as! TextViewsCell
cell.fillData(myData[indexPath.row])
cell.textViewCosure = { [weak self] idx, str in
// update our data
self?.myData[indexPath.row][idx] = str
// update table view cell height
self?.tableView.beginUpdates()
self?.tableView.endUpdates()
}
return cell
}
}
Result - red border is the tableView, green border is each cell's contentView, blue border is each textView:
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 :)