autolayout-conform UILabel 与垂直文本(objC 或 Swift)?

发布于 2025-01-16 08:38:26 字数 304 浏览 0 评论 0原文

我将如何创建一个具有垂直文本流的 UIView / UILabel ,它看起来像这个示例屏幕的红色视图?

查看方式垂直文本

我读过有关 view.transform = CGAffineTransform(... 的内容,它允许轻松旋转,但它会打破自动布局约束。

我很乐意使用第三方库,但是我找不到 任何。

How would I create an UIView / UILabel with vertical text flow which would look like the red view of this example screen?

view with vertical text

I have read about view.transform = CGAffineTransform(... which allows for easy rotation, BUT it would break the auto-layout constraints.

I would be happy to use a third-party library, but I cannot find any.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

冷弦 2025-01-23 08:38:28

正如 Apple 的文档中所述:

在 iOS 8.0 及更高版本中,transform 属性不会影响自动布局。自动布局根据视图未变换的框架计算视图的对齐矩形。

因此,为了让转换后的视图与自动布局“发挥良好”作用,我们实际上需要告诉约束使用相反的轴。

例如,如果我们在 UIView 中嵌入 UILabel 并将标签旋转 90 度,我们想要限制“容器”视图的宽度到标签的高度,其高度到标签的宽度

这是一个示例 VerticalLabelView 视图子类:

class VerticalLabelView: UIView {
    
    public var numberOfLines: Int = 1 {
        didSet {
            label.numberOfLines = numberOfLines
        }
    }
    public var text: String = "" {
        didSet {
            label.text = text
        }
    }
    
    // vertical and horizontal "padding"
    //  defaults to 16-ps (8-pts on each side)
    public var vPad: CGFloat = 16.0 {
        didSet {
            h.constant = vPad
        }
    }
    public var hPad: CGFloat = 16.0 {
        didSet {
            w.constant = hPad
        }
    }
    
    // because the label is rotated, we need to swap the axis
    override func setContentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) {
        label.setContentHuggingPriority(priority, for: axis == .horizontal ? .vertical : .horizontal)
    }
    
    // this is just for development
    //  show/hide border of label
    public var showBorder: Bool = false {
        didSet {
            label.layer.borderWidth = showBorder ? 1 : 0
            label.layer.borderColor = showBorder ? UIColor.red.cgColor : UIColor.clear.cgColor
        }
    }
    
    public let label = UILabel()
    
    private var w: NSLayoutConstraint!
    private var h: NSLayoutConstraint!
    private var mh: NSLayoutConstraint!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        addSubview(label)
        label.backgroundColor = .clear
        
        label.translatesAutoresizingMaskIntoConstraints = false
        
        // rotate 90-degrees
        let angle = .pi * 0.5
        label.transform = CGAffineTransform(rotationAngle: angle)
        
        // so we can change the "padding" dynamically
        w = self.widthAnchor.constraint(equalTo: label.heightAnchor, constant: hPad)
        h = self.heightAnchor.constraint(equalTo: label.widthAnchor, constant: vPad)
        
        NSLayoutConstraint.activate([
            
            label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: self.centerYAnchor),
            w, h,
            
        ])
        
    }
    
}

我添加了一些属性以允许将视图视为标签,因此我们可以这样做:

let v = VerticalLabelView()

// "pass-through" properties
v.text = "Some text which will be put into the label."
v.numberOfLines = 0

// directly setting properties
v.label.textColor = .red

当然,这可以扩展为“通过”我们需要使用的所有标签属性,因此我们不需要直接引用 .label

现在,这个 VerticalLabelView 的使用方式与普通 UILabel 非常相似。

下面是两个示例 - 它们都使用此 BaseVC 来设置视图:

class BaseVC: UIViewController {
    
