Link Search Menu Expand Document

UIKit Continued - Custom View and App Life Cycle

Michael Lin

Table of Contents

  1. UIKit Continued - Custom View and App Life Cycle
  2. Custom View
    1. Constraint Priority
    2. Intrinsic Content Size
    3. Content Hugging and Compression Resistance
    4. Creating a Custom View
  3. App’s Life Cycle
    1. Background
    2. Using Life Cycle Methods

Custom View

Right now you are probably familiar with customizing your UI through tinkering the properties of some build-in elements. Custom views take it one step further by subclassing and composing one or more views together into a new component. It enables you to encapsulate a complex view hierarchy into a single view object, improving code reusability and readability.

Auth Text Field in MP3

Above is a custom view that was implemented in the Project 3 starter code, it includes a UILabel and a UITextField for user input. We can instantiate such view through its custom initializer.

AuthTextField(title: "Email:")

Custom views unlock a whole bunch of ways to customize our UI, and it’s used in almost every real world iOS project. But before we can dive into how to create them, we will need to learn a few new auto layout concepts.

Constraint Priority

Every constraint is associated with a priority. By default, the constraints you create have the maximum priority required.

Constraint Priority in Xcode

Notice the @1000 at the end of each constraint on the left. It is the raw value of the priority of that constraint, which is a Float ranging from 0 to 1000. Constraints with a priority of 1000 are the ones that are required. Anything below 1000 means the constraint is optional.

This is also why constraint conflicts occur: it happens when auto layout cannot satisfy all the required constraints simultaneously. Conversely, for constraints with less than required priority level, it can break without triggering a conflict. Between contraints with non-required priority, the one with lower priority will be dropped first.

Optional constraints are a powerful tool for creating flexible UI. One thing I always like to do is to think of them as springs. They will push or pull the UI towards their defined length or position, and will give way to other constraints with higher priority, which allows you to specify the order of which different parts of the UI extend or compress.

To set the priority of a constraint, use the priority variable defined under NSLayoutConstraint

bottomInsetConstraint.priority = .init(rawValue: 700)

Although you can manually create a priority using its initializer and raw value, more often we will choose from one of the predefined values such as .defaultHigh (rawValue = 750). This gives us some consistency which setting up constraint priorities.

bottomInsetConstraint.priority = .defaultHigh

Intrinsic Content Size

So far you might have discovered that not always do we have to set constraints on the size of a view.

MP3 iOS Sign In Screen

For example, in the Project 3 starter code, we didn’t define the height of any of the controls. Yet the views still automatically extend to fit their content. This is because certain views such as labels and buttons have a natural size which can be inferred from their content and attributes. This is referred to as intrinsic content size. It tells auto layout what is its preferred size based on the current content. For example, a button’s preferred size is the size of its title plus a small padding on each side.

Intrinsic Content Size for Common Controls

You can override the default computation of a view’s intrinsic content size if you want

override var intrinsicContentSize: CGSize {
    // Calculate w and h
    return CGSize(width: w, height: h)
}

Then call invalidateIntrinsicContentSize() whenever something changes that affects the intrinsic content size. The method will notify Auto Layout to recalculate the frame based on the new content size.

Content Hugging and Compression Resistance

Behind the scene, intrinsic content size is translated into two pairs of optional inequality constraints known as content hugging and compression resistance constraints.

view.height >= intrinsicContentSize.height @750
view.width >= intrinsicContentSize.width @750

view.height <= intrinsicContentSize.height @250
view.width <= intrinsicContentSize.width @250

Both the compression resistance constraints have priority defaultHigh at 750, whereas the two content hugging constraints have priority defaultLow at 250. Intuitively, this means that it’s easier to stretch a view than it is to shrink it, which is what you want most of the time.

You can set the priority of any of the four constraints using the setContentCompressionResistancePriority(_:for:) and setContentHuggingPriority(_:for:) methods. For example

view.setContentHuggingPriority(.init(rawValue: 249), for: .vertical)

This sets the view’s content hugging priority to be slightly lower than that of the others’. So when auto layout solve for the constraint system, this view will be the first to expand.

Understanding these concepts can help you leverage the layout process and is crucial to creating a highly robust and reusable views.

Creating a Custom View

Custom views are created through subclassing an existing view. It could be any known UIView subclass such as the build-in controls like UILabel or UIButton, container views UIStackView, or even another custom view you defined. We will use the AuthTextField from MP3 starter as our example.

final class AuthTextField: UIView { ... }

AuthTextField(title: "Email:")

Auth Text Field in MP3

App’s Life Cycle

In this section we will bring our attention to the two files that we’ve been kind of ignoring so far: AppDelegate and SceneDelegate. These are the files that are responsible for responding to the transition events between states of the app, known as life cycles.

App Delegate in iOS

At a high level, their job is to inform you of any transitions between states of the app so that you can perform some sort of life cycle task. One common use case is for saving the app’s state beforing entering the background.

Managing your App's Life Cycle

Background

Before iOS 12, each app can only have one instance running on foreground at any given time. So UIKit delivers all life-cycle events to the UIApplicationDelegate object in AppDelegate.swift. It works with UIApplication and you can think of it as the “root” of the App. Previously, the app delegate would have access to a UIWindow object where the app’s root view controller will reside. But on iOS 12 Apple added a new feature that allows multiple instances of your app’s UI to run simultaneously.

App Delegate in iOS

This means that an app may have to manage more than one life cycles and windows at the same time, which is why after iOS 12, we have a new concept called scene. A scene manages an instance of your app’s UI and its life cycle just as the app delegate did before. In other words, We still only have one “app”, but now we can run multiple “scenes” under the same app.

Using Life Cycle Methods

Despite the convoluted system of app delegate and scenes, using their life cycle methods is pretty straightforward. Just put the code that you want to execute in the corresponding life cycle methods, and it will be triggered when the corresponding events occur. One of the methods that you might’ve seen being used is scene(_:willConnectTo:options:). We implemented a simple check in MP3 to see if an user has already logged in.

class SceneDelegate: UIResponder, UIWindowSceneDelegates {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.windowScene = windowScene
        if FIRAuthProvider.shared.isSignedIn() {
            let vc = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
            window?.rootViewController = vc
        } else {
            let vc = UIStoryboard(name: "Auth", bundle: nil).instantiateInitialViewController()
            window?.rootViewController = vc
        }
        window?.makeKeyAndVisible()
    }

    /* ... */
}