让自动布局与 UIControl 子类一起工作

Getting autolayout to work with a UIControl subclass

我已经能够基本上按照我想要的方式获得我的自定义控件,但我希望它能与自动版式一起使用。我做了很多尝试和错误,但没有找到任何地方。

当我设置框架时控件按预期布局。

circleContol = CircleControl(size: 80, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
circleContol.center = CGPoint(x: 150, y: 150)
view.addSubview(circleContol)

但是,我正在努力让控件与 Autolayout 配合使用。

即使用

NSLayoutConstraint.activate([
    circleContol.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    circleContol.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])

结果

我试过尝试在我的 UIControl 子类中设置自动版式,但无法正确设置。例如,我尝试了以下方法,但我不确定这是否是正确的方法,或者是设置此约束的最佳位置 - 特别是圆形 shapeLayer 似乎是另一个问题。

override func updateConstraints() {

    NSLayoutConstraint.activate([
        imageOverlay.centerYAnchor.constraint(equalTo: centerYAnchor),
        imageOverlay.centerXAnchor.constraint(equalTo: centerXAnchor),
        imageOverlay.heightAnchor.constraint(equalToConstant: radius),
        imageOverlay.widthAnchor.constraint(equalToConstant: radius),
    ])

    super.updateConstraints()
}

这里是playground liveView代码,希望有人能提供一些指导。它有很多代码,但我觉得这是让别人看到问题的最佳方式

谢谢

上面的每个例子都可以在playGround中看到使用这些方法

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    // 1. Lays out as expected
    createControl()

    // 2. Views all out of place
    // createUsingAutoLayout()
}

PlayGround 代码

import UIKit
import PlaygroundSupport

class CircleControl: UIControl {

    private var shaperLayer: CAShapeLayer!
    private var stroke: CGFloat!
    private var padding: CGFloat!
    private var circleColour: UIColor!
    private var title: String!
    private var imageColour: UIColor!
    private var textColour: UIColor!
    private var imageOverlay: UIImageView!
    private let fontSize: CGFloat = 20

    private var actualRadius: CGFloat {
        let halfSquare: CGFloat = max(bounds.size.width, bounds.size.height) / 2
        let temp: CGFloat = halfSquare * halfSquare + halfSquare * halfSquare
        let radius: CGFloat = temp.squareRoot()
        return radius
    }

    private var radius: CGFloat {
        return actualRadius + padding + (stroke * 0.5)
    }

    private var textRadius: CGFloat {
        return actualRadius + padding + stroke + (fontSize * 0.5)
    }

    init(size: Int, title: String, circleStrokeSize: CGFloat, circlePadding: CGFloat = 0, image: UIImage, imageColour: UIColor, circleColour: UIColor, textColour: UIColor) {
        let f = CGRect(x: 0, y: 0, width: size, height: size)
        self.stroke = circleStrokeSize
        self.padding = circlePadding
        self.circleColour = circleColour
        self.imageColour = imageColour
        self.textColour = textColour
        self.title = title
        super.init(frame: f)

        // add subviews
        updateView()

        // add image to button
        let tinted = image.withRenderingMode(.alwaysTemplate)
        imageOverlay.image = tinted
    }

    private func addTextToView() {
        let label = CurvedTextView(frame: CGRect(x: 0, y: 0, width: 300, height: 300), title: title, r: textRadius, fontSize: fontSize, fontColour: textColour, location: .bottom)
        label.center = imageOverlay.center
        label.isUserInteractionEnabled = false
        label.backgroundColor = UIColor.clear
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        fatalError("init(coder:) has not been implemented")
    }

    func updateView() {
        addImageOverlay()
        addCircle()
        addTextToView()
    }

    func addImageOverlay() {
        imageOverlay = UIImageView(frame: bounds)
        imageOverlay.tintColor = imageColour
        imageOverlay.contentMode = .scaleAspectFill
        addSubview(imageOverlay)
        imageOverlay.isUserInteractionEnabled = false
        imageOverlay.translatesAutoresizingMaskIntoConstraints = false
    }

    func addCircle() {
        if let s = shaperLayer {
            s.removeFromSuperlayer()
        }

        shaperLayer = CAShapeLayer()
        let circularPath = UIBezierPath(arcCenter: UIView(frame: bounds).center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        shaperLayer.path = circularPath.cgPath
        shaperLayer.strokeColor = circleColour.cgColor
        shaperLayer.fillColor = UIColor.clear.cgColor
        shaperLayer.lineWidth = stroke
        layer.addSublayer(shaperLayer)
    }
}

class CurvedTextView: UIView {

    var radius: CGFloat!
    var fontSize: CGFloat!
    var fontColour: UIColor!
    var labelPost: LabelPostition = .bottom
    var title: String!

    init(frame: CGRect, title: String, r: CGFloat, fontSize: CGFloat, fontColour: UIColor, location: LabelPostition) {
        self.radius = r
        self.fontSize = fontSize
        self.fontColour = fontColour
        self.labelPost = location
        self.title = title
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        fatalError("init(coder:) has not been implemented")
    }

    enum LabelPostition {
        case bottom
    }

    func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
        let characters: [String] = str.map { String([=14=]) }
        let l = characters.count
        let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.kern: 0.8]