    let greenView: UIView = {
        let v = UIView()
        v.backgroundColor = .green
        return v
    }()
    let normalLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        return v
    }()
    
    let lYellow: VerticalLabelView = {
        let v = VerticalLabelView()
        v.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 0.5, alpha: 1.0)
        v.numberOfLines = 0
        return v
    }()
    
    let lRed: VerticalLabelView = {
        let v = VerticalLabelView()
        v.backgroundColor = UIColor(red: 1.0, green: 0.5, blue: 0.5, alpha: 1.0)
        v.numberOfLines = 0
        return v
    }()
    
    let lBlue: VerticalLabelView = {
        let v = VerticalLabelView()
        v.backgroundColor = UIColor(red: 0.3, green: 0.8, blue: 1.0, alpha: 1.0)
        v.numberOfLines = 1
        return v
    }()
    
    let container: UIView = {
        let v = UIView()
        v.backgroundColor = .systemYellow
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let strs: [String] = [
            "Multiline Vertical Text",
            "Vertical Text",
            "Overflow Vertical Text",
        ]
        
        // default UILabel
        normalLabel.text = "Regular UILabel wrapping text"
        // add the normal label to the green view
        greenView.addSubview(normalLabel)
        
        // set text of vertical labels
        for (s, v) in zip(strs, [lYellow, lRed, lBlue]) {
            v.text = s
        }
        
        [container, greenView, normalLabel, lYellow, lRed, lBlue].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // add greenView to the container
        container.addSubview(greenView)
        
        // add container to self's view
        view.addSubview(container)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // constrain container Top and CenterX
            container.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            container.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            // comment next line to allow container subviews to set the height
            container.heightAnchor.constraint(equalToConstant: 260.0),
            
            // comment next line to allow container subviews to set the width
            container.widthAnchor.constraint(equalToConstant: 160.0),
            
            // green view at Top, stretched full width
            greenView.topAnchor.constraint(equalTo: container.topAnchor, constant: 0.0),
            greenView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
            greenView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),

            // constrain normal label in green view
            //  with 8-pts "padding" on all 4 sides
            normalLabel.topAnchor.constraint(equalTo: greenView.topAnchor, constant: 8.0),
            normalLabel.leadingAnchor.constraint(equalTo: greenView.leadingAnchor, constant: 8.0),
            normalLabel.trailingAnchor.constraint(equalTo: greenView.trailingAnchor, constant: -8.0),
            normalLabel.bottomAnchor.constraint(equalTo: greenView.bottomAnchor, constant: -8.0),
            
        ])
    }
    
}

第一个示例 - SubviewsExampleVC - 将每个示例添加为子视图,然后我们在视图之间添加约束:

class SubviewsExampleVC: BaseVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add vertical labels to the container
        [lYellow, lRed, lBlue].forEach { v in
            container.addSubview(v)
        }

        NSLayoutConstraint.activate([
            
            // yellow label constrained to Bottom of green view
            lYellow.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
            // Leading to container Leading
            lYellow.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
            
            // red label constrained to Bottom of green view
            lRed.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
            // Leading to yellow label Trailing
            lRed.leadingAnchor.constraint(equalTo: lYellow.trailingAnchor, constant: 0.0),
            
            // blue label constrained to Bottom of green view
            lBlue.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
            // Leading to red label Trailing
            lBlue.leadingAnchor.constraint(equalTo: lRed.trailingAnchor, constant: 0.0),
            
            // if we want the labels to fill the container width
            //  blue label Trailing constrained to container Trailing
            lBlue.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
            
            // using constraints to set the vertical label heights
            lYellow.heightAnchor.constraint(equalToConstant: 132.0),
            lRed.heightAnchor.constraint(equalTo: lYellow.heightAnchor),
            lBlue.heightAnchor.constraint(equalTo: lYellow.heightAnchor),
            
        ])
        
        // as always, we need to control which view(s)
        //  hug their content

        // so, for example, if we want the Yellow label to "stretch" horizontally
        lRed.setContentHuggingPriority(.required, for: .horizontal)
        lBlue.setContentHuggingPriority(.required, for: .horizontal)
        
        // or, for example, if we want the Red label to "stretch" horizontally
        //lYellow.setContentHuggingPriority(.required, for: .horizontal)
        //lBlue.setContentHuggingPriority(.required, for: .horizontal)

    }

}

第二个示例 = StackviewExampleVC - 将每个示例添加为 UIStackView 的排列子视图:

class StackviewExampleVC: BaseVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // horizontal stack view
        let stackView = UIStackView()
        
        // add vertical labels to the stack view
        [lYellow, lRed, lBlue].forEach { v in
            stackView.addArrangedSubview(v)
        }
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // add stack view to container
        container.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            
            // constrain stack view Top to green view Bottom
            stackView.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),

            // Leading / Trailing to container Leading / Trailing
            stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
            stackView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
            
            // stack view height
            stackView.heightAnchor.constraint(equalToConstant: 132.0),
            
        ])

        // as always, we need to control which view(s)
        //  hug their content
        // so, for example, if we want the Yellow label to "stretch" horizontally
        lRed.setContentHuggingPriority(.required, for: .horizontal)
        lBlue.setContentHuggingPriority(.required, for: .horizontal)
        
        // or, for example, if we want the Red label to "stretch" horizontally
        //lYellow.setContentHuggingPriority(.required, for: .horizontal)
        //lBlue.setContentHuggingPriority(.required, for: .horizontal)
        
    }
    
}

