Home » Ios » Swift subclass UIView

Swift subclass UIView

Posted by: admin November 30, 2017 Leave a comment

Questions:

I’ve just started using Swift. I’ve read the book and I learn better while doing so I started something simple.

I want to subclass UIView and show a login like view. I’ve created this in Objective-C but now I want to port it to Swift. I do not use storyboards, so I create all my UI in code.

But the first problem is that I must implement initWithCoder. I gave it a default implementation since It won’t be called. Now when I run the program it crashes because I’ve to implement initWithFrame as well. Now I got this:

override init() {
    super.init()
    println("Default init")
}

override init(frame: CGRect) {
    super.init(frame: frame)
    println("Frame init")
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    println("Coder init")
}

My question is where should I create my textfield etc… and if I never implement frame and coder how can I “hide” this?

Answers:

I usually do something like this, its a bit verbose.

class MyView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        addBehavior()
    }

    convenience init() {
        self.init(frame: CGRect.zero)
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("This class does not support NSCoding")
    }

    func addBehavior() {
        print("Add all the behavior here")
    }
}



let u = MyView(frame: CGRect.zero)
let v = MyView()

(Edit: I’ve edited my answer so that the relation between the initializers is more clear)

Questions:
Answers:

This is more simple.

override init (frame : CGRect) {
    super.init(frame : frame)
    // Do what you want.
}

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

Questions:
Answers:

Custom UIView Subclass Example

I usually create iOS apps without using storyboards or nibs. I’ll share some techniques I’ve learned to answer your questions.

 

Hiding Unwanted init Methods

My first suggestion is to declare a base UIView to hide unwanted initializers. I’ve discussed this approach in detail in my answer to “How to Hide Storyboard and Nib Specific Initializers in UI Subclasses”. Note: This approach assumes you will not use BaseView or its descendants in storyboards or nibs since it will intentionally cause the app to crash.

class BaseView: UIView {

    // This initializer hides init(frame:) from subclasses
    init() {
        super.init(frame: CGRect.zero)
    }

    // This attribute hides `init(coder:)` from subclasses
    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    }
}

Your custom UIView subclass should inherit from BaseView. It must call super.init() in its initializer. It does not need to implement init(coder:). This is demonstrated in the example below.

 

Adding a UITextField

I create stored properties for subviews referenced outside of the init method. I would typically do so for a UITextField. I prefer to instantiate subviews within the declaration of the subview property like this: let textField = UITextField().

The UITextField will not be visible unless you add it to the custom view’s subview list by calling addSubview(_:). This is demonstrated in the example below.

 

Programmatic Layout Without Auto Layout

The UITextField will not be visible unless you set its size and position. I often do layout in code (not using Auto Layout) within the layoutSubviews method. layoutSubviews() is called initially and whenever a resize event happens. This allows adjusting layout depending on the size of CustomView. For example, if CustomView appears the full width on various sizes of iPhones and iPads and adjusts for rotation, it needs to accommodate many initial sizes and resize dynamically.

You can refer to frame.height and frame.width within layoutSubviews() to get the CustomView’s dimensions for reference. This is demonstrated in the example below.

 

Example UIView Subclass

A custom UIView subclass containing a UITextField which does not need to implement init?(coder:).

class CustomView: BaseView {

    let textField = UITextField()

    override init() {
        super.init()

        // configure and add textField as subview
        textField.placeholder = "placeholder text"
        textField.font = UIFont.systemFont(ofSize: 12)
        addSubview(textField)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // Set textField size and position
        textField.frame.size = CGSize(width: frame.width - 20, height: 30)
        textField.frame.origin = CGPoint(x: 10, y: 10)
    }
}

 

Programmatic Layout with Auto Layout

You can also implement layout using Auto Layout in code. Since I don’t often do this, I will not show an example. You can find examples of implementing Auto Layout in code on Stack Overflow and elsewhere on the Internet.

 

Programmatic Layout Frameworks

There are open source frameworks that implement layout in code. One I am interested in but have not tried is LayoutKit. It was written by the development team an LinkedIn. From the Github repository: “LinkedIn created LayoutKit because we have found that Auto Layout is not performant enough for complicated view hierarchies in scrollable views.”

 

Why put fatalError in init(coder:)

When creating UIView subclasses that will never be used in a storyboard or nib, you might introduce initializers with different parameters and initialization requirements that could not be called by the init(coder:) method. If you did not fail init(coder:) with a fatalError, it could lead to very confusing problems down the line if accidentally used in a storyboard/nib. The fatalError asserts these intentions.

required init?(coder aDecoder: NSCoder) {
    fatalError("NSCoding not supported")
}

