【问题标题】:Wrap items in a horizontal UIStackView on multiple lines在多行的水平 UIStackView 中包装项目
【发布时间】:2020-06-20 06:07:46
【问题描述】:

我正在开发一个显示 UITableView 项目的 iOS 应用程序。 每行将显示具有不同宽度的标签列表(由​​标签的文本长度给出)。 我可以将所有标签放置在水平的UIStackView 中,但我希望它们包含多行而不是单个可滚动的行。基本上我对类似于 FlexBox 的 flex-wrap 属性的功能感兴趣。

我附上了一张图片供参考。

任何想法如何实现这一目标?

【问题讨论】:

  • 为什么不使用UICollectionView?这确实是它存在的原因。
  • 我已经考虑过了,但我认为这是最后的解决方案。我希望单元格始终可见,因此 CollectionView 的重用机制将被实现但不被使用。还有一个问题就是总是调整 CollectionView 的高度约束,这样就没有垂直滚动了。
  • 如果没有更清洁的解决方案,我会试试这个。
  • 如果在这个用例中使用 UICollectionView,像使用'inset' flowlayout delegate 让单元格居中这样简单的事情会变成一场噩梦吗? @dfd

标签: ios autolayout uistackview


【解决方案1】:

有许多不同的方法可以解决这个问题。

一种方法 - 使用堆栈视图:

  • 将标签添加到“容器”视图中
  • x = 0y = 0 开头
  • 遍历标签,计算新的x 值(标签宽度 + 所需的标签间距)
  • 如果新的x 超出容器边缘,请重置x = 0 并将所需高度添加到y 以“移至下一行”
  • 标签布局后,设置容器视图的高度

这是一个简单的例子:

class TagLabelsViewController: UIViewController {
    
    let containerView: UIView = {
        let v = UIView()
        return v
    }()
    
    let tagNames: [String] = [
        "First Tag",
        "Second",
        "Third Tag",
        "Fourth",
        "The Fifth Tag",
        "Sixth",
        "Seventh",
        "Tag Eight",
        "Here are some Letter Tags",
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
        "Nine",
        "Ten",
        "Eleven",
        "Tag Twelve",
        "Tag 13",
        "Fourteen",
        "Fifteen",
        "Sixteen",
        "Seventeen",
        "Eightteen",
        "Nineteen",
        "Last Tag",
    ]
    
    var tagLabels = [UILabel]()
    
    let tagHeight:CGFloat = 30
    let tagPadding: CGFloat = 16
    let tagSpacingX: CGFloat = 8
    let tagSpacingY: CGFloat = 8
    
    // container view height will be modified when laying out subviews
    var containerHeightConstraint: NSLayoutConstraint = NSLayoutConstraint()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add the container view
        view.addSubview(containerView)
        
        // give it a background color so we can see it
        containerView.backgroundColor = .yellow
        
        // use autolayout
        containerView.translatesAutoresizingMaskIntoConstraints = false
        
        // initialize height constraint - actual height will be set later
        containerHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: 10.0)
        
        // constrain container safe-area top / leading / trailing to view with 20-pts padding
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            containerHeightConstraint,
        ])
        
        // add the buttons to the scroll view
        addTagLabels()
        
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // call this here, after views have been laid-out
        // this will also be called when the size changes, such as device rotation,
        // so the buttons will "re-layout"
        displayTagLabels()
        
    }
    
    func addTagLabels() -> Void {
        
        for j in 0..<self.tagNames.count {
            
            // create a new label
            let newLabel = UILabel()
            
            // set its properties (title, colors, corners, etc)
            newLabel.text = tagNames[j]
            newLabel.textAlignment = .center
            newLabel.backgroundColor = UIColor.cyan
            newLabel.layer.masksToBounds = true
            newLabel.layer.cornerRadius = 8
            newLabel.layer.borderColor = UIColor.red.cgColor
            newLabel.layer.borderWidth = 1

            // set its frame width and height
            newLabel.frame.size.width = newLabel.intrinsicContentSize.width + tagPadding
            newLabel.frame.size.height = tagHeight
            
            // add it to the scroll view
            containerView.addSubview(newLabel)
            
            // append it to tagLabels array
            tagLabels.append(newLabel)
            
        }
        
    }
    
    func displayTagLabels() {
        
        let containerWidth = containerView.frame.size.width
        
        var currentOriginX: CGFloat = 0
        var currentOriginY: CGFloat = 0
        
        // for each label in the array
        tagLabels.forEach { label in
            
            // if current X + label width will be greater than container view width
            //  "move to next row"
            if currentOriginX + label.frame.width > containerWidth {
                currentOriginX = 0
                currentOriginY += tagHeight + tagSpacingY
            }
            
            // set the btn frame origin
            label.frame.origin.x = currentOriginX
            label.frame.origin.y = currentOriginY
            
            // increment current X by btn width + spacing
            currentOriginX += label.frame.width + tagSpacingX
            
        }
        
        // update container view height
        containerHeightConstraint.constant = currentOriginY + tagHeight
        
    }
    
}

