Best practice to subclass UIKit classes - ios

I'm using Swift, and I find myself having to subclass UIKit classes such as UIView and UIButton. I don't care about setting the frame since I'm using AutoLayout, so I don't want/need to use init(frame: CGRect).
class customSubclass: UIView {
var logo: UIImage
init(logo: UIImage) {
self.logo = logo
//compiler yells at me since super.init() isn't called before return from initializer
//so I end up doing this
super.init(frame: CGRectZero)
self.translatesAutoresizingMaskIntoConstraints = false
}
I also don't find it very sexy to set it's frame to CGRectZero.
Is there a way to having a custom initializer for a subclass of a UIView or UIButton without explicitly setting it's frame?
Note Every subclass is instantiated in code, so required init(coder: aDecoder) is in my code, but isn't actually doing anything.
Thanks!

During initialization of a subclass, you must call the designated initializer of a superclass. In your case, since you are creating these views programmatically, you must use super.init(frame: CGRect). As you mentioned, it would be useful to implement two designated initializers for your subclass, one of which takes in a frame:CGRect argument.
Please see the accepted answer to this question for a more thorough review.

If you're using autolayout in a storyboard or xib, then just override the init(coder:) method so that it calls the superclass's version, and have your convenience initializers pass CGRectZero or some other value in.

Instead of creating a new class and subclassing, you could try using extensions.
extension UIView {
var logo: UIImage = myImage
func myLogo() {
// code here
}
}

Related

Swift - Subclassed UICollectionViewCell not loading IBOutlet properties

I come from Obj-C and I'm struggling on doing something super basic in Swift!
I have a custom UICollectionViewCell:
class CustomCell: UICollectionViewCell
{
// Outlets
// ***************************
#IBOutlet weak var button: UIButton!
// Init
// ***************************
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
setup()
}
override init(frame: CGRect)
{
super.init(frame: frame)
setup()
}
func setup()
{
button.backgroundColor = .white
}
}
The cell is loaded from an external .xib file, so init(coder:) is called for the initialization but my button is not ready.
If I change to button?.backgroundColor the app doesn't crash but obviously nothing happen.
I can call my setup() function in layoutSubviews() and it works, but it's definitely not the right place to be.
How do I solve this massive problem? lol
Edit
Probably I have to call setup() from awakeFromNib(), right?
I usually don't use external .xib, I'm not familiar with them
Edit: Sorry It seems youe edited your question before my answer, it seems as you load it from XIB, then you can run the awakeFromNib which will be called when you register a nib using this method:
Apple Source UICollectionView
Apple Source UITableView
--- old post below ---
In Xcode 6 you have to provide additional init(coder:) initializer in
classes like RDCell, which is the subclass of UICollectionViewCell.
This initializer is called instead of init(frame:) when the class gets
initialized from a storyboard or a xib file. That’s not our case, but
we still need to provide init(coder:). We can use the solution
provided to us by Xcode. In Issue Navigator click on an error that
says “'required' initializer 'init(coder:)' must be provided by
subclass of 'UICollectionViewCell'“,
Source

How come I can initialize a UIView without parameters, but its documentation does not have an empty initializer?

