iosxcodeuiviewinitwithcoderinitwithframe

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


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)


Solution

  • 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)
        }
    }