3

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)

Rishil Patel
  • 1,977
  • 3
  • 14
  • 30
user8822312
  • 255
  • 4
  • 14

2 Answers2

2

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).

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I noticed if I comment out `setupFillView` method in `init?(coder:)` and leave the method in `init(frame:)`, the red circle will NOT be added when running the simulator. I may think it is because I create this instance of RecordView in the storyboard. However in Main.storyboard I can still see the red circle rendered and I don't know why it is the case. Also, if I delete the method in `init(frame:)` but leave it in `init?(coder:)`, it functions well in the simulator but in Main.storyboard the red circle is not there. – user8822312 Dec 03 '17 at 17:21
  • If you've added the view to a storyboard scene, `init?(coder:)` is used when you run the app. But, if that view is `IBDesignable`, when you preview the storyboard scene in IB, it will use `init(frame:)`. – Rob Dec 03 '17 at 17:42
  • @Rob Yes, IBDesignable means we are instantiating in code, after all (though it is Objective-C code behind the scenes, not our own explicit code), so it makes sense. It wasn't my goal to cover all cases, merely to respond to the general cry for understanding in the quoted sentence. – matt Dec 03 '17 at 17:46
  • Matt, my comment was for Yonliang, who was wondering why he sees it in IB when `init(coder:)` was commented out, but it disappears when `init(frame:)` is commented out. He must be doing `@IBDesignable` views. – Rob Dec 03 '17 at 17:51
  • It's my bad. It does be a @IBDesignable view. I once thought it was an irrelevant part and didn't post it. I've edited this question. Sorry about this. – user8822312 Dec 03 '17 at 18:43
1

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)
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Previously I do put these codes in layoutSubViews(). Then when I try to implement an animation where I shrink the red circle using UIView.animateWithScale, I got an unexpected result. Now I don't know where should I write the initialization code... [I also asked about this issue here](https://stackoverflow.com/questions/47615855/swift-uiview-animate-works-unexpectedly) – user8822312 Dec 03 '17 at 18:17
  • @YongliangHe - Basic configuration and adding of subviews belongs in `init` methods (or methods called from there). You put code that is contingent upon the size of the view in `layoutSubviews`. The `init` method is called once before the size has been determined. The `layoutSubviews` will be called when the size has been determined (and may be called several times). – Rob Dec 03 '17 at 18:21
  • could you please explain when would a view get its bounds value in its lifecycle? – user8822312 Dec 04 '17 at 19:11
  • @YongliangHe - When the view is first laid out (and every time it is updated), `layoutSubviews` is called. The `bounds` is not reliable at the time `init` is called, but it is when `layoutSubviews` is called. – Rob Dec 04 '17 at 19:14