IBDesignable 呈现在屏幕的左上角而不是它们的正确位置

IBDesignable rendered at the top left of the screen instead of their correct position

我在 Swift 中有一个复选框/单选按钮的自定义实现。 class 被注释为 @IBDesignable。我实现了 layoutSubviewsprepareForInterfaceBuilder.

此实现分布在多个应用程序中使用的 pod 中。我知道 Cocoapods 已经并且仍在导致 IB 行为中的一些不一致,但我不认为这是这里问题的一部分。

当我 运行 应用程序时一切正常。但是,当我使用界面构建器时,框总是呈现在父视图的左上角,而不是在它自己的视图中。

我该如何解决这个问题?我对 layoutSubviewsprepareForInterfaceBuilder 的实施是否相关?

编辑:我在这里做了一个示例项目:https://github.com/csarkis/TestIBDesignablesPod/

只需 运行 pod install 并打开项目。

这是可设计视图的简化再现:

class RadioButton: UIControl {
    /// Radio button radius
    private var radioButtonSize: CGFloat = 20

    /// A Boolean value that determines the off/on state of the radio button. Default is on
    @IBInspectable public var isOn: Bool = true { ... }

    ...

    // MARK: Initialisers
    /// :nodoc:
    public override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }

    /// :nodoc:
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupViews()
    }

    /// Setup the view initially
    private func setupViews() {
        self.subviews.forEach { [=10=].removeFromSuperview() }
        self.translatesAutoresizingMaskIntoConstraints = false
        self.autoresizesSubviews = false

        self.heightAnchor.constraint(equalToConstant: 40).isActive = true
        self.widthAnchor.constraint(equalToConstant: 40).isActive = true

        prepareForInterfaceBuilder()
        setNeedsDisplay()
    }

    /// Main color of the radio button element
    private var color: UIColor { ... }

    // MARK: View management
    // MARK: Custom drawings
    /// Width of the unchecked radio circle
    private var borderWidth: CGFloat = 2

    /// :nodoc:
    public override func draw(_ rect: CGRect) {
        super.draw(rect)

        let context = UIGraphicsGetCurrentContext()!
        context.setStrokeColor(self.color.cgColor)
        context.setFillColor(self.color.cgColor)
        context.setLineWidth(self.borderWidth)

        drawRadio(in: rect, for: context)
    }

    private func drawRadio(in rect: CGRect, for context: CGContext) {
        // Radio button
        // Draw inside the box, considering the border width
        let newRect = rect.insetBy(dx: (rect.width - radioButtonSize + borderWidth) / 2,
                                   dy: (rect.height - radioButtonSize + borderWidth) / 2)

        // Draw the outlined circle
        let borderCircle = UIBezierPath(ovalIn: newRect)
        borderCircle.stroke()
        context.addPath(borderCircle.cgPath)
        context.strokePath()
        context.fillPath()

        ...
    }

    /// :nodoc:
    public override func layoutSubviews() {
        super.layoutSubviews()

        self.setNeedsDisplay()
    }

    /// :nodoc:
    public override var intrinsicContentSize: CGSize {
        CGSize(width: 40, height: 40)
    }

    /// :nodoc:
    public override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        self.setNeedsDisplay()
    }

    ...
}

我对这个简单的 pod 的观察是行为非常不一致(很多崩溃和随机错误)。在这里,复选框首先呈现在左上角,并在我们修改任何属性后立即移动到正确的位置。 class 是 RadioButton.swift

问题是 translatesAutoresizingMaskIntoConstraints:视图(尤其是可设计的视图)永远不应为此设置自己的值 属性。

Interface Builder(或视图控制器,如果以编程方式进行)负责配置它。如果视图为子视图使用自动布局,它可以为其子视图设置 translatesAutoresizingMask 和约束(如果它有的话,在这种情况下你没有),但视图永远不应该设置自己的 translatesAutoresizingMaskIntoConstraints (也不添加自己的 width/height 约束)。

参见 https://github.com/csarkis/TestIBDesignablesPod/pull/1


其他一些建议:

  1. 您的 draw(_:) 正在使用提供的 CGRect 来确定单选按钮的大小和位置。

    不要那样做。始终使用 bounds 来计算事物的去向,而不是提供的 rect 参数。正如文档所说,矩形是

    The portion of the view’s bounds that needs to be updated. The first time your view is drawn, this rectangle is typically the entire visible bounds of your view. However, during subsequent drawing operations, the rectangle may specify only part of your view.

    因此,在复杂的视图中,我们可能会使用 rect 来确定 绘制了什么 (例如,如果 rect 不与您绘制的内容相交重新绘制,则不需要绘制它)。但永远不要使用 rect 来确定 在何处绘制 。使用 bounds.

  2. 在更激进的编辑中,您的 @IBDesignable 视图有一个 setupViews

    /// Setup the view initially
    private func setupViews() {
        self.subviews.forEach { [=10=].removeFromSuperview() }
        self.translatesAutoresizingMaskIntoConstraints = false
        self.autoresizesSubviews = false
    
        self.heightAnchor.constraint(equalToConstant: 40).isActive = true
        self.widthAnchor.constraint(equalToConstant: 40).isActive = true
    
        prepareForInterfaceBuilder()
        setNeedsDisplay()
    }
    

    你不应该在这个可设计的视图中做 任何

    • 您没有子视图,因此无需遍历并删除所有子视图(顺便说一句,这是一种极其轻率的做法,因为您不知道其他人可能添加了哪些子视图)。 autoresizesSubviews 也不是必须的。

    • 如上所述,视图(尤其是可设计的视图)永远不应设置自己的 translatesAutoresizingMaskIntoConstraints。这是将视图添加到视图层次结构(在本例中为 IB)的任何工作。你也不应该设置约束。

    • 您在这里呼叫prepareForInterfaceBuildersetNeedsDisplay。既不需要也不需要。

    现在您不再有 setupSubviews,现在不再需要一大堆其他观察器和覆盖,从而大大简化了代码。参见 https://github.com/csarkis/TestIBDesignablesPod/pull/2