两个示例都会生成以下输出:

在此处输入图像描述

请注意:这是仅示例代码 - 它无意也不应被视为生产就绪

As noted in Apple's docs:

In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.

So, to get transformed views to "play nice" with auto layout, we need to - in effect - tell constraints to use the opposite axis.

For example, if we embed a UILabel in a UIView and rotate the label 90-degrees, we want to constrain the "container" view's Width to the label's Height and its Height to the label's Width.

Here's a sample VerticalLabelView view subclass:

class VerticalLabelView: UIView {
    
    public var numberOfLines: Int = 1 {
        didSet {
            label.numberOfLines = numberOfLines
        }
    }
    public var text: String = "" {
        didSet {
            label.text = text
        }
    }
    
    // vertical and horizontal "padding"
    //  defaults to 16-ps (8-pts on each side)
    public var vPad: CGFloat = 16.0 {
        didSet {
            h.constant = vPad
        }
    }
    public var hPad: CGFloat = 16.0 {
        didSet {
            w.constant = hPad
        }
    }
    
    // because the label is rotated, we need to swap the axis
    override func setContentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) {
        label.setContentHuggingPriority(priority, for: axis == .horizontal ? .vertical : .horizontal)
    }
    
    // this is just for development
    //  show/hide border of label
    public var showBorder: Bool = false {
        didSet {
            label.layer.borderWidth = showBorder ? 1 : 0
            label.layer.borderColor = showBorder ? UIColor.red.cgColor : UIColor.clear.cgColor
        }
    }
    
    public let label = UILabel()
    
    private var w: NSLayoutConstraint!
    private var h: NSLayoutConstraint!
    private var mh: NSLayoutConstraint!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        addSubview(label)
        label.backgroundColor = .clear
        
        label.translatesAutoresizingMaskIntoConstraints = false
        
        // rotate 90-degrees
        let angle = .pi * 0.5
        label.transform = CGAffineTransform(rotationAngle: angle)
        
        // so we can change the "padding" dynamically
        w = self.widthAnchor.constraint(equalTo: label.heightAnchor, constant: hPad)
        h = self.heightAnchor.constraint(equalTo: label.widthAnchor, constant: vPad)
        
        NSLayoutConstraint.activate([
            
            label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: self.centerYAnchor),
            w, h,
            
        ])
        
    }
    
}

I've added a few properties to allow the view to be treated like a label, so we can do:

let v = VerticalLabelView()

// "pass-through" properties
v.text = "Some text which will be put into the label."
v.numberOfLines = 0

// directly setting properties
v.label.textColor = .red

This could, of course, be extended to "pass through" all label properties we need to use so we wouldn't need to reference the .label directly.

This VerticalLabelView can now be used much like a normal UILabel.

Here are two examples - they both use this BaseVC to setup the views:

class BaseVC: UIViewController {
    