结果:

这很简单,使用代码中的 cmets,您应该能够根据需要对其进行调整。

如果您想要一个“预建”的解决方案,也许有更多的功能,搜索

swift left aligned tags view

想出了很多匹配项。这个(和我无关)看起来很有趣:https://github.com/ElaWorkshop/TagListView


编辑

在表格视图单元格中使用此概念与将其用作视图控制器中的视图没有太大区别。

第一步,让我们创建一个自定义的UIView 子类来处理所有的布局逻辑:

class TagLabelsView: UIView {
    
    var tagNames: [String] = [] {
        didSet {
            addTagLabels()
        }
    }
    
    let tagHeight:CGFloat = 30
    let tagPadding: CGFloat = 16
    let tagSpacingX: CGFloat = 8
    let tagSpacingY: CGFloat = 8

    var intrinsicHeight: CGFloat = 0
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() -> Void {
    }

    func addTagLabels() -> Void {
        
        // if we already have tag labels (or buttons, etc)
        //  remove any excess (e.g. we had 10 tags, new set is only 7)
        while self.subviews.count > tagNames.count {
            self.subviews[0].removeFromSuperview()
        }
        
        // if we don't have enough labels, create and add as needed
        while self.subviews.count < tagNames.count {

            // create a new label
            let newLabel = UILabel()
            
            // set its properties (title, colors, corners, etc)
            newLabel.textAlignment = .center
            newLabel.backgroundColor = UIColor.cyan
            newLabel.layer.masksToBounds = true
            newLabel.layer.cornerRadius = 8
            newLabel.layer.borderColor = UIColor.red.cgColor
            newLabel.layer.borderWidth = 1

            addSubview(newLabel)
            
        }

        // now loop through labels and set text and size
        for (str, v) in zip(tagNames, self.subviews) {
            guard let label = v as? UILabel else {
                fatalError("non-UILabel subview found!")
            }
            label.text = str
            label.frame.size.width = label.intrinsicContentSize.width + tagPadding
            label.frame.size.height = tagHeight
        }

    }
    
    func displayTagLabels() {
        
        var currentOriginX: CGFloat = 0
        var currentOriginY: CGFloat = 0

        // for each label in the array
        self.subviews.forEach { v in
            
            guard let label = v as? UILabel else {
                fatalError("non-UILabel subview found!")
            }

            // if current X + label width will be greater than container view width
            //  "move to next row"
            if currentOriginX + label.frame.width > bounds.width {
                currentOriginX = 0
                currentOriginY += tagHeight + tagSpacingY
            }
            
            // set the btn frame origin
            label.frame.origin.x = currentOriginX
            label.frame.origin.y = currentOriginY
            
            // increment current X by btn width + spacing
            currentOriginX += label.frame.width + tagSpacingX
            
        }
        
        // update intrinsic height
        intrinsicHeight = currentOriginY + tagHeight
        invalidateIntrinsicContentSize()
        
    }

    // allow this view to set its own intrinsic height
    override var intrinsicContentSize: CGSize {
        var sz = super.intrinsicContentSize
        sz.height = intrinsicHeight
        return sz
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        displayTagLabels()
    }
    
}

我们可以在单元格中使用它——或者,作为“常规的旧子视图”——像这样:

let tagsView = TagLabelsView()
let tags: [String] = ["One", "Two", "Three", "etc..."]
tagsView.tagNames = tags

这是一个使用我们自定义 TagLabelsView 的完整示例:

class PlainTagLabelsViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        let tagsView = TagLabelsView()
        
        // add the tags view
        view.addSubview(tagsView)
        
        // use autolayout
        tagsView.translatesAutoresizingMaskIntoConstraints = false
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            // constrain to safe-area top / leading / trailing to view with 20-pts padding
            tagsView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            tagsView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            tagsView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
        ])

        // some sample "tags" from Stack Overflow
        let tags: [String] = [
            "asp.net-core",
            "asp.net-mvc",
            "asp.net",
            "azure",
            "bash",
            "c",
            "c#",
            "c++",
            "class",
            "codeigniter",
            "cordova",
            "css",
            "csv",
            "dart",
            "database",
            "dataframe",
        ]

        tagsView.tagNames = tags
        
        // give the tags view a background color so we can see it
        tagsView.backgroundColor = .yellow
    }
    
}

要在表格视图单元格中使用它,我们创建一个单元格类,使用我们的TagLabelsView 作为子视图:

class TagsCell: UITableViewCell {

