Subclassing UIView in Swift

Provide the functionality that a standard UIView just can’t

Here are Apple’s Subclassing notes from their documentation:

The UIView class is a key subclassing point for visual content that also requires user interactions. Although there are many good reasons to subclass UIView, it is recommended that you do so only when the basic UIView class or the standard system views do not provide the capabilities that you need. Subclassing requires more work on your part to implement the view and to tune its performance.

Which is where many stackoverflow posts will point you to. This doesn’t necessarily help that many users, in the way that this guide will with a practical guide as to actually subclass a UIView with examples.

This guide to the rescue!

Image for post
Image for post
Photo by Kym Ellis on Unsplash

Difficulty: Beginner | Easy | Normal | Challenging

Prerequisites:

  • Some knowledge of OO would be useful (although I’ll take you through the basics here), and I will use Playgrounds (and include this in full at the end) but if you want to see the result of your work press one of the following symbols (the eye or the rectangle) to see it!
Image for post
Image for post

Terminology

Class: An object that defines properties and methods in common

Subclass: A class that inherits from a superclass

Superclass: The class that is being inherited from

There are a host of reasons why you might choose to subclass rather than extend, but one that is as good as any is if you are designing using a Storyboard you might love using @IBDesignable to make the storyboard work as part of the design process (It’s awesome, do it!).

In any case, Apple recommend you only subclass when the functionality of a standard UIView doesn’t give you the capabilities that you require.

Responsibilities of a UIView

UIViews have a large number of responsibilities. An (incomplete) list of these is:

  • Drawing (using UIKit or Core Graphics) and animation
  • The animation of a UIView
  • The management of layout and subviews (which can number zero or more), adjusting the size and position of subviews
  • Use Auto Layout to define the rules for resizing and repositioning the views in the view hierarchy
  • Event handling, and responding to touches and other events as a subclass of UIResponder or the installation of gesture recognizers to handle common gestures

It should be noted that the constraints for any specific view instance should be declared in the parent, that is they should be declared in the parent rather than the subview.

Creating a UIView

A red UIView

A red UIView can easily be created using UIView(frame:), and then later changing the background color by using the background instance property. In the playground this looks like the following:

Image for post
Image for post
Click for Gist

This is fine, but how would we create a UIView subclass that is always red?

A red UIView subclass

It seems like a reasonable amount of code for something so relatively trivial. However, this particular subclass can be initialised from the storyboard or code.

Image for post
Image for post
Click for Gist

clipsToBounds

“Setting this value to true causes subviews to be clipped to the bounds of the receiver. If set to false, subviews whose frames extend beyond the visible bounds of the receiver are not clipped. The default value is false.”

Frames and bounds

Each view is defined by both its frame and bounds properties. The frame of any iOS view is the expression of the origin and dimensions of the view in the coordinate system of the superview, while the bounds expresses the dimensions of the view in the coordinate system of that view. This means that you can never guarantee that the frame and bounds of a UIView are the same (and there are examples where they are not).

This leads to potential issues, as the initilizers init(frame: CGRect) and init?(coder: NSCoder) are called, and then the frame may later be changed through a resize. Therefore an init method is simply not he best place to perform layout changes on your UIView.

Setting the layout with constraints (Auto Layout)

If you use Auto Layout you can set constraints either in Interface builder or programatically. If setting them programatically you can add constraints in the initializer (here in a function called from the initializer, so in practical terms inside the initializer).

Image for post
Image for post
Click for Gist

Since we are using auto layout these constraints persist even through a resize or orientation change. Nice.

Setting the layout by overriding layoutSubviews

If you have complex setup that means constraint-based layout is not suitable for you, you can override layoutSubviews() and set the frame there.

Image for post
Image for post
Click for Gist

The view drawing cycle

The system works through the Run Loop, then Constraints before the Deferred Layout Pass, where views are updated if they are marked as dirty. The func draw(_:) method is at the heart of this, and is called by the system when it is first displayed and when a change is made to the visible part of the view.

Drawing a simple shape

We can override func draw(:) and we are given a context to draw into. Thanks Swift!

Image for post
Image for post
Click for Gist

why didn't you use bP.close()? In this case it’s not required, but I can see why some would want it there.

Equally we could use override func draw(_ rect: CGRect, for formatter: UIViewPrintFormatter) however I don’t want to draw differently when printing. Ok?

setNeedsDisplay() and setNeedsDisplay(_:)

setNeedsDisplay() forces a redraw of a particular view. Because you should never call draw(_ rect: CGRect) directly you can think of setNeedsDisplay() as a method of asking UIKit for a redraw. Another way of thinking of this is that calling setNeedsDisplay() marks a view as dirty, that is on the next update cycle the view will be redrawn through a deferred layout pass.

Since the pass is deferred, it will occur during the next update cycle during which func draw(_:) will be called on all such views.

This seems a little bewildering to the beginner, since most UI components take care of this for us. However, there can be a property that is not directly tied to a UI component and we need to inform Swift of what’s what with our call.

Image for post
Image for post
Click for Gist

and the following can be called with the following (setting the shapePoints property for the number of points for the polygon)

let shapeView = ShapeView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))shapeView.shapePoints = 1

Triggering layout refreshes

When a view changes the layout changes, and this requires a recalculation by autolayout.

Usually the layout is automatically updated, that is when a view is resized, added to the view hierarchy, constraints are updated, the device is rotated or the user scrolls.

There are situations where we need to force the layout of a particular view instance to be recalculated. and these are covered by the next couple of methods.

setNeedsLayout()

Asks for a layout update on a particular view. This will take place on the next update cycle, which due to the quick refresh of iOS device screens should be fast enough so the user does not experience any lag.

layoutIfNeeded()

This is similar to layoutIfNeeded() that forces an immediate update of the layout. One particular instance where this is used is for the animation of constraints, which due to animation would need to be immediately.

LayoutSubview

The layoutSubview method is called when the view resizes — including the very first time it is set.

This meant that it would be a suitable place to set corner radius of a UIView, or similar.

Rather than being called directly, this is triggered by the system when the view is first laid out (that is, on first draw) as well as on rotation. It can be requested with setNeedsLayout() for the next drawing update or forced immediately through layoutIfNeeded().

If you are working in a view controller viewDidLayoutSubviews() is your similar method here.

Intrinsic content size

It might be nice for any subclass to declare an intrinsicContentSize which would be a natural size for the subclass. For reference, the intrinsic size of a UILabel would be the size of the label with the text, giving auto layout something to work from.

Conclusion:

Subclassing a UIView is a common task for Swift developers, and you might as well get used to the methods that are relevant to subclassing a UIView.

The examples above should help the reader understand the way this can be done, and to extend your knowledge why not Read Apple’s documentation for UIView?

Any questions please do get in touch with me on Twitter.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store