    let greenView: UIView = {
        let v = UIView()
        v.backgroundColor = .green
        return v
    }()
    let normalLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        return v
    }()
    
    let lYellow: VerticalLabelView = {
        let v = VerticalLabelView()
        v.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 0.5, alpha: 1.0)
        v.numberOfLines = 0
        return v
    }()
    
    let lRed: VerticalLabelView = {
        let v = VerticalLabelView()
        v.backgroundColor = UIColor(red: 1.0, green: 0.5, blue: 0.5, alpha: 1.0)
        v.numberOfLines = 0
        return v
    }()
    
    let lBlue: VerticalLabelView = {
        let v = VerticalLabelView()
        v.backgroundColor = UIColor(red: 0.3, green: 0.8, blue: 1.0, alpha: 1.0)
        v.numberOfLines = 1
        return v
    }()
    
    let container: UIView = {
        let v = UIView()
        v.backgroundColor = .systemYellow
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let strs: [String] = [
            "Multiline Vertical Text",
            "Vertical Text",
            "Overflow Vertical Text",
        ]
        
        // default UILabel
        normalLabel.text = "Regular UILabel wrapping text"
        // add the normal label to the green view
        greenView.addSubview(normalLabel)
        
        // set text of vertical labels
        for (s, v) in zip(strs, [lYellow, lRed, lBlue]) {
            v.text = s
        }
        
        [container, greenView, normalLabel, lYellow, lRed, lBlue].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // add greenView to the container
        container.addSubview(greenView)
        
        // add container to self's view
        view.addSubview(container)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // constrain container Top and CenterX
            container.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            container.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            // comment next line to allow container subviews to set the height
            container.heightAnchor.constraint(equalToConstant: 260.0),
            
            // comment next line to allow container subviews to set the width
            container.widthAnchor.constraint(equalToConstant: 160.0),
            
            // green view at Top, stretched full width
            greenView.topAnchor.constraint(equalTo: container.topAnchor, constant: 0.0),
            greenView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
            greenView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),

            // constrain normal label in green view
            //  with 8-pts "padding" on all 4 sides
            normalLabel.topAnchor.constraint(equalTo: greenView.topAnchor, constant: 8.0),
            normalLabel.leadingAnchor.constraint(equalTo: greenView.leadingAnchor, constant: 8.0),
            normalLabel.trailingAnchor.constraint(equalTo: greenView.trailingAnchor, constant: -8.0),
            normalLabel.bottomAnchor.constraint(equalTo: greenView.bottomAnchor, constant: -8.0),
            
        ])
    }
    
}

The first example - SubviewsExampleVC - adds each as a subview, and then we add constraints between the views:

class SubviewsExampleVC: BaseVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add vertical labels to the container
        [lYellow, lRed, lBlue].forEach { v in
            container.addSubview(v)
        }

        NSLayoutConstraint.activate([
            
            // yellow label constrained to Bottom of green view
            lYellow.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
            // Leading to container Leading
            lYellow.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
            
            // red label constrained to Bottom of green view
            lRed.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
            // Leading to yellow label Trailing
            lRed.leadingAnchor.constraint(equalTo: lYellow.trailingAnchor, constant: 0.0),
            
            // blue label constrained to Bottom of green view
            lBlue.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
            // Leading to red label Trailing
            lBlue.leadingAnchor.constraint(equalTo: lRed.trailingAnchor, constant: 0.0),
            
            // if we want the labels to fill the container width
            //  blue label Trailing constrained to container Trailing
            lBlue.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
            
            // using constraints to set the vertical label heights
            lYellow.heightAnchor.constraint(equalToConstant: 132.0),
            lRed.heightAnchor.constraint(equalTo: lYellow.heightAnchor),
            lBlue.heightAnchor.constraint(equalTo: lYellow.heightAnchor),
            
        ])
        
        // as always, we need to control which view(s)
        //  hug their content

        // so, for example, if we want the Yellow label to "stretch" horizontally
        lRed.setContentHuggingPriority(.required, for: .horizontal)
        lBlue.setContentHuggingPriority(.required, for: .horizontal)
        
        // or, for example, if we want the Red label to "stretch" horizontally
        //lYellow.setContentHuggingPriority(.required, for: .horizontal)
        //lBlue.setContentHuggingPriority(.required, for: .horizontal)

    }

}

The second example = StackviewExampleVC - adds each as an arranged subview of a UIStackView:

class StackviewExampleVC: BaseVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // horizontal stack view
        let stackView = UIStackView()
        
        // add vertical labels to the stack view
        [lYellow, lRed, lBlue].forEach { v in
            stackView.addArrangedSubview(v)
        }
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // add stack view to container
        container.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            
            // constrain stack view Top to green view Bottom
            stackView.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),

            // Leading / Trailing to container Leading / Trailing
            stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
            stackView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
            
            // stack view height
            stackView.heightAnchor.constraint(equalToConstant: 132.0),
            
        ])

        // as always, we need to control which view(s)
        //  hug their content
        // so, for example, if we want the Yellow label to "stretch" horizontally
        lRed.setContentHuggingPriority(.required, for: .horizontal)
        lBlue.setContentHuggingPriority(.required, for: .horizontal)
        
        // or, for example, if we want the Red label to "stretch" horizontally
        //lYellow.setContentHuggingPriority(.required, for: .horizontal)
        //lBlue.setContentHuggingPriority(.required, for: .horizontal)
        
    }
    
}

Both examples produce this output:

enter image description here

Please note: this is Example Code Only - it is not intended to be, nor should it be considered to be, Production Ready

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文