iosNudge

Profile Header with Auto Layout

MediumUIKitUIKitAccessibility 40 min read

Build the top part of a profile screen in UIKit, like in a social app. Write the layout in code with Auto Layout. No storyboards, no frame math.

+------------------------------------------+
|  (avatar)   Anna Lindqvist               |
|             @anna.codes                  |
|                                          |
|  iOS developer in Stockholm. I write     |
|  about SwiftUI, Auto Layout and good...  |
|                                          |
|  [            + Follow               ]   |
+------------------------------------------+
final class ProfileHeaderView: UIView {
    var onFollowTapped: (() -> Void)?
    func configure(with profile: Profile)
}

final class ProfileViewController: UIViewController {
    init(profile: Profile = .sample)
}

Requirements

  • A round avatar (use the SF Symbol person.crop.circle.fill), always square.
  • Name (bold) and username next to the avatar. The bio below. A full-width Follow button at the bottom.
  • The button toggles between "Follow" and "Following" and changes its style.
  • Every label wraps to as many lines as it needs. Nothing is cut with "…".
  • Dynamic Type: all text grows with the user's text size, live, without a restart.
  • At the accessibility text sizes, put the name and username under the avatar. There is not enough width next to it.
  • Rotation: in landscape, nothing goes under the notch or the Dynamic Island.
  • On iPad, the text lines do not get too long.
  • If the text is too big for the screen, the user can scroll.

What a good answer handles

  • Priorities. The avatar must not shrink when the name is long. Explain hugging and compression resistance.
  • Custom fonts and Dynamic Type. A bold name font that still scales.
  • No conflicts. No "Unable to simultaneously satisfy constraints" logs in any size or orientation.
  • VoiceOver. The name is a heading. The avatar is not read as "image".
  • No fixed sizes from the screen. Do not use UIScreen.main.bounds.

Notes

  • Target iOS 17 or later. registerForTraitChanges is the iOS 17 way to react to trait changes.
  • Test with Xcode's Environment Overrides (text size) and by rotating the simulator.

Starter code

Copy this into a new Xcode project (or Swift Playgrounds) and build from here.

import UIKit

// MARK: - Model (done for you)

struct Profile {
    let name: String
    let username: String
    let bio: String
    var isFollowing: Bool
}

extension Profile {
    static let sample = Profile(
        name: "Anna Lindqvist",
        username: "@anna.codes",
        bio: "iOS developer in Stockholm. I write about SwiftUI, Auto Layout and good coffee. Talks, notes and side projects are linked below.",
        isFollowing: false
    )
}

// MARK: - Header view

final class ProfileHeaderView: UIView {
    var onFollowTapped: (() -> Void)?

    private let avatarView = UIImageView(image: UIImage(systemName: "person.crop.circle.fill"))
    private let nameLabel = UILabel()
    private let usernameLabel = UILabel()
    private let bioLabel = UILabel()
    private let followButton = UIButton(configuration: .filled())

    override init(frame: CGRect) {
        super.init(frame: frame)
        // TODO: style the views and build the layout with Auto Layout.
        // Support Dynamic Type. At accessibility text sizes, put the name under the avatar.
    }

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

    func configure(with profile: Profile) {
        // TODO: set the texts and the Follow / Following button state.
    }
}

// MARK: - View controller

final class ProfileViewController: UIViewController {
    private var profile: Profile
    private let header = ProfileHeaderView()

    init(profile: Profile = .sample) {
        self.profile = profile
        super.init(nibName: nil, bundle: nil)
    }

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

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Profile"
        view.backgroundColor = .systemBackground
        header.configure(with: profile)
        // TODO: place the header (it must work in landscape and with huge text)
        // and handle the follow button.
    }
}

#Preview("Portrait") {
    UINavigationController(rootViewController: ProfileViewController())
}

#Preview("Landscape", traits: .landscapeLeft) {
    UINavigationController(rootViewController: ProfileViewController())
}

Solution

Try to build it first. Then compare your design with ours.