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 andinit?(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:
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)
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)
}
}