AppleMousse Studio Journal SwiftUI, without tripping over it
SwiftUI 8 min read

SwiftUI, without tripping over it

Because staring at a blank screen in Xcode is quite enough as a rite of passage

J
Julien Taret
Founder — AppleMousse Studio
SwiftUI, without tripping over it

UIKit is describing every gesture needed to make coffee. SwiftUI is saying “a coffee” and having it appear.

We have all had the moment. You open Xcode for the first time, you look for where to put a button, and thirty minutes later you are staring at a blank screen with the dignity of a backend developer discovering CSS.

Think of this guide as the mate who suffered before you and is handing over his notes.


1. What is SwiftUI, really?

SwiftUI is Apple’s modern framework. The principle: you describe what the interface should show, and SwiftUI handles the rest.

That is what we call the declarative approach. And no, it is not a word invented to frighten juniors — it is just a different way of thinking about UI.

import SwiftUI

struct WelcomeView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
            .font(.largeTitle)
            .foregroundStyle(.orange) // modern (iOS 15+)
            .padding()
    }
}

💡 .foregroundStyle() is the recommended syntax since iOS 15 — it accepts gradients, materials, colours. .foregroundColor() still works, but it is the version starting to smell of pine boxes ;).


2. Declarative vs imperative (UIKit)

With UIKit (the old world), you describe every step. With SwiftUI, you describe the result. A bit like the difference between giving turn-by-turn directions and saying “take me to the bakery”.

// UIKit — imperative (the chatty sat-nav)
let label = UILabel()
label.text = "Hello"
label.textColor = .blue
label.font = .preferredFont(forTextStyle: .title1)
view.addSubview(label)

// SwiftUI — declarative (the zen sat-nav)
Text("Hello")
    .foregroundColor(.blue)
    .font(.title)

💡 The lifecycle still exists in SwiftUI — it is just handled differently. .onAppear and .task replace viewDidLoad. They did not remove it, they just made it less stressful.


3. The essential building blocks

Everything in SwiftUI is a View. Modifiers style it. State drives the data. And data flow keeps everything in sync.

struct ExampleView: View {
    @State private var count = 0 // local source of truth

    var body: some View {
        Text("Count: \(count)")
            .font(.title)
            .padding()
        Button("+") { count += 1 }
    }
}

💡 When @State changes, SwiftUI recomputes body automatically. It is magic. Well, it is code — but it has the same effect.


4. The essential views

SwiftUI gives you native views (Text, Image, Button, Toggle, List) and layout containers (VStack, HStack, ZStack, ScrollView, NavigationStack).

VStack(spacing: 12) {
    Text("Profile").font(.headline)

    Image(systemName: "person.circle")
        .font(.system(size: 60))

    Toggle("Notifications", isOn: $notifOn)

    HStack {
        Button("Cancel") { }
        Button("Confirm") { }
    }
}

💡 VStack = vertical, HStack = horizontal, ZStack = stacked on top. Mix all three by accident and you get either a brilliant UI or post-traumatic stress.


5. State management

This is where things get serious. SwiftUI has several wrappers depending on where your data lives:

  • @State → state local to the view
  • @Binding → state shared between two views
  • @StateObject / @ObservedObject → external observable object
  • @EnvironmentObject → global state broadcast through the view tree

And since iOS 17: @Observable, which simplifies all of it by removing @Published.

// Classic approach (iOS 14+)
class CartViewModel: ObservableObject {
    @Published var items: [String] = []
    func addItem(_ item: String) { items.append(item) }
}

// Modern approach (iOS 17+, recommended)
@Observable class CartViewModel {
    var items: [String] = [] // no more @Published
    func addItem(_ item: String) { items.append(item) }
}

struct CartView: View {
    @State private var vm = CartViewModel()

    var body: some View {
        List(vm.items, id: \.self) { Text($0) }
        Button("Add") { vm.addItem("Item") }
    }
}

💡 @Observable (iOS 17) frees you from per-property @Published. If you target iOS 14–16, ObservableObject is still the reference — and yes, you will still be writing @Published everywhere. Sorry.


6. Navigation & presentation

NavigationStack handles stack-based navigation. sheet and fullScreenCover present modal views.

struct ContentView: View {
    @State private var showSheet = false
    let items = ["Paris", "Lyon", "Nice"]

    var body: some View {
        NavigationStack {
            List(items, id: \.self) { city in
                NavigationLink(city) {
                    Text("Welcome to \(city)")
                }
            }
            .navigationTitle("Cities")
            .toolbar {
                Button("Info") { showSheet = true }
            }
        }
        .sheet(isPresented: $showSheet) {
            Text("Detail sheet")
        }
    }
}

💡 NavigationStack replaces NavigationView as of iOS 16. TabView is still essential for tab navigation. And NavigationSplitView exists for column layouts — iPad and Mac users, that one is for you.


7. Reusable components

Pulling views out into standalone components makes the code modular and testable. You pass data through simple properties.

