I have a custom implementation of a checkbox / radio button in Swift.
The class is annotated as @IBDesignable. I implement layoutSubviews and prepareForInterfaceBuilder.
This implementation is distributed in a pod that is used in several applications. I know that Cocoapods has caused and is still causing some inconsistencies in the IB behavior, but I don't believe that it is part of the issue here.
Everything is fine when I run the application. However, when I use the interface builder, the box is always rendered on the top left of the parent view instead of being in its own view.
How can I fix this? Is my implementation of layoutSubviews and prepareForInterfaceBuilder relevant?
EDIT: I made a sample project here: https://github.com/csarkis/TestIBDesignablesPod/
Just run pod install and open the project.
Here is a simplified rendition of the designable view:
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 { $0.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()
}
...
}
My observation on this simple pod is that the behavior is very inconsistant (a lot of crashes and random errors). Here, the checkbox is first rendered on the top left and moves to its correct position as soon as we modify any attribute. The class is RadioButton.swift.
The issue is
translatesAutoresizingMaskIntoConstraints: A view (esp a designable one) should never set its own value for this property.Interface Builder (or the view controller, if doing it programmatically) is responsible for configuring this. A view can set
translatesAutoresizingMaskand constraints for its subviews (if it had any, which you do not in this case) if it is using auto layout for subviews, but a view should never set its owntranslatesAutoresizingMaskIntoConstraints(nor add its own width/height constraints).See https://github.com/csarkis/TestIBDesignablesPod/pull/1.
A few other suggestions:
Your
draw(_:)is using suppliedCGRectto determine the size and placement of the radio button.Do not do that. Always use
boundsfor computing where things go, not the suppliedrectparameter. As the documentation says, the rect isSo, in complicated views, we might use
rectto determine what is drawn (e.g. ifrectdoesn't intersect with what you're drawing, you do not need to draw it). But never userectto determine where it is drawn. Usebounds.In a more radical edit, you have a
setupViewsfor your@IBDesignableview:You should not be doing any of that in this designable view:
You have no subviews, so iterating through and deleting all subviews is unnecessary (and that is an extremely imprudent practice, btw, as you do not know what subviews someone else may have added). Nor is
autoresizesSubviewsnecessary.As described above, a view (esp a designable one) should never set its own
translatesAutoresizingMaskIntoConstraints. That is the job of whatever added the view to the view hierarchy (IB in this case). Nor should you set the constraints.You are calling
prepareForInterfaceBuilderandsetNeedsDisplayhere. Neither is needed nor desired.Now that you no longer have
setupSubviews, a whole bunch of other observers and overrides are now no longer necessary, greatly simplifying the code. See https://github.com/csarkis/TestIBDesignablesPod/pull/2.