SwiftUI Interview Questions: Views & Fundamentals

Reviewed by Mark Dickie · Last updated

2 questions on this page you can answer and see marked on the spot. Go to the first one

SwiftUI views are declarative, value-typed structures conforming to the View protocol that describe what a screen should look like for a given state. For an interview, you should be able to explain how the body property produces the view hierarchy, how SwiftUI diffs that hierarchy to update the screen, and how property wrappers such as @State and @Binding propagate changes. Expect questions on view identity, modifier order, container layout, and the difference between @State, @ObservedObject, and @EnvironmentObject.

Property WrapperWhat It HoldsWho Owns the State
@StateSimple local values (strings, ints, bools)The current view
@BindingA read-write reference to a parent's @StateThe parent view
@ObservedObjectA reference to an external ObservableObjectAnother part of the app
@EnvironmentObjectShared state injected through the environmentAn ancestor view
  1. Every View requires a body computed property returning some View; the compiler infers the concrete type at build time.
  2. Modifiers wrap the view they are applied to, so .padding().background() and .background().padding() produce different visual results.
  3. VStack, HStack, and ZStack arrange children along one axis; List and ScrollView add scrolling, and Form groups input controls.
  4. some View hides the exact return type while guaranteeing a single concrete type per call site.

What does a SwiftUI interview test on views?

Most rounds check whether you can build a custom view from scratch, explain how SwiftUI re-renders when state changes, and reason about view identity with id(_:) or ForEach. You may be asked to trace a data flow from a model object through @Published and into the view tree, or to describe what happens when a View struct is recreated on every state update while its underlying state persists.

How should you think about view modifiers and layout?

Modifiers in SwiftUI are not in-place mutations. Each one returns a new opaque view that wraps the previous one, which is why order changes the output. For layout, the system measures children, negotiates sizes through ViewThatFits and spacer behavior, and positions content according to the container's alignment guides. Knowing where Spacer distributes free space and how frame(width:height:) constrains measurement is fair game.

Key facts

  • Tarmac's Swift interview questions cover 13 questions at difficulty 1–4 of 5.
  • Tarmac tracked 157 job postings asking for Swift in September 2026.
  • Roles asking for Swift advertise a median base salary of US$169,050, across 45 job postings as of September 2026.
  • Tarmac last reviewed these Swift interview questions on 14 October 2026.

At a glance

Questions13
Difficulty1–4 of 5
FormatsTrue / false, Short answer, Find the bug, Code output

What you'll review

  1. swiftui views

Practice questions

Try one before you open the answer. Pick an option and press Check; it's marked on the spot.

In SwiftUI, custom views are declared as structs that conform to the View protocol, so they are value types.#

Options

Show answer

True. Custom SwiftUI views are declared as structs that conform to the View protocol, which makes them value types. They act as lightweight, declarative descriptions of the interface, and SwiftUI diffs them to apply minimal updates to the screen.

Why:

Custom SwiftUI views are written as structs conforming to View, which makes them value types. A view struct is a lightweight, declarative description of the desired UI rather than a heavyweight object tied to the screen. SwiftUI compares the old and new view descriptions and applies minimal updates to the rendered output, so the statement is true.

In SwiftUI, a type conforming to View implements a body property. Which opaque return type, written with the some keyword and the View protocol, is conventionally used as the declared type of body so that the concrete view type is hidden from callers?#

Show answer

some View (an opaque return type, introduced in Swift 5.1).

Why:

SwiftUI's View protocol requires a body property whose type is some View. Writing some View uses Swift's opaque return type feature, available since Swift 5.1. The some keyword lets the compiler fix one concrete type at compile time without exposing it to the caller. any View is an existential container rather than an opaque type, View alone names the protocol and is not a return-type annotation, and ObservableObject is an unrelated protocol, so none of these is the conventional body return type.

What is the opaque return type used for the body property of a SwiftUI View?#

Show answer

