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

Posted

技术标签:

【中文标题】尝试通过使用 UICollectionViewCompositionalLayout 实现固定高度、动态宽度(包装内容)水平 UICollectionView 的单元格【英文标题】:Attempt to achieve fixed height, dynamic width (wrap content) horizontal UICollectionView's cell by using UICollectionViewCompositionalLayout 【发布时间】:2020-08-31 11:09:04 【问题描述】:

在https://***.com/a/51231881/72437 中,它展示了如何使用UICollectionViewCompositionalLayout 在垂直UICollectionView 的单元格中实现全宽动态高度

我们希望在水平的UICollectionView 上实现相同的要求

    固定高度 44 最小宽度 44 随着内容的增长,宽度应该动态放大

我们的解决方案如下所示

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

我们期望通过使用widthDimension: NSCollectionLayoutDimension.estimated(44),这是使单元格宽度动态增长的关键。但是,这并没有按预期工作。好像

请问,我们如何使用UICollectionViewCompositionalLayout 来解决这个问题?完整的可行项目位于https://github.com/yccheok/PageViewControllerWithTabs/tree/UICollectionViewCompositionalLayout


p/s

我们希望避免使用UICollectionViewDelegateFlowLayout method collectionView(_:layout:sizeForItemAt:)。因为,我们的单元格可以变得复杂,它会包含除UILabel 之外的其他视图。必须手动计算内容大小,会使解决方案不灵活且容易出错。

【问题讨论】:

【参考方案1】:

我已经创造了你需要的东西。请按照以下步骤操作:

创建一个内部带有标签的单元格。 为标签添加尾随和前导约束(我的示例有 10 和 10)(重要) 将标签设置为垂直对齐。 将标签的宽度设置为 >= 44。您可以通过检查约束并从等于 >= 更改来做到这一点。

完成此操作后,您需要创建布局。我有以下功能(在你的情况下,我的身高 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
    

然后在调用委托或数据源方法之前,在 viewDidLoad() 函数中,您需要调用以下内容(假设您保持相同的函数名称):

collectionView.collectionViewLayout = layoutConfig()

您应该得到以下结果。它水平滚动:

这里是示例的源代码:

https://github.com/williamfinn/HorizontalCollectionExample

【讨论】:

天哪!非常感谢!我想我注意到我的问题是调用NSCollectionLayoutGroup.horizontal( layoutSize:, subitem:, count: ) 而不是你的NSCollectionLayoutGroup.horizontal(layoutSize:, subitems: )。今晚我可以睡得更香,明天继续。非常感谢!【参考方案2】:

你似乎一遍又一遍地问同样的问题?

如果您先提出完整的问题,可能会有所帮助。

您说“我们的单元格内容可能会变得复杂”,但您没有提供任何有关“复杂”可能是什么的信息。

这里有一个例子可能会让你朝着正确的方向前进。

首先,输出...每个“选项卡”都有一个图像视图、一个标签和一个按钮,或者如下元素的组合:

// 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

使用不同颜色的标签:

使用相同颜色的标签,除了“活动”标签:

在标签元素上使用背景颜色来查看框架:

我用它作为标签信息的结构:

struct TabInfo 
    var name: String? = ""
    var color: Int = 0
    var imageName: String? = ""
    var buttonTitle: String? = ""

我在你的 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)
    

视图控制器的示例代码:

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)
        
    
    

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
    


最后是集合视图单元:

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
    


请注意,这只是示例代码!!!

它的目的不是为生产做好准备——它只是为了帮助你。

【讨论】:

感谢您的指点!我对您的解决方案非常感兴趣。我肯定想在明天之前花一天的时间进行彻底的尝试,因为你知道如何设法使用UICollectionViewDelegateFlowLayout 而不必实现collectionView(_:layout:sizeForItemAt:)。现在,我不知道它是如何通过浏览代码来工作的。我以某种方式接受@williamfinn 的回答,因为他在UICollectionViewCompositionalLayout 上指出了我的错误。还是谢谢。【参考方案3】:

实现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)
    

【讨论】:

嗨,我们宁愿不使用这种方式。原因是,我们的单元格内容会变得复杂。以上是一个简单的例子。一旦单元格内容变得复杂,就没有简单的方法来衡量内容大小。 @CheokYanCheng 即使内容变得复杂,单元格宽度也必须有最大限制吧? 对不起。这不是我想要传达的信息。简而言之,我们只想告诉系统“根据当前内容包装此单元格宽度”。我们希望避免执行手动计算来测量内容大小。我们已经经历过,我们知道这样做很痛苦,而且容易出错。我们需要一个可靠的解决方案,而 UICollectionViewCompositionalLayout 似乎是正确的方向。

以上是关于尝试通过使用 UICollectionViewCompositionalLayout 实现固定高度、动态宽度(包装内容)水平 UICollectionView 的单元格的主要内容,如果未能解决你的问题,请参考以下文章

尝试通过使用 jquery 的 DOM 访问来修改输入值

尝试通过 Spring Boot Rest 使用 Jackson 验证 JSON

尝试通过 python 套接字升级到 http2

尝试使用特定用户通过 ssh 登录时出现“写入失败:管道损坏”

尝试通过命令行连接到 MySQL,但改为使用 MariaDB

尝试使用 Edge 通过 HTTP 下载文件时隐藏 HTTPS 警告