6

I have a custom UIControl, and I implement as:

required init(coder: NSCoder) {
    super.init(coder: coder)
    initSubComponents()
}

func initSubComponents() {
    // Display UIControl border for visual debugging
    self.layer.borderColor = UIColor.redColor().CGColor
    self.layer.borderWidth = 3.0

    subviewContainer = UIView(frame: self.bounds.rectByInsetting(dx: 0, dy: 0))
    // Display container border for visual debugging
    subviewContainer.layer.borderColor = UIColor.redColor().CGColor
    subviewContainer.layer.borderWidth = 3.0

    println("UIControl frame: \(self.frame)")
    println("subviewContainer frame: \(subviewContainer.frame)")
}

or

override func drawRect(rect: CGRect) {
    initSubComponents()
    // Code to add those subviews into this UIControl
}

func initSubComponents() {
    // Display UIControl border for visual debugging
    self.layer.borderColor = UIColor.redColor().CGColor
    self.layer.borderWidth = 3.0

    subviewContainer = UIView(frame: self.bounds.rectByInsetting(dx: 0, dy: 0))
    // Display container border for visual debugging
    subviewContainer.layer.borderColor = UIColor.redColor().CGColor
    subviewContainer.layer.borderWidth = 3.0

    println("UIControl frame: \(self.frame)")
    println("subviewContainer frame: \(subviewContainer.frame)")
}

I found a situation that I don't understand: the frame I got from the above 2 different approaches are different! Why? The first approach should be better, cause I should not init in override func drawRect(rect: CGRect), however I got the exact frame I expect in the second approach, not the first approach!

Stephen Kuo
  • 1,175
  • 3
  • 11
  • 19
  • You should never initialise things in drawRect, you should only draw things there, as this method get called many times (first display, when these methods are called `setNeedsDisplay`, `setNeedsDisplayInRect`). You should initialise things in `init()` – Marius Fanu Apr 17 '15 at 06:29
  • Also, if you have subviews for the content you shouldn't really need drawRect. – Wain Apr 17 '15 at 06:51
  • To be more clear, I modify my code, sorry. Same question, I still don't get it. – Stephen Kuo Apr 17 '15 at 08:04

2 Answers2

3

This is happening because on init the control is getting it's frame from the storyboard/nib. The size of the view in the storyboard/nib can be different than the size on a device until it is laid out for the first time.

As people in the comments have stated drawRect is constantly being called, and so it has the correct frame because the control has already been laid out. But this is the wrong place to initialize sub components. Also as the default implementation states, unless you are actually drawing something in drawRect you shouldn't use it since it adversely affects performance.

There are a couple of solutions to you your problem I can think of right now:

  1. Initialize the sub components in the initWithCoder method with constraints and rely on autolayout updating everything when the control is laid out.
  2. Use a flag to initialize the sub components only once. Wait for the first layout in layoutSubviews and initialize from there.
Nikola Lajic
  • 3,985
  • 28
  • 34
  • Thanks, you point me to the right direction, `layoutSubviews` solves my problem. In my understanding of `layoutSubviews`, it is called only when superview is resized, so if the superview is not resized, this method is called just once, right? – Stephen Kuo Apr 17 '15 at 08:20
  • `layoutSubiews` can be triggered by many things such as animations and transitions. – Nikola Lajic Apr 17 '15 at 08:21
0

A good solution would be adding auto layout constraints to your subviewContainer, as constraints are updated automatically every time self.frame changes.

Your code would be like this:

required init(coder: NSCoder) {
    super.init(coder: coder)
    initSubComponents()
}

func initSubComponents() {

    // Original code, I removed debugging code for easier reading.
    subviewContainer = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))

    //Where the magic happens:
    subviewContainer.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint(item: subviewContainer, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1.0, constant: 0).isActive = true
    NSLayoutConstraint(item: subviewContainer, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
    NSLayoutConstraint(item: subviewContainer, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: subviewContainer, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0).isActive = true
}

There are more ways to add auto layout constraints, click to find out here.

It's a bit more code, but general a much better way to deal with this problem, as there are situations when drawRect won't come to the rescue.

Community
  • 1
  • 1
Bright
  • 5,699
  • 2
  • 50
  • 72