some View, an opaque return type. The some keyword tells the compiler the concrete type is fixed at compile time, while callers see only that it conforms to View.

Why:

SwiftUI's View protocol declares body with the return type some View, using Swift's opaque return type feature (available since Swift 5.1). The some keyword guarantees the concrete type is fixed at compile time without exposing it to the caller. any View is an existential container, View is the protocol itself, and ObservableObject is an unrelated protocol, so none of these satisfy the body requirement.

In SwiftUI, each view modifier wraps the view it is applied to and returns a new view. Consider these two snippets:#

Show answer

In Snippet A the red background fills the entire padded area, because padding is applied first and the background then covers the padded view; in Snippet B the red only covers the text itself, and the padding area remains the container's default color. The difference comes from modifier order: each modifier wraps the view before it, so the modifier applied first determines what the later modifier sees.

Why:

SwiftUI applies modifiers in sequence, each wrapping the view produced by the previous one. In Snippet A, .padding() is applied first, creating a view with extra space around the text; .background(Color.red) then wraps that padded view, so the red fills the entire padded region and extends beyond the text. In Snippet B, .background(Color.red) is applied to the bare Text first, so the red covers only the text bounds; .padding() then wraps that result and adds transparent space around it, which shows the container's color rather than red. The visible difference is therefore caused solely by modifier order.

Consider this SwiftUI view:#

Show answer

The view shows the text 'Hello, World!' in white, centered inside a red rounded rectangle with a 10-point corner radius. Because .padding() is applied before .background(Color.red), the red fill covers the 16-point padding area on all four sides, so the white text is surrounded by a red margin. The .foregroundColor(.white) modifier overrides the default black text color, and .cornerRadius(10) rounds the corners of the entire padded red background.

Why:

The Text view is first padded by 16 points on each side, which enlarges its layout box. The .background(Color.red) modifier then fills that padded box, so the red area surrounds the text with a margin. The .foregroundColor(.white) modifier sets the text glyphs to white instead of the default black. Finally, .cornerRadius(10) clips the whole padded, red-backed view to a rounded rectangle with a 10-point radius, giving white centered text inside a red rounded rectangle with visible padding on every side.

The following SwiftUI view compiles without errors, but it contains a subtle bug that causes the displayed label to stop updating after the first render when the parent view passes a new label value. Which line is the source of the bug?#

import SwiftUI · · · `var body: some View {`

Options

Show answer

Line 4 — @State private var label: String. @State captures the initializer value only on first creation and ignores subsequent parent updates, so the text stays stale; it should be a plain let property.

Why:

@State is designed for values that a view owns and mutates internally. SwiftUI persists @State storage across view re-creations, so the value assigned in init is only observed the very first time the view is created. When the parent recreates BadgeView with a different label, the @State wrapper ignores the new initializer value and keeps the original, producing stale text. Because label is never mutated inside BadgeView and always originates from the parent, it should be a plain let label: String — which re-reads the passed value on every re-creation. Custom initializers are perfectly valid in SwiftUI, label and self.label are equivalent in this context, and the order of .padding and .background is a layout choice, not a correctness issue.

Consider the following SwiftUI view. When LeaderboardView is initialized with scores = [(player: “Alice”, score: 100), (player: “Bob”, score: 85), (player: “Charlie”, score: 70)], what text does it render on initial display (before any user interaction)? Assume @State starts at its declared default.#

import SwiftUI
Show answer
Leaderboard
Alice: 100
Bob: 85
...and 1 more
Why:

On first render @State initializes maxDisplay to 2. The computed displayed property calls scores.prefix(2), yielding the first two tuples — Alice (100) and Bob (85) — which ForEach renders in array order. The if branch then checks scores.count (3) > maxDisplay (2), which is true, so the text “…and 1 more” appears and the else branch (“All shown”) is skipped. VStack arranges children top-to-bottom, so the four Text views appear in that exact order.

