ios: questions regarding init(frame:) and init?(coder:)

3.8k views Asked by At

Apple's tutorial describes the difference between init(frame:) and init?(coder:) as

You typically create a view in one of two ways: by programatically initializing the view, or by allowing the view to be loaded by the storyboard. There’s a corresponding initializer for each approach: init(frame:) for programatically initializing the view and init?(coder:) for loading the view from the storyboard. You will need to implement both of these methods in your custom control. While designing the app, Interface Builder programatically instantiates the view when you add it to the canvas. At runtime, your app loads the view from the storyboard.

I feel so confused by the description "programtically initializing" and "loaded by the storyboard". Say I have a subclass of UIView called MyView, does "programtically initialization" mean I write code to add an instance of MyView to somewhere like:

override func viewDidLoad() {      
        super.viewDidLoad()
        let myView = MyView()  // init(frame:) get invoked here??
}

while init?(coder:) get called when in Main.storyboard I drag a UIView from object library and then in the identity inspector I set its class to MyView?

Besides, in my xcode project, these two methods end up with different layout for simulator and Main.storyboard with the same code: enter image description here

import UIKit

@IBDesignable
class RecordView: UIView {

    @IBInspectable
    var borderColor: UIColor = UIColor.clear {
        didSet {
            self.layer.borderColor = borderColor.cgColor
        }
    }

    @IBInspectable
    var borderWidth: CGFloat = 20 {
        didSet {
            layer.borderWidth = borderWidth
        }
    }

    @IBInspectable
    var cornerRadius: CGFloat = 100 {
        didSet {
            layer.cornerRadius = cornerRadius
        }
    }

    private var fillView = UIView()

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupFillView()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupFillView()
    }



    private func setupFillView() {
        let radius = (self.cornerRadius - self.borderWidth) * 0.95
        fillView.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: radius * 2, height: radius * 2))
        fillView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
        fillView.layer.cornerRadius = radius
        fillView.backgroundColor = UIColor.red
        self.addSubview(fillView)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
    }

    func didClick() {
        UIView.animate(withDuration: 1.0, animations: {
            self.fillView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
        }) { (true) in
            print()
        }
    }
}

Why do they behave differently? (I drag a UIView from object library and set its class to RecordView)

2

There are 2 answers

4
Rob On BEST ANSWER

First your delineation between init?(coder:) and init(frame:) is basically correct. The former is used when instantiating a storyboard scene when you actually run the app, but the latter is used when you programmatically instantiate it with either let foo = RecordView() or let bar = RecordView(frame: ...). Also, init(frame:) is used when previewing @IBDesignable views in IB.

Second, regarding your problem, I'd suggest you remove the setting of the center of fillView (as well as the corner radius stuff) from setupFillView. The problem is that when init is called, you generally don't know what bounds will eventually be. You should set the center in layoutSubviews, which is called every time the view changes size.

class RecordView: UIView {  // this is the black circle with a white border

    private var fillView = UIView() // this is the inner red circle

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupFillView()
    }

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        setupFillView()
    }

    private func setupFillView() {
        fillView.backgroundColor = .red
        self.addSubview(fillView)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        let radius = (cornerRadius - borderWidth) * 0.95       // these are not defined in this snippet, but I simply assume you omitted them for the sake of brevity?
        fillView.frame = CGRect(origin: .zero, size: CGSize(width: radius * 2, height: radius * 2))
        fillView.layer.cornerRadius = radius
        fillView.center = CGPoint(x: bounds.midX, y: bounds.midY)
    }
}
5
matt On

I feel so confused by the description "programtically initializing" and "loaded by the storyboard".

Object-based programming is about classes and instances. You need to make an instance of a class. With Xcode, there are two broadly different ways to get an instance of a class:

  • your code creates the instance

  • you load a nib (such a view controller's view in the storyboard) and the nib-loading process creates the instance and hands it to you

The initializer that is called in those two circumstances is different. If your code creates a UIView instance, the designated initializer which you must call is init(frame:). But if the nib creates the view, the designated initializer that the nib-loading process calls is init(coder:).

Therefore, if you have a UIView subclass and you want to override the initializer, you have to think about which initializer will be called (based on how the view instance will be created).