Skip to content

Swift Views and UI — Building iOS Interfaces with UIKit

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Swift Views and UI. We cover key concepts, practical examples, and best practices to help you master this topic.

Swift views and UI development centers on UIKit, Apple's mature framework for building graphical interfaces where views are arranged in a hierarchy and managed by view controllers to create responsive, interactive applications.

What You'll Learn

  • UIView and UIViewController fundamentals
  • The view hierarchy and how views are laid out
  • Auto Layout constraints and UIStackView for responsive design
  • Building UI programmatically vs using Interface Builder
  • Handling user interactions with gestures and targets
  • Common UIKit patterns and best practices

Why It Matters

Every iOS, iPadOS, and tvOS app you build is a collection of views managed by view controllers. UIKit has been the foundation of iOS development since 2007 and is still widely used alongside SwiftUI. Understanding how views work — their lifecycle, layout, and interaction model — is essential for any Apple platform developer.

Real-World Use

Consider the Settings app on iOS. The entire interface is built from UITableView (for the scrolling list), UITableViewCell (for each row), UINavigationController (for push/pop navigation), and UIViewController subclasses that manage each settings screen. Every row tap triggers a navigation push to a new view controller.

Learning Path

flowchart LR
  A[Enums
Lesson 16] --> B[Views and UI
You are here] B --> C[Table and Collection Views
Lesson 18] B --> D[Navigation
Lesson 19] style B fill:#f90,color:#fff

UIView — The Building Block

UIView is the base class for all visible elements in UIKit. It manages a rectangular area, handles drawing and touch events, and participates in the view hierarchy.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let containerView = UIView(frame: CGRect(x: 20, y: 100, width: 300, height: 200))
        containerView.backgroundColor = UIColor.systemBlue
        containerView.layer.cornerRadius = 12
        containerView.layer.shadowColor = UIColor.black.cgColor
        containerView.layer.shadowOpacity = 0.3
        containerView.layer.shadowOffset = CGSize(width: 0, height: 4)
        containerView.layer.shadowRadius = 8
        view.addSubview(containerView)

        let label = UILabel(frame: CGRect(x: 16, y: 16, width: 268, height: 30))
        label.text = "Hello, UIKit!"
        label.textColor = UIColor.white
        label.font = UIFont.boldSystemFont(ofSize: 20)
        containerView.addSubview(label)

        let button = UIButton(type: .system)
        button.frame = CGRect(x: 16, y: 60, width: 268, height: 44)
        button.setTitle("Tap Me", for: .normal)
        button.backgroundColor = UIColor.white
        button.setTitleColor(.systemBlue, for: .normal)
        button.layer.cornerRadius = 8
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        containerView.addSubview(button)
    }

    @objc func buttonTapped() {
        print("Button was tapped!")
    }
}

// In a real app, this view controller would be set as the root
// let vc = ViewController()
// window.rootViewController = vc

Views are organized in a hierarchy. Each view has a superview and can have multiple subviews. The parent view (containerView) contains the label and button. When the parent is moved or hidden, all its children follow.

UIViewController — The Controller

UIViewController manages a set of views and coordinates the data flow between the model and the view. It has a well-defined lifecycle.

class ProfileViewController: UIViewController {

    private let nameLabel = UILabel()
    private let bioLabel = UILabel()
    private let avatarImageView = UIImageView()
    private let followButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        loadProfileData()
    }

    private func setupUI() {
        view.backgroundColor = .systemBackground

        avatarImageView.frame = CGRect(x: view.frame.midX - 40, y: 100, width: 80, height: 80)
        avatarImageView.backgroundColor = .systemGray
        avatarImageView.layer.cornerRadius = 40
        avatarImageView.clipsToBounds = true
        view.addSubview(avatarImageView)

        nameLabel.frame = CGRect(x: 20, y: 200, width: view.frame.width - 40, height: 30)
        nameLabel.textAlignment = .center
        nameLabel.font = UIFont.boldSystemFont(ofSize: 24)
        view.addSubview(nameLabel)

        bioLabel.frame = CGRect(x: 20, y: 240, width: view.frame.width - 40, height: 60)
        bioLabel.textAlignment = .center
        bioLabel.numberOfLines = 0
        bioLabel.textColor = .secondaryLabel
        view.addSubview(bioLabel)

        followButton.frame = CGRect(x: view.frame.midX - 60, y: 320, width: 120, height: 44)
        followButton.setTitle("Follow", for: .normal)
        followButton.backgroundColor = .systemBlue
        followButton.setTitleColor(.white, for: .normal)
        followButton.layer.cornerRadius = 22
        followButton.addTarget(self, action: #selector(followTapped), for: .touchUpInside)
        view.addSubview(followButton)
    }

    private func loadProfileData() {
        nameLabel.text = "Alice Johnson"
        bioLabel.text = "iOS developer. Coffee enthusiast. Building the future one app at a time."
    }

    @objc private func followTapped() {
        print("Followed Alice!")
    }
}