In SwiftUI, a parent view re-renders and SwiftUI recreates the child view struct, calling its initializer again. The child declares @StateObject private var vm = CounterVM(), where CounterVM is a final class conforming to ObservableObject with @Published var count = 0. What happens to the CounterVM instance owned by that @StateObject during the parent's re-render, and why? Answer in 2-4 sentences.#

Show answer

The same CounterVM instance survives the re-render and is reused, so its count is preserved. SwiftUI keeps @StateObject storage outside the transient view struct, tied to the view's identity in the hierarchy, and the autoclosure CounterVM() runs only when the view is first created. Recreating the struct only produces a new wrapper that points at the existing stored object.

Why:

A @StateObject stores its ObservableObject in SwiftUI-managed storage associated with the view's identity, not in the value-type struct. When the parent re-renders, SwiftUI recreates the ChildView struct and runs its initializer, but the @StateObject's wrapped-value autoclosure is evaluated only on first insertion into the hierarchy. On later re-renders SwiftUI reuses the existing CounterVM, so its count is kept. A correct answer must say the instance persists or is reused rather than being recreated, and should give the reason: SwiftUI-managed storage tied to view identity, with the initializer evaluated only once.

A SwiftUI list displays user names. Tapping Next User should advance to the next name, and the Text in UserProfileView should update to match. However, after tapping Next User, the displayed name never changes. Identify the buggy line by its 1-based line number.#

1  struct ContentView: View {
2      @State private var userIdx = 0
3      let names = ["Alice", "Bob", "Charlie"]
4      var body: some View {
5          VStack {
6              UserProfileView(name: names[userIdx])
7              Button("Next User") {
8                  userIdx = (userIdx + 1) % names.count
9              }
10         }
11     }
12 }
13 struct UserProfileView: View {
14     let name: String
15     @State private var displayName = name
16     var body: some View {
17         Text(displayName)
18     }
19 }
Show answer

The bug is on line 15.

Why:

Line 15 is the bug: @State private var displayName = name copies the parent's value into state. SwiftUI creates that state storage once, when the view first appears, and keeps it for the view's identity afterward. When ContentView re-renders and passes a new name (because userIdx changed), the UserProfileView struct is recreated but its stored displayName is not reset, so it keeps showing "Alice". Line 17 is correct: it faithfully displays the state it is given. The fix is to remove the @State copy and render Text(name) directly, since name is already the source of truth; syncing with .onChange(of: name) is a workaround that duplicates the data.

This SwiftUI app embeds a Counter view inside a RootView. The stepper lets the user increment value, but every time the user taps 'Switch Tab' the counter resets to 0, even though no code assigns value. Line numbers are counted from 1 at import SwiftUI. Which line contains the bug that causes the reset?#

import SwiftUI

struct Counter: View {
    let title: String
    @State private var value = 0

    var body: some View {
        VStack {
            Text("\(title): \(value)")
            Stepper("Count", value: $value)
        }
    }
}

struct RootView: View {
    @State private var tab = 0

    var body: some View {
        Counter(title: "Tab \(tab)").id(UUID())
        Button("Switch Tab") { tab += 1 }
    }
}
Show answer

The bug is on line 19.

Why:

The bug is on line 19, the .id(UUID()) modifier applied to Counter. @State storage is tied to a view's identity in SwiftUI's view tree. UUID() returns a new value on every call, so each time RootView.body is evaluated (for example, when tab changes from tapping 'Switch Tab'), the Counter receives a different identity. SwiftUI treats that as a brand-new view, discards the old Counter along with its @State value, and creates a fresh one starting at 0. Removing the .id(UUID()) modifier, or replacing it with a constant identifier, keeps the @State alive across re-renders.

Consider the following SwiftUI view. Its body is declared as some View and is built with an if/else inside the property. At compile time, what is the concrete type that body actually returns (the type the compiler substitutes for some View)? Name the type, including its generic arguments, and explain which mechanism produces it.#

Show answer

The concrete type is _ConditionalContent<Text, Image>. The View protocol's body requirement is annotated with @ViewBuilder, and the result builder transforms the if/else into _ConditionalContent<TrueView, FalseView>, a single generic type that conforms to View when both branches do. The opaque some View return type is satisfied by this concrete type without type erasure, so no AnyView wrapper is needed.

Why:

SwiftUI's View protocol declares body with @ViewBuilder. That result builder rewrites an if/else into _ConditionalContent<TrueView, FalseView>, a single concrete generic type that conforms to View when both branches do. Here the branches are Text("Online") and Image(systemName: "wifi.slash"), so the concrete return type is _ConditionalContent<Text, Image>, which the some View opaque return type hides from callers. No type erasure such as AnyView is involved; the builder produces a statically typed value.

A SwiftUI counter app has a ParentView that contains a ChildView with its own CounterViewModel. The Toggle button changes the parent's trigger state, which changes the label passed to ChildView. Users report that every time Toggle is tapped, the count shown by ChildView resets to 0, even though the Increment button works between toggles. Identify the line in the code below that causes this behavior (1-based line numbers).#

import SwiftUI

final class CounterViewModel: ObservableObject {
    @Published var count = 0

    func increment() {
        count += 1
    }
}

struct ChildView: View {
    let label: String
    @ObservedObject var viewModel = CounterViewModel()

    var body: some View {
        VStack(spacing: 8) {
            Text("\(label): \(viewModel.count)")
            Button("Increment") { viewModel.increment() }
        }
    }
}

struct ParentView: View {
    @State private var trigger = false

    var body: some View {
        VStack {
            Button("Toggle") { trigger.toggle() }
            ChildView(label: trigger ? "On" : "Off")
        }
    }
}
Show answer

The bug is on line 13.

Why:

The bug is on line 13: @ObservedObject var viewModel = CounterViewModel(). @ObservedObject subscribes to an object that is owned elsewhere; it does not store the object itself. The default-value expression CounterViewModel() is evaluated whenever ChildView is initialized. Tapping Toggle changes trigger, which ParentView's body reads, so SwiftUI re-evaluates the parent body and constructs a new ChildView value. That re-initialization creates a fresh CounterViewModel with count = 0, which replaces the instance that held the user's increments. The fix is to change line 13 to @StateObject var viewModel = CounterViewModel(), since @StateObject keeps the object in SwiftUI's persistent storage across re-initializations of the view. Lines 3-9, 17-18, and 23-31 are correct as written.

Consider this SwiftUI code (iOS 17, Swift 5.9):#

Show answer

AnyView erases the concrete view type, so SwiftUI cannot structurally diff the branch's subtree by type. When the parent body re-evaluates and returns a new AnyView, SwiftUI cannot update the existing subtree in place and must rebuild it, which costs extra work and can lose view identity, so @State inside the subtree may reset and implicit animations may be interrupted. Returning some View from @ViewBuilder (or wrapping in Group) preserves the concrete _ConditionalContent type so SwiftUI can diff efficiently.

Why:

The key consequence is that AnyView hides the concrete type of each branch from SwiftUI's diffing engine. SwiftUI relies on static types in the view graph to compare old and new view values and update matching views in place. Because the helper produces a type-erased value on every body evaluation, SwiftUI sees an opaque replacement and rebuilds the subtree rather than updating it, which adds work and can reset @State and interrupt implicit animations. The idiomatic fix is @ViewBuilder, which keeps the concrete type through _ConditionalContent, or Group, which is transparent to diffing.

Related interview questions

Job market

See swift salaries and hiring demand from live job postings.

Practise these until they stick

That's every question we hold on this topic, and the page marks what you pick. What it can't do is remember. A free account keeps every answer, and what you miss comes back until it's right: after a day, then at longer gaps.

Start with this topic

Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan

Interview booked? Practise the job ad instead

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.