    let tagsView: TagLabelsView = {
        let v = TagLabelsView()
        return v
    }()
    
    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 {
        
        // add the container view
        contentView.addSubview(tagsView)
        
        // give it a background color so we can see it
        tagsView.backgroundColor = .yellow
        
        // use autolayout
        tagsView.translatesAutoresizingMaskIntoConstraints = false
        
        // constrain tagsView top / leading / trailing / bottom to
        //  contentView Layout Margins Guide
        let g = contentView.layoutMarginsGuide
        
        NSLayoutConstraint.activate([
            tagsView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            tagsView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            tagsView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            tagsView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
        ])

    }

    func fillData(_ tagNames: [String]) -> Void {
        tagsView.tagNames = tagNames
    }
    
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        //force layout of all subviews including RectsView, which
        //updates RectsView's intrinsic height, and thus height of a cell
        self.setNeedsLayout()
        self.layoutIfNeeded()

        //now intrinsic height is correct, so we can call super method
        return super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
    }

}

还有一个带有包含多组“标签”的表格视图的示例视图控制器:

class TagLabelsViewController: UIViewController {

    var myData: [[String]] = []
    
    let tableView: UITableView = {
        let v = UITableView()
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add the table view
        view.addSubview(tableView)
        
        // use autolayout
        tableView.translatesAutoresizingMaskIntoConstraints = false
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            // constrain table view safe-area top / leading / trailing / bottom to view with 20-pts padding
            tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
        ])
        
        tableView.register(TagsCell.self, forCellReuseIdentifier: "c")
        tableView.dataSource = self
        tableView.delegate = self

        // get some sample tag data
        myData = SampleTags().samples()
    }

}

extension TagLabelsViewController: UITableViewDataSource, UITableViewDelegate {
    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 c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! TagsCell
        c.fillData(myData[indexPath.row])
        return c
    }
}

class SampleTags: NSData {
    func samples() -> [[String]] {

        let tmp: [[String]] = [
            [
                ".htaccess",
                ".net",
                "ajax",
                "algorithm",
            ],
            [
                "amazon-web-services",
                "android-layout",
                "android-studio",
                "android",
                "angular",
                "angularjs",
                "apache-spark",
            ],
            [
                "apache",
                "api",
                "arrays",
            ],
            [
                "asp.net-core",
                "asp.net-mvc",
                "asp.net",
                "azure",
                "bash",
                "c",
                "c#",
                "c++",
                "class",
                "codeigniter",
                "cordova",
                "css",
                "csv",
                "dart",
                "database",
                "dataframe",
            ],
            [
                "date",
                "datetime",
                "dictionary",
                "django",
                "docker",
            ],
            [
                "eclipse",
                "email",
                "entity-framework",
                "excel",
                "express",
                "facebook",
            ],
            [
                "file",
                "firebase",
                "flutter",
                "for-loop",
                "forms",
                "function",
                "git",
                "go",
                "google-chrome",
                "google-maps",
                "hibernate",
                "html",
                "http",
            ],
            [
                "image",
                "ios",
                "iphone",
                "java",
                "javascript",
                "jquery",
                "json",
                "kotlin",
                "laravel",
                "linq",
                "linux",
            ],
            [
                "list",
                "loops",
                "macos",
                "matlab",
                "matplotlib",
                "maven",
                "mongodb",
                "multithreading",
                "mysql",
                "node.js",
            ],
            [
                "numpy",
                "object",
                "objective-c",
                "oop",
                "opencv",
                "oracle",
                "pandas",
                "performance",
                "perl",
                "php",
                "postgresql",
                "powershell",
                "python-2.7",
                "python-3.x",
                "python",
            ],
            [
                "qt",
                "r",
                "react-native",
                "reactjs",
                "regex",
                "rest",
                "ruby-on-rails-3",
                "ruby-on-rails",
                "ruby",
                "scala",
                "selenium",
                "shell",
                "sockets",
                "sorting",
                "spring-boot",
                "spring-mvc",
                "spring",
                "sql-server",
                "sql",
            ],
            [
                "sqlite",
                "string",
                "swift",
            ],
            [
                "swing",
                "symfony",
                "tensorflow",
                "tsql",
                "twitter-bootstrap",
                "typescript",
                "uitableview",
                "unit-testing",
                "unity3d",
                "validation",
                "vb.net",
                "vba",
                "visual-studio",
                "vue.js",
                "web-services",
                "windows",
                "winforms",
                "wordpress",
                "wpf",
                "xaml",
                "xcode",
                "xml",
            ],
        ]
        
        return tmp
    }
}

样本输出(iPhone 13 Pro Max):

【讨论】:

  • 很好的答案@DonMag,在 2021 年底仍然有效。您将如何在 tableview Cell 中执行此操作,它应该自动调整大小以适合您的 containerview 的高度?
  • @JanL - 将其添加到表格视图单元格非常简单。请参阅我的答案的编辑
猜你喜欢
  • 1970-01-01
  • 2018-08-16
  • 2021-03-20
  • 1970-01-01
  • 1970-01-01
  • 2020-01-28
  • 2021-06-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多