The view controller sets up its views in viewDidLoad. It owns the views and manages their content. In a production app, the data would come from a model or API.

Auto Layout

Frame-based layouts break on different screen sizes. Auto Layout uses constraints to define relationships between views.

class AutoLayoutViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let redView = UIView()
        redView.backgroundColor = .systemRed
        redView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(redView)

        let blueView = UIView()
        blueView.backgroundColor = .systemBlue
        blueView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(blueView)

        NSLayoutConstraint.activate([
            redView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            redView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            redView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            redView.heightAnchor.constraint(equalToConstant: 100),

            blueView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 20),
            blueView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            blueView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            blueView.heightAnchor.constraint(equalTo: redView.heightAnchor)
        ])
    }
}

Setting translatesAutoresizingMaskIntoConstraints = false is required when using Auto Layout. The constraints pin the views to the edges and define their sizes, adapting automatically when the device rotates or the screen size changes.

UIStackView

UIStackView simplifies horizontal or vertical layout by managing the distribution of its arranged subviews.

class StackViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 12
        stackView.alignment = .fill
        stackView.distribution = .fillEqually
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)

        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40),
            stackView.heightAnchor.constraint(equalToConstant: 200)
        ])

        let colors: [UIColor] = [.systemRed, .systemGreen, .systemBlue, .systemOrange]
        for color in colors {
            let subview = UIView()
            subview.backgroundColor = color
            subview.layer.cornerRadius = 8
            stackView.addArrangedSubview(subview)
        }
    }
}

The stack view automatically arranges the four colored views vertically with equal height and 12-point spacing. Changes in device orientation or screen size are handled automatically.

Handling User Interaction

UIKit provides several mechanisms for user interaction: target-action with buttons, gesture recognizers for more complex gestures, and touch methods for low-level control.

class InteractionViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        tapGesture.numberOfTapsRequired = 2
        view.addGestureRecognizer(tapGesture)

        let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
        swipeLeft.direction = .left
        view.addGestureRecognizer(swipeLeft)

        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
        longPress.minimumPressDuration = 1.0
        view.addGestureRecognizer(longPress)

        let toggle = UISwitch()
        toggle.isOn = true
        toggle.translatesAutoresizingMaskIntoConstraints = false
        toggle.addTarget(self, action: #selector(toggleChanged(_:)), for: .valueChanged)
        view.addSubview(toggle)

        NSLayoutConstraint.activate([
            toggle.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            toggle.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
        print("Double tap detected at \(gesture.location(in: view))")
    }

    @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
        print("Swiped left!")
    }

    @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        if gesture.state == .began {
            print("Long press started")
        }
    }

    @objc func toggleChanged(_ sender: UISwitch) {
        print("Toggle is now: \(sender.isOn)")
    }
}

Gesture recognizers decouple the gesture detection from the view, allowing multiple gestures on the same view and fine-grained control over recognition criteria.

View Controller Lifecycle

Understanding when view controller methods are called is critical for proper setup and teardown.

class LifecycleViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        print("View will appear")
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print("View did appear — start animations, network calls")
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        print("View will disappear")
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        print("View did disappear — stop timers, save state")
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        print("Layout about to update — adjust frames here")
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        print("Layout complete — final frame values available")
    }
}

