在多行的水平 UIStackView 中包装项目 [英] Wrap items in a horizontal UIStackView on multiple lines

查看:20
本文介绍了在多行的水平 UIStackView 中包装项目的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

解决方案

有很多不同的方法可以解决这个问题.

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

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

这是一个简单的例子:

类 TagLabelsViewController: UIViewController {让容器视图:UIView = {让 v = UIView()返回 v}()让标记名称:[字符串] = [第一个标签",第二",第三个标签",第四",第五个标签",第六",第七",标签八",这里有一些字母标签",A"、B"、C"、D"、E"、F"、G"、H"、I"、J"、九",十",十一",标签十二",标签 13",十四",十五",十六",《十七岁》,《十八岁》,十九",最后一个标签",]var tagLabels = [UILabel]()让 tagHeight:CGFloat = 30让 tagPadding: CGFloat = 16让 tagSpacingX: CGFloat = 8让 tagSpacingY: CGFloat = 8//布局子视图时将修改容器视图高度var containerHeightConstraint: NSLayoutConstraint = NSLayoutConstraint()覆盖 func viewDidLoad() {super.viewDidLoad()//添加容器视图view.addSubview(containerView)//给它一个背景颜色,这样我们就可以看到它containerView.backgroundColor = .yellow//使用自动布局containerView.translatesAutoresizingMaskIntoConstraints = false//初始化高度约束 - 实际高度将在稍后设置containerHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: 10.0)//使用 20 点填充约束容器安全区域顶部/前导/尾随查看让 g = view.safeAreaLayoutGuideNSLayoutConstraint.activate([containerView.topAnchor.constraint(equalTo: g.topAnchor, 常量: 20.0),containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, 常量: -20.0),容器高度约束,])//将按钮添加到滚动视图添加标签标签()}覆盖 func viewDidLayoutSubviews() {super.viewDidLayoutSubviews()//在视图布局完成后调用这里//当大小改变时也会调用这个,比如设备旋转,//所以按钮将重新布局"显示标签标签()}func addTagLabels() ->空白 {对于 j in 0..容器宽度 {当前原点X = 0currentOriginY += tagHeight + tagSpacingY}//设置 btn 帧原点label.frame.origin.x = currentOriginXlabel.frame.origin.y = currentOriginY//将当前 X 增加 btn 宽度 + 间距currentOriginX += label.frame.width + tagSpacingX}//更新容器视图高度containerHeightConstraint.constant = currentOriginY + tagHeight}}

结果:

这很简单,通过代码中的注释,您应该能够根据需要对其进行调整.

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

swift 左对齐标签视图

想出了很多匹配项.这个(我与它无关)看起来很有趣:

I am working on an iOS app which display a UITableView of items. Each row will show a list of tags with different widths(given by the tag's text length). I can place all the tags in a horizontal UIStackView but I want them to wrap on multiple lines instead of o a single scrollable one. Basically I'm interested in a functionality similar to FlexBox's flex-wrap property.

I've attached an image for reference.

Any ideas how to achieve this?

解决方案

There are a number of different ways to approach this.

One approach - not using stack views:

  • add your labels to a "container" view
  • start with x = 0 and y = 0
  • loop through the labels, calculating a new x value (label width + desired spacing between labels)
  • if the new x would be past the edge of the container, reset x = 0 and add desired height to y to "move to the next row"
  • after labels have been laid out, set the height of the container view

Here is a simple example:

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

The results:

It's pretty straight-forward, and with the comments in the code you should be able to adapt it to your needs.

If you want a "pre-built" solution, perhaps with more features, searching for

swift left aligned tags view

comes up with lots of matches. This one (I have nothing to do with it) looks interesting: https://github.com/ElaWorkshop/TagListView


Edit

Using this concept in a table view cell is not much different than using it as a view in a view controller.

First step, let's create a custom UIView subclass to handle all of the layout logic:

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

We can use that inside a cell -- or, as a "regular old subview" -- like this:

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

Here's a complete example using our custom 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
    }
    
}

To use that in a table view cell, we create a cell class that uses our TagLabelsView as a subview:

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

}

and a sample view controller with a table view containing several sets of "tags":

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

Sample Output (iPhone 13 Pro Max):

这篇关于在多行的水平 UIStackView 中包装项目的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