        var arcs: [CGFloat] = []
        var totalArc: CGFloat = 0

        for i in 0 ..< l {
            arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
            totalArc += arcs[i]
        }

        let direction: CGFloat = clockwise ? -1 : 1
        let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2

        var thetaI = theta - direction * totalArc / 2

        for i in 0 ..< l {
            thetaI += direction * arcs[i] / 2
            centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)

            thetaI += direction * arcs[i] / 2
        }
    }

    func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
        let attributes = [NSAttributedString.Key.foregroundColor: c, NSAttributedString.Key.font: font]
        context.saveGState()
        context.scaleBy(x: 1, y: -1)
        context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
        context.rotate(by: -slantAngle)
        let offset = str.size(withAttributes: attributes)
        context.translateBy (x: -offset.width / 2, y: -offset.height / 2)
        str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
        context.restoreGState()
    }

    func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
        return 2 * asin(chord / (2 * radius))
    }

    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else { return }

        let size = self.bounds.size

        context.translateBy (x: size.width / 2, y: size.height / 2)
        context.scaleBy (x: 1, y: -1)

        let f = UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight.semibold)

        var startAngle: CGFloat = .pi
        var clock = true

        switch labelPost {
        case .bottom:
            startAngle = -(.pi / 2)
            clock = false
        }

        centreArcPerpendicular(text: title, context: context, radius: radius, angle: startAngle, colour: fontColour, font: f, clockwise: clock)
    }
}

class MyViewController : UIViewController {

    var circleContol: CircleControl!
    private let testImage = UIImage(named: "faceLike.png")!

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .lightGray
        self.view = view
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // 1. Lays out as expected
        createControl()

        // 2. Views all out of place
        // createUsingAutoLayout()
    }

    func createControl() {
        circleContol = CircleControl(size: 80, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
        circleContol.center = CGPoint(x: 150, y: 150)
        view.addSubview(circleContol)
    }

    func createUsingAutoLayout() {
        circleContol = CircleControl(size: 150, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
        view.addSubview(circleContol)

        circleContol.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            circleContol.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            circleContol.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
}

PlaygroundPage.current.liveView = MyViewController()

我不能 运行 你的游乐场,但我可能有一些想法。

在您的组件中设置带有框架的元素的位置,这就是为什么 autoLayout 不会影响它,您应该添加约束。

而且你不应该在 updateConstraints 方法中设置你的约束,约束只需要设置一次,autoLayout 就可以工作。

尝试编辑:

private func addTextToView() {
    let label = CurvedTextView(frame: CGRect(x: 0, y: 0, width: 300, height: 300), title: title, r: textRadius, fontSize: fontSize, fontColour: textColour, location: .bottom)
    label.isUserInteractionEnabled = false
    label.backgroundColor = UIColor.clear
    label.translatesAutoresizingMaskIntoConstraints = false

    addSubview(label)

    NSLayoutConstraint.activate([
        label.topAnchor.constraint(equalTo: topAnchor),
        label.bottomAnchor.constraint(equalTo: bottomAnchor),
        label.trailingAnchor.constraint(equalTo: trailingAnchor),
        label.leadingAnchor.constraint(equalTo: leadingAnchor)
        ])
}

func addImageOverlay() {
    imageOverlay = UIImageView(frame: bounds)
    imageOverlay.tintColor = imageColour
    imageOverlay.contentMode = .scaleAspectFill
    addSubview(imageOverlay)
    imageOverlay.isUserInteractionEnabled = false
    imageOverlay.translatesAutoresizingMaskIntoConstraints = false

    NSLayoutConstraint.activate([
        imageOverlay.topAnchor.constraint(equalTo: topAnchor),
        imageOverlay.bottomAnchor.constraint(equalTo: bottomAnchor),
        imageOverlay.trailingAnchor.constraint(equalTo: trailingAnchor),
        imageOverlay.leadingAnchor.constraint(equalTo: leadingAnchor)
    ])
}

希望对您有所帮助


最终解决方案很简单

我需要做的就是确保 imageView 位于正确的位置。弯曲的文本和圆圈基于边界大小(等于 imageView 大小)。因此,一旦 imageView 位于其正确位置,圆圈和绘制的文本都会自动修复。

func addImageOverlay() {
    imageOverlay = UIImageView(frame: bounds)
    imageOverlay.tintColor = imageColour
    imageOverlay.contentMode = .scaleAspectFill
    addSubview(imageOverlay)
    imageOverlay.isUserInteractionEnabled = false
    imageOverlay.translatesAutoresizingMaskIntoConstraints = false

    NSLayoutConstraint.activate([
        imageOverlay.topAnchor.constraint(equalTo: topAnchor),
        imageOverlay.bottomAnchor.constraint(equalTo: bottomAnchor),
        imageOverlay.trailingAnchor.constraint(equalTo: trailingAnchor),
        imageOverlay.leadingAnchor.constraint(equalTo: leadingAnchor),
        imageOverlay.heightAnchor.constraint(equalToConstant: bounds.size.width),
        imageOverlay.widthAnchor.constraint(equalToConstant: bounds.size.width)
    ])
}