If you want to run some code when the subclass is created regardless of whether it is created in code or a storyboard/nib then you could do something like the following (based on Jeff Gu Kang’s answer)

class CustomView: UIView {
    override init (frame: CGRect) {
        super.init(frame: frame)
        initCommon()
    }

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

    func initCommon() {
        // Your custom initialization code
    }
}

Questions:
Answers:

It’s important that your UIView can be created by interface builder/storyboards or from code. I find it’s useful to have a setup method to reduce duplicating any setup code. e.g.

class RedView: UIView {
    override init (frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        setup()
    }

    func setup () {
        backgroundColor = .red
    }
}

Questions:
Answers:

Here’s an example of how I usually build my subclasses(UIView). I have the content as variables so they can be accessed and tweaked maybe later in some other class. I’ve also shown how I use auto layout and adding content.

For example in a ViewController I have this view initialized In ViewDidLoad() since that is only called once when the view is visible. Then I use these functions I make here addContentToView() and then activateConstraints() to build the content and set constraints. If I later in a ViewController want the color of let’s say a button to be red, I just do that in that specific function in that ViewController.
Something like: func tweaksome(){ self.customView.someButton.color = UIColor.red}

class SomeView: UIView {


var leading: NSLayoutConstraint!
var trailing: NSLayoutConstraint!
var bottom: NSLayoutConstraint!
var height: NSLayoutConstraint!


var someButton: UIButton = {
    var btn: UIButton = UIButton(type: UIButtonType.system)
    btn.setImage(UIImage(named: "someImage"), for: .normal)
    btn.translatesAutoresizingMaskIntoConstraints = false
    return btn
}()

var btnLeading: NSLayoutConstraint!
var btnBottom: NSLayoutConstraint!
var btnTop: NSLayoutConstraint!
var btnWidth: NSLayoutConstraint!

var textfield: UITextField = {
    var tf: UITextField = UITextField()
    tf.adjustsFontSizeToFitWidth = true
    tf.placeholder = "Cool placeholder"
    tf.translatesAutoresizingMaskIntoConstraints = false
    tf.backgroundColor = UIColor.white
    tf.textColor = UIColor.black
    return tf
}()
var txtfieldLeading: NSLayoutConstraint!
var txtfieldTrailing: NSLayoutConstraint!
var txtfieldCenterY: NSLayoutConstraint!

override init(frame: CGRect){
    super.init(frame: frame)
    self.translatesAutoresizingMaskIntoConstraints = false
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    //fatalError("init(coder:) has not been implemented")
}



/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
    // Drawing code

}
*/
func activateConstraints(){
    NSLayoutConstraint.activate([self.btnLeading, self.btnBottom, self.btnTop, self.btnWidth])
    NSLayoutConstraint.activate([self.txtfieldCenterY, self.txtfieldLeading, self.txtfieldTrailing])
}

func addContentToView(){
    //setting the sizes
    self.addSubview(self.userLocationBtn)

    self.btnLeading = NSLayoutConstraint(
        item: someButton,
        attribute: .leading,
        relatedBy: .equal,
        toItem: self,
        attribute: .leading,
        multiplier: 1.0,
        constant: 5.0)
    self.btnBottom = NSLayoutConstraint(
        item: someButton,
        attribute: .bottom,
        relatedBy: .equal,
        toItem: self,
        attribute: .bottom,
        multiplier: 1.0,
        constant: 0.0)
    self.btnTop = NSLayoutConstraint(
        item: someButton,
        attribute: .top,
        relatedBy: .equal,
        toItem: self,
        attribute: .top,
        multiplier: 1.0,
        constant: 0.0)
    self.btnWidth = NSLayoutConstraint(
        item: someButton,
        attribute: .width,
        relatedBy: .equal,
        toItem: self,
        attribute: .height,
        multiplier: 1.0,
        constant: 0.0)        


    self.addSubview(self.textfield)
    self.txtfieldLeading = NSLayoutConstraint(
        item: self.textfield,
        attribute: .leading,
        relatedBy: .equal,
        toItem: someButton,
        attribute: .trailing,
        multiplier: 1.0,
        constant: 5)
    self.txtfieldTrailing = NSLayoutConstraint(
        item: self.textfield,
        attribute: .trailing,
        relatedBy: .equal,
        toItem: self.doneButton,
        attribute: .leading,
        multiplier: 1.0,
        constant: -5)
    self.txtfieldCenterY = NSLayoutConstraint(
        item: self.textfield,
        attribute: .centerY,
        relatedBy: .equal,
        toItem: self,
        attribute: .centerY,
        multiplier: 1.0,
        constant: 0.0)
}
}