简体   繁体   English

尝试通过使用 UICollectionViewCompositionalLayout 实现固定高度、动态宽度(包裹内容)水平 UICollectionView 的单元格

[英]Attempt to achieve fixed height, dynamic width (wrap content) horizontal UICollectionView's cell by using UICollectionViewCompositionalLayout

In https://stackoverflow.com/a/51231881/72437 , it show how to achieve a full width, dynamic height in vertical UICollectionView 's cell, by using UICollectionViewCompositionalLayouthttps://stackoverflow.com/a/51231881/72437 中,它展示了如何通过使用UICollectionViewCompositionalLayout在垂直UICollectionView的单元格中实现全宽、动态高度

We would like to achieve the same on a horizontal UICollectionView , with requirements我们希望在水平UICollectionView上实现相同的要求

  1. Fixed height 44固定高度 44
  2. Minimum width 44最小宽度 44
  3. 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.我们期望通过使用widthDimension: NSCollectionLayoutDimension.estimated(44) ,这是使单元格宽度动态增长的关键。 However, that doesn't work as expected.但是,这并不像预期的那样工作。 It looks like看起来像

在此处输入图片说明

May I know, how can we solve this problem by using UICollectionViewCompositionalLayout ?我可以知道,我们如何通过使用UICollectionViewCompositionalLayout来解决这个问题? The complete workable project is located at https://github.com/yccheok/PageViewControllerWithTabs/tree/UICollectionViewCompositionalLayout完整可行的项目位于https://github.com/yccheok/PageViewControllerWithTabs/tree/UICollectionViewCompositionalLayout


p/s每秒

We want to avoid from using UICollectionViewDelegateFlowLayout method collectionView(_:layout:sizeForItemAt:) .我们想避免使用UICollectionViewDelegateFlowLayout method collectionView(_:layout:sizeForItemAt:) As, our cell can grow complex, and it will contain other views besides UILabel .因为,我们的单元格可以变得复杂,它会包含除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)向标签添加尾随和前导约束(我的示例有 10 和 10)(重要)
  • 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 >=.将标签的宽度设置为 >= 44。您可以通过检查约束并从等于 >= 更改来做到这一点。

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):我有以下功能(在你的情况下,我的身高 50 只是更改为 44):

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):然后在您的 viewDidLoad() 函数中,在您调用委托或 dataSource 方法之前,您需要调用以下内容(假设您保持相同的函数名称):

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 https://github.com/williamfinn/Horizo​​ntalCollectionExample

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:我从你的 GitHub 存储库中使用了这个:

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 : 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.实现UICollectionViewDelegateFlowLayout方法collectionView(_:layout:sizeForItemAt:)方法以根据您的文本返回单元格大小。

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)
    
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM