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

autolayout-conform UILabel with vertical text (objC or Swift)?

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

我读过 view.transform = CGAffineTransform(... 可以轻松旋转,但它会破坏自动布局约束。

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

如 Apple 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.

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

例如,如果我们在UIView中嵌入一个UILabel并将标签旋转90度,我们想要约束“容器”视图的宽度 到标签的 Height 及其 Height 到标签的 Width.

这是一个示例 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)
        
    }
    
}

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

请注意:这只是 示例代码 - 它不是,也不应该被认为是,生产就绪