let view = UIView()
Why does this compile without an error when the only documented UIView initializer is init(frame: CGRect)?
Specifically, I am trying to write a new class that inherits from UIView, but this code throws an error:
class SquadHorizontalScrollViewCell: UIView {
init(var: FIRDataSnapshot){
super.init()
....
It says it must call a designated initializer.
UIView inherits from UIResponder which inherits from NSObject.
The NSObject.init() initializer is accessible on UIView, but not on subclasses of NSObject which replace this designated initializer.
Let's consider an example.
class A: NSObject {
init(_ string: String) { }
}
This leads to a compiler error for let a = A() - missing argument #1 for initializer because this initializer replaces the designated init() initializer for NSObject in the subclass.
You just need to define the initializer of the subclass as a convenience initializer:
class A: NSObject {
convenience init(_ string: String) {
self.init() // Convenience initializers must call a designated initializer.
}
}
let a = A() now compiles.
UIView can also compile with other designated initializers defined in the subclass, since its designated initializer is not known at compile time. As per the docs, if instantiating programmatically, init(frame:) is the designated initializer, otherwise init() is the designated initializer. This means that UIView inherits the NSObject designated initializer rather than replacing it as in the above example.
In your example:
class SquadHorizontalScrollViewCell: UIView {
init(var: FIRDataSnapshot){
super.init()
We see that the designated initializer is init(frame: CGRect), so you have to call this designated initializer instead.
A designated initializer should call its superclass designated initializer.
In this case super.init() is the designated initializer of NSObject not UIView.
It would be UIView's responsibility to call UIResponder init ,I guess it has no designated initializer, hence UIView will call Super.init in its init(frame:CGrect) initializer. check "Initializer Delegation"
for why let x = UIView() is ok , its because of this
Unlike subclasses in Objective-C, Swift subclasses do not inherit
their superclass initializers by default. Swift’s approach prevents a
situation in which a simple initializer from a superclass is inherited
by a more specialized subclass and is used to create a new instance of
the subclass that is not fully or correctly initialized. (Apple)
since UIView is objective c class it still can do it. but you won't be able to call SquadHorizontalScrollViewCell() unless you did not provide any initializer or you overrides the designated initializer of the superclass (UIView)
Check this link for more info
For UIView init(frame: CGRect) is default initializer. You must call it after initialize your instance variable. If you take view from NIB then init?(coder aDecoder: NSCoder) is called instead of init(frame: CGRect). So in that case you have to initialize your instance variable in awakeFromNib() method. In this case your code should be like this:
class SquadHorizontalScrollViewCell: UIView {
init(firDataSnapshot: FIRDataSnapshot){
// intialize your instance variable
super.init(frame: CGRectZero) // set your expected frame. For demo I have set `CGRectZero`
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
For more info you can check this link https://developer.apple.com/reference/uikit/uiview
At a certain point your view will need to be init with something, that is why the compilation is complaining, because it cannot find how to start the initialisation of your custom view. Because at the end, a view will be init from a xib (init(coder aDecoder: NSCoder)), or from a frame ( init(frame: CGFrame)). So here, the easiest way is to call super.init(frame: CGRectZero) at least in your custom init method.
init (var: FIRDataSnapshot) {
super.init(frame: CGRectZero)
}
// This method below is always needed when you want to override your init method
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
but you'll still need to set the size of your frame etc.
You'll notice if you create your own UIView subclass and only override init(frame:) with a log statement, then instantiate this new class using just init(), your init(frame:) is actually called with a zero-sized frame. So the designated initializer is still getting called.

Initializing UIView init AFTER its superclass init?

Looking at the a lecture slide in the Stanford iOS 9 course here, he is creating a new UIView with two initializers (one if the UIView was created from storyboard, and one if it was created in code). The following code is written at the bottom of that particular slide:
func setup() {....} //This contains the initialization code for the newly created UIView
override init(frame: CGRect) { //Initializer if the UIView was created using code.
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) { //Initializer if UIView was created in storyboard
super.init(coder:aDecoder)
setup()
}
The rule is that you must initialize ALL of your own properties FIRST before you can grab an init from a superclass. So why is it that in this case he calls his superclass init super.init BEFORE he initializes himself setup()? Doesn't that contradict the following rule:
Safety check 1 A designated initializer must ensure that all of the properties introduced by its class are initialized before it delegates up to a superclass initializer.
As mentioned above, the memory for an object is only considered fully initialized once the initial state of all of its stored properties is known. In order for this rule to be satisfied, a designated initializer must make sure that all its own properties are initialized before it hands off up the chain.
I haven't seen all the rest of the code in this example, but the rule is only that your properties have to be initialized (i.e. the memory they occupy has to be set to some initial value) before calling super.init(), not that you can't run extra setup code.
You can even get away with sort of not-really-initializing your properties by either declaring your properties lazy var, or using var optionals which automatically initialize to nil. You can then set them after your call to super.init().
For example:
class Foo: UIView {
var someSubview: UIView! // initializes automatically to nil
lazy var initialBackgroundColor: UIColor? = {
return self.someSubview.backgroundColor
}()
init() {
super.init(frame: .zero)
setup() // do some other stuff
}
func setup() {
someSubview = UIView()
someSubview.backgroundColor = UIColor.whiteColor()
addSubview(someSubview)
}
}

UIStackView subclasses written in Swift may crash due to unimplemented init(frame:)

I have written a UIStackView subclass, but I am experiencing a strange run-time problem. Here is some sample code where it can be seen:
class SubclassedStackView: UIStackView {
init(text: String, subtext: String) {
let textlabel = UILabel()
let subtextLabel = UILabel()
textlabel.text = text
subtextLabel.text = subtext
super.init(arrangedSubviews: [textlabel, subtextLabel])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If you then use it such as this:
let stackView = SubclassedStackView(text: "Test", subtext: "Uh-oh!")
You get a runtime exception with the following message:
fatal error: use of unimplemented initializer 'init(frame:)' for class 'test.SubclassedStackView'
A look at the call stack shows that the base initializer -[UIStackView initWithArrangedSubviews:] is attempting to call init(frame: CGRect) on the subclass, which is intentionally left unimplemented.
Of course I could implement this extra initializer, weird as it would be for it to be called by the superclass, but in my real-life case this would force me to change my properties to use optional types (or implicitly unwrapped optionals) where I shouldn't have to do that.
I could also call init(frame:) instead of init(arrangedSubviews:) and subsequently call addArrangedSubview(view:) to add the arranged subviews. The run-time issue would disappear, but I don't wish to provide a frame.
Why does the superclass's initializer call the subclass's initializer? Can anyone suggest a way to work around this issue without introducing optionals?
Edit: Apple acknowledged this bug which should be fixed in iOS 10. http://www.openradar.me/radar?id=4989179939258368 Still applies to iOS 8-9 unfortunately.
I'm not sure if this will work for your needs, but I've managed to circumvent the problem with an extension on UIStackView:
extension UIStackView {
convenience init(text: String, subtext: String) {
let textlabel = UILabel()
let subtextLabel = UILabel()
textlabel.text = text
subtextLabel.text = subtext
self.init(arrangedSubviews: [textlabel, subtextLabel])
}
}
// ...
let sv = UIStackView(text: "", subtext: "") // <UIStackView: 0x7fcd32022c20; frame = (0 0; 0 0); layer = <CATransformLayer: 0x7fcd32030810>>
A look at the call stack shows that the base initialiser -[UIStackView initWithArrangedSubviews:] is attempting to call init(frame: CGRect) on the subclass, which is intentionally left unimplemented.
Why not just add the missing constructor?
override init(frame: CGRect) {
super.init(frame: frame)
}
The problem's coming from the frame initializer not being available since you've provided your own, and it needs that for its internal implementation. If the frame will be managed by AutoLayout anyways you don't need to be concerned with what it's actually set to initially, so you can just let it perform its internal routines necessary to initialize with your subviews. I don't see from the code above why you would need to add optionals..
init(arrangedSubviews: [] is a convenience initializer. As per documentation, you must call the superclass's designated initializer (which is init(frame:)) instead

Adding additional init code to an inherited initializer from superclass

I have a Swift UIView class (named HypnosisView) that draws a circle on the screen. The frame of the view is set to fill the screen. I would like to programmatically set the background color of the view upon initialization (so when an instance of the view is created it automatically has the specified background color). I was able to make this work with a convenience initializer, however I'm wondering if there is a more efficient way to do this (or if in fact I'm doing this correctly). In an ideal scenario, I would like to just add a piece of code that sets the background: self.background = UIColor.clearColor() to the inherited init(frame: CGRect) method, so I don't have to write a whole new initializer just to set the background color. Here is my convenience initializer method (what I'm currently using which works):
convenience init(rect: CGRect){
self.init(frame: rect)
self.backgroundColor = UIColor.clearColor()
}
and I call that method in the delegate like this:
var mainFrame = self.window!.bounds
var mainView = HypnosisView(rect: mainFrame)
Let me know if you have any questions. Thanks!
As discussed in the comments, when wanting to customize the behavior of a UIView, it's often easier to use a convenience initializer as opposed to overriding a designated initializer.
For UIViews specifically, if you override the designated init(frame aRect: CGRect), you are unfortunately also required to override init(coder decoder: NSCoder!) which is part of the NSCoding protocol. So generally if you just want to set a few properties to some default values, do as the original poster asked and create a convenience initializer that in turn calls init(frame aRect: CGRect):
convenience init(rect: CGRect, bgColor: UIColor){
self.init(frame: rect)
self.backgroundColor = bgColor
}
For a discussion on getting rid of NSCoding compliance, see Class does not implement its superclass's required members

Resources