struct UserCard: View {
    let name: String
    let role: String

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: "person.crop.circle")
                .font(.system(size: 40))
                .foregroundColor(.orange)
            VStack(alignment: .leading) {
                Text(name).font(.headline)
                Text(role).foregroundColor(.secondary)
            }
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

// Usage:
UserCard(name: "Alice", role: "Designer")

💡 There is no magic rule about how many lines before you extract a view. The real signal: if you start losing your place scrolling through your own body, something should move into a subview.


8. MVVM with SwiftUI

The MVVM pattern separates the View (SwiftUI), the ViewModel (which holds the logic) and the Model (your data and services).

// Model
struct User: Identifiable {
    let id = UUID()
    var name: String
}

// ViewModel
class UserViewModel: ObservableObject {
    @Published var users: [User] = []

    func loadUsers() async {
        users = [User(name: "Alice"), User(name: "Bob")]
    }
}

// View
struct UsersView: View {
    @StateObject private var vm = UserViewModel()

    var body: some View {
        List(vm.users) { Text($0.name) }
            .task { await vm.loadUsers() }
    }
}

💡 .task {} replaces .onAppear {} for async/await calls. It also handles cancellation automatically if the view disappears before the task finishes. The kind of attention to detail that makes life easier.


9. Animations & transitions

SwiftUI offers two approaches: implicit with .animation(_:value:) and explicit with withAnimation. Both coexist, depending on the case.

struct LikeButton: View {
    @State private var liked = false

    var body: some View {
        VStack {
            Button {
                withAnimation(.spring(response: 0.5)) {
                    liked.toggle()
                }
            } label: {
                Image(systemName: liked ? "heart.fill" : "heart")
                    .foregroundColor(liked ? .red : .gray)
                    .font(.system(size: 40))
                    .scaleEffect(liked ? 1.3 : 1.0)
            }
            if liked {
                Text("Liked! ❤️")
                    .transition(.move(edge: .bottom).combined(with: .opacity))
            }
        }
    }
}

💡 withAnimation is explicit — you trigger it inside an action. .animation(_:value:) is implicit — it reacts automatically to a change. For genuinely advanced cases there is matchedGeometryEffect, keyframes and phase animations (iOS 17+). In other words: the rabbit hole.


10. Good practice

Small views, immutable structures, MVVM to scale, and previews so you do not spend your life in the simulator.

// ✅ Small, focused views
struct PriceLabel: View {
    let price: Double
    var body: some View {
        Text("\(price, format: .currency(code: "EUR"))")
            .font(.title2).bold()
    }
}

// ✅ Immutable structures for models
struct Product: Identifiable, Hashable {
    let id: UUID
    let name: String
    let price: Double
}

// ✅ The right wrapper for the context
@State var localValue = ""   // Local to the view
@Binding var shared: Bool    // Shared with a parent view
@EnvironmentObject var store // Global in the tree

💡 Avoid very deep view hierarchies. Prefer horizontal composition. Your future self — the one rereading this code at 11pm on a Tuesday — will thank you.


11. Previews in Xcode

Previews let you see changes in real time, without compiling and launching the app every single time. You can preview several states at once. It is one of the rare development features that genuinely makes people happy.

struct ProductCard: View {
    let title: String
    let price: Double

    var body: some View {
        VStack {
            Text(title).font(.headline)
            Text("\(price)€").foregroundColor(.green)
        }.padding()
    }
}

// Multi-state preview
#Preview("Cheap") {
    ProductCard(title: "Coffee", price: 1.50)
}

#Preview("Premium") {
    ProductCard(title: "Watch", price: 299.0)
        .preferredColorScheme(.dark)
}

💡 #Preview (iOS 17+) replaces the old PreviewProvider and is considerably more concise. If you are still using PreviewProvider, you have done nothing wrong — but you can do better.


12. Common modifiers

Modifiers chain together to compose a view’s style and behaviour. Order matters.padding().background() does not give the same result as .background().padding(). It is counter-intuitive the first time. And a bit on the tenth, too.

Text("SwiftUI is powerful")
    // Spacing & size
    .padding(16)
    .frame(maxWidth: .infinity)
    // Appearance
    .background(Color.orange.opacity(0.15))
    .foregroundColor(.orange)
    .font(.headline)
    // Shape
    .clipShape(RoundedRectangle(cornerRadius: 10))
    .overlay(
        RoundedRectangle(cornerRadius: 10)
            .stroke(Color.orange, lineWidth: 1))
    // Interaction
    .onTapGesture { print("Tapped!") }
    .shadow(radius: 4)

💡 Each modifier conceptually produces a new wrapping view. In practice SwiftUI optimises internally and does not rebuild the whole hierarchy every time. But reason as if it did — it helps you understand why order changes everything.


SwiftUI is frightening on the first evening. On the second, it starts to click. The first time a spring animation fires exactly the way you pictured it, you understand why Apple bet everything on it.

The rest is practice. And previews.

J

Julien Taret

Founder of AppleMousse Studio. Designer and iOS developer since 2025, makes apps the way you make pastry: simple ingredients and a lot of patience.