Typical flow: viewDidLoad (once) -> viewWillAppear -> viewDidAppear -> viewWillDisappear -> viewDidDisappear. Each method has specific purposes for setup, animation, and cleanup.

Building UI Programmatically vs Storyboards

Modern iOS development increasingly uses programmatic UI for better code review, merge Conflict Resolution, and reusability.

class ProgrammaticViewController: UIViewController {

    private let titleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.preferredFont(forTextStyle: .largeTitle)
        label.textAlignment = .center
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private let actionButton: UIButton = {
        let button = UIButton(type: .system)
        button.configuration = .filled()
        button.configuration?.cornerStyle = .large
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupViews()
    }

    private func setupViews() {
        titleLabel.text = "Welcome to the App"
        view.addSubview(titleLabel)

        actionButton.setTitle("Get Started", for: .normal)
        actionButton.addTarget(self, action: #selector(getStarted), for: .touchUpInside)
        view.addSubview(actionButton)

        NSLayoutConstraint.activate([
            titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -60),
            titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),

            actionButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30),
            actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            actionButton.widthAnchor.constraint(equalToConstant: 200),
            actionButton.heightAnchor.constraint(equalToConstant: 50)
        ])
    }

    @objc private func getStarted() {
        print("Getting started...")
    }
}

The closure-based property initialization pattern keeps setup code close to the property declaration. Each view is a private let constant, ensuring the view hierarchy is only modified through constraints.

Common Mistakes

  1. Forgetting to set translatesAutoresizingMaskIntoConstraints = false: This is the most common Auto Layout mistake. Every view using constraints must have this set to false, or the constraints will conflict with the autoresizing mask.

  2. Memory cycles with closures: Strong reference cycles occur when a view controller holds a strong reference to a view or closure that captures self strongly. Use [weak self] in closures.

  3. Not calling super in lifecycle methods: Always call super.viewDidLoad(), super.viewWillAppear(animated), etc. at the appropriate time to ensure UIKit's internal setup runs correctly.

  4. Performing layout in viewDidLoad: Frame-based layout in viewDidLoad uses incorrect values because the view has not been sized yet. Use viewWillLayoutSubviews or viewDidLayoutSubviews for frame adjustments.

  5. Ignoring safe area insets: Views should not extend into the safe area (notch, home indicator, status bar). Use safeAreaLayoutGuide for constraints instead of the view's edges.

Practice Questions

  1. What is the difference between viewDidLoad and viewWillAppear?
  2. Why must you set translatesAutoresizingMaskIntoConstraints = false with Auto Layout?
  3. How does UIStackView simplify layout management?
  4. What is the view hierarchy and how do views relate to their superview?
  5. Challenge: Build a login screen with a username text field, password text field, and login button using Auto Layout. The fields should stack vertically, the button should be below them, and everything should adapt to any screen size.

Mini Project

Create a SettingsViewController with:

  • A vertical UIStackView containing rows for: Notifications (with UISwitch), Brightness (with UISlider), Language (with UIPickerView button)
  • Each row is a UIView containing a UILabel and the control
  • The stack view is pinned to safe area layout guide
  • The navigation bar has a "Save" button that prints all current values

FAQ

Should I use Storyboards or programmatic UI?

Both are valid. Programmatic UI gives better control, mergeable code, and easier code review. Storyboards offer visual feedback and faster prototyping for simple screens. Many teams use a hybrid approach.

What is the difference between frame and bounds?

Frame is the view's rectangle in its superview's coordinate system. Bounds is the view's rectangle in its own coordinate system. Frame determines position, bounds determines the drawing area.

How do I handle different screen sizes?

Use Auto Layout constraints with relative values (proportions, safe area guides) rather than fixed frames. UIStackView handles much of the adaptation automatically.

What is the safe area?

The safe area excludes the notch, status bar, home indicator, and navigation bars. Use view.safeAreaLayoutGuide to position content within the visible, unobstructed area.

When should I call super in viewDidLoad?

Always call super.viewDidLoad() first, before your own setup. This ensures UIKit's internal initialization runs before you start working with views.

What's Next

Learn how to build scrolling lists with Table and Collection Views, or master screen-to-screen transitions in the Navigation lesson.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro