SwiftUI Fundamentals
Irbisa · cheatsheetSeptember 13, 2026

SwiftUI Fundamentals

View protocol, modifiers, layout containers, navigation, lists

Junior Developer20 itemscompressed for a skim
  1. 01

    What is SwiftUI and how does it differ from UIKit?

    Easy

    SwiftUI is Apple's declarative UI framework, introduced in iOS 13 (2019).

    import SwiftUI
    
    struct GreetingView: View {
      let name: String
      var body: some View {
        VStack(spacing: 12) {
    …
  2. 02

    Explain stack containers in SwiftUI: VStack, HStack, ZStack, LazyVStack/HStack.

    Easy

    Stacks are how SwiftUI lays out children along an axis, and picking one is mostly a question of direction plus whether the content is long enough to need laziness.

    // Vertical stack
    VStack(alignment: .leading, spacing: 8) {
      Text("Title").font(.headline)
      Text("Subtitle").foregroundStyle(.secondary)
    }
    …
  3. 03

    How do view modifiers work in SwiftUI?

    Medium

    A modifier is a method on View that returns a new view wrapping the original with extra behavior (padding, background, foreground, gesture).

    // Built-in modifiers
    Text("Hello")
      .font(.title)
      .foregroundStyle(.blue)
      .padding()
      .background(.yellow)
    …
  4. 04

    How does navigation work in SwiftUI? NavigationStack and NavigationSplitView.

    Medium

    SwiftUI navigation is state-driven: you push by appending a value to a path, not by calling a method on a navigation controller.

    // Typed route stack
    struct Route: Hashable { let userId: String }
    
    struct RootView: View {
      @State private var path: [Route] = []
      var body: some View {
    …
  5. 05

    How do you build a List in SwiftUI, and why does row identity matter?

    Medium

    A List gives you a scrolling, platform-styled container with separators, selection, swipe actions and pull-to-refresh for the cost of one view, and ForEach fills it from a collection.

    struct Todo: Identifiable { let id: UUID; var title: String; var done: Bool }
    
    struct TodoListView: View {
      @State var todos: [Todo] = []
      @State var search = ""
      var body: some View {
    …
  6. 06

    How do you write SwiftUI previews for a view with several states or a service dependency?

    Medium

    Previews render a view inside Xcode without launching the app, which turns the iteration loop from a build-and-navigate into a couple of seconds.

    struct ProfileCard: View {
      let user: User
      var body: some View {
        VStack(alignment: .leading) {
          Text(user.name).font(.title)
          Text(user.bio).foregroundStyle(.secondary)
    …
  7. 07

    Walk through how SwiftUI decides a view's size — who has the final say, the parent or the child?

    Medium

    SwiftUI layout is a three-step negotiation: the parent proposes a size, the child chooses its own size, and the parent then places the child inside itself.

    struct Badge: View {
      var body: some View {
        Text("Sale")
          .padding(8)                      // proposes (offered size - 16) to the Text
          .background(.pink)               // sizes itself around the padded text
          .frame(width: 200, height: 60)   // a NEW parent 200x60; the badge centres inside it
    …
  8. 08

    What is @ViewBuilder doing behind the scenes, and why is returning AnyView a bad habit?

    Medium

    @ViewBuilder is a result builder that folds a list of statements into one nested generic view type, which is how body can contain several views and an if without you ever writing that type out.

    struct Header: View {
      let isEditing: Bool
    
      var body: some View {          // body is @ViewBuilder, so several statements are allowed
        HStack {
          Text("Inbox").font(.headline)
    …
  9. 09

    How do you present a sheet or an alert in SwiftUI, and when do you use the item: form instead of isPresented:?

    Easy

    Presentation is a modifier bound to state: you flip a Bool or set an optional item, and the system presents or dismisses to match.

    struct InboxView: View {
      @State private var isShowingSettings = false
      @State private var editing: Message?          // nil means no sheet
      @State private var pendingDelete: Message?
      @State private var isConfirmingDelete = false
    …
  10. 10

    What is the difference between withAnimation and the .animation modifier, and why was the single-argument .animation(_:) deprecated?

    Medium

    withAnimation animates every change that results from the state mutation inside its closure, while the animation(_:value:) modifier animates one view's reaction to a single named value.

    struct Panel: View {
      @State private var isExpanded = false
      @State private var count = 0
    
      var body: some View {
        VStack {
    …
  11. 11

    When do you use a List, and when a ScrollView with a LazyVStack?

    Easy

    List gives you recycled rows, separators, swipe actions, selection and platform styling for free, while ScrollView plus LazyVStack gives you a blank canvas and full control of the row layout.

    // List: recycled rows, separators, swipe actions, selection — the choice for long data
    List {
      ForEach(messages) { message in
        Row(message: message)
          .swipeActions { Button("Delete", role: .destructive) { delete(message) } }
      }
    …
  12. 12

    What do you actually change on a SwiftUI screen to make it work with VoiceOver and Dynamic Type?

    Medium

    Standard SwiftUI controls are accessible by default, so the work is supplying what the framework cannot infer and making the layout survive very large text.

    struct MessageRow: View {
      let message: Message
      @Environment(\.dynamicTypeSize) private var typeSize
    
      var body: some View {
        let layout = typeSize.isAccessibilitySize
    …
  13. 13

    Your floating Save button covers the last row of a list, and the keyboard shoves the whole screen up — which safe-area APIs fix each?

    Easy

    The button belongs in safeAreaInset so the scroll view insets its content behind it, and the layout that must not move needs ignoresSafeArea(.keyboard).

    struct InboxView: View {
      @State private var draft = ""
    
      var body: some View {
        List(messages) { MessageRow($0) }
          // Right: the bar joins the safe area, so the last row scrolls clear of it
    …
  14. 14

    You wrapped a Text in .frame(maxWidth: .infinity) and it stayed centred — how do the different alignments in SwiftUI actually differ?

    Easy

    .frame centres its child by default, so you have to pass alignment: .leading, and even that only positions the whole text block — the wrapped lines inside it are governed separately by multilineTextAlignment.

    // Wrong: the frame stretches, the text stays centred inside it
    Text("Title").frame(maxWidth: .infinity)
    
    // Right: tell the frame where to put the child
    Text("Title").frame(maxWidth: .infinity, alignment: .leading)
    …
  15. 15

    A bundled image blows out the layout at full size, and a remote one flickers back to its placeholder while you scroll — what is going on?

    Easy

    Image ignores the size proposed to it until you call .resizable(), and AsyncImage restarts its request whenever the row is rebuilt because it keeps no decoded-image cache of its own.

    // Wrong: a bundled image ignores the proposed size until it is resizable
    Image("cover").frame(width: 120, height: 120)   // still full size, just overflowing
    
    // Right: resizable, then a content mode, then clip what .fill spills
    Image("cover")
      .resizable()
    …
  16. 16

    A detail screen fires its request again every time you navigate back to it, and list rows re-fetch as you scroll — where does that come from?

    Medium

    onAppear runs on every appearance, including a pop back onto the screen, and .task is bound to that same appear/disappear pair — so work started there restarts whenever the view returns and is cancelled the moment it leaves.

    struct ItemDetail: View {
      let id: Item.ID
      @State private var model = DetailModel()
    
      var body: some View {
        Content(state: model.state)
    …
  17. 17

    You need a photo grid that shows two columns on iPhone and four on iPad, plus a comparison table whose columns line up — what does SwiftUI give you?

    Medium

    LazyVGrid with an adaptive GridItem handles the flow-and-wrap case, Grid handles the table case where cells in different rows must share column widths, and ViewThatFits chooses between whole layouts.

    // Flow layout: as many columns as the width allows, built lazily
    ScrollView {
      LazyVGrid(columns: [GridItem(.adaptive(minimum: 120), spacing: 8)], spacing: 8) {
        ForEach(photos) { photo in
          Thumbnail(photo).aspectRatio(1, contentMode: .fill)
        }
    …
  18. 18

    A child needs to tell its parent how tall it is, and the parent cannot wrap everything in a GeometryReader — how does data travel up the tree?

    Medium

    Environment values flow down and preferences flow up: a child writes a value with .preference(key:value:), SwiftUI reduces every value in the subtree into one, and an ancestor reads the result with .onPreferenceChange or .overlayPreferenceValue.

    // A key is a default plus a rule for merging siblings
    struct MaxHeightKey: PreferenceKey {
      static let defaultValue: CGFloat = 0
      static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = max(value, nextValue())
      }
    …
  19. 19

    A list row has a star button and a delete button, and tapping either one runs both plus the row's navigation — what is happening?

    Medium

    Inside a List row the default button style hands the whole row's tap region to every button in it, so each button needs an explicit .buttonStyle(.borderless) or .plain to own its own bounds.

    // Wrong: inside a List row, .automatic buttons inherit the row's tap region,
    // so a single tap runs both actions and the row's navigation.
    List(messages) { message in
      HStack {
        Text(message.title)
        Spacer()
    …
  20. 20

    After saving on the fourth pushed screen you must land back on the root, and the same flow also runs inside a sheet — how do you build that?

    Medium

    Pop-to-root is one data change — clearing the array that drives the NavigationStack — so the deep screen needs access to that state, because @Environment(\.dismiss) only ever undoes one level.

    @Observable @MainActor
    final class Router {
      var path: [Route] = []
      func popToRoot() { path.removeAll() }
    }
    …