AppleMousse Studio Journal SwiftUI, sans se prendre les pieds dedan…
SwiftUI 8 min de lecture

SwiftUI, sans se prendre les pieds dedans

Parce que fixer un écran blanc dans Xcode, ça suffit comme rite de passage

J
Julien Taret
Fondateur — AppleMousse Studio
SwiftUI, sans se prendre les pieds dedans

UIKit, c’est quand tu décris chaque geste pour faire du café. SwiftUI, c’est quand tu dis juste “un café” et que ça apparaît.

On a tous vécu le moment. On ouvre Xcode pour la première fois, on cherche où poser un bouton, et trente minutes plus tard on fixe un écran blanc avec la dignité d’un développeur backend qui découvre le CSS.

Considérez ce guide comme le copain qui a déjà souffert avant vous et qui vous passe ses notes.


1. Qu’est-ce que SwiftUI, vraiment ?

SwiftUI est le framework moderne d’Apple. Son principe : on décrit ce que l’interface doit afficher, et SwiftUI se charge du reste.

C’est ce qu’on appelle l’approche déclarative. Et non, ce n’est pas un mot inventé pour faire peur aux juniors — c’est juste une façon de penser l’UI différemment.

import SwiftUI

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

💡 .foregroundStyle() est la syntaxe recommandée depuis iOS 15 — elle accepte des dégradés, des matériaux, des couleurs. .foregroundColor() reste valide mais c’est la version qui commence à sentir le sapin ;).


2. Déclaratif vs Impératif (UIKit)

Avec UIKit (l’ancien monde), on décrit chaque étape. Avec SwiftUI, on décrit le résultat. C’est un peu comme la différence entre donner des instructions GPS virage par virage, et dire “emmène-moi à la boulangerie”.

// UIKit — impératif (le GPS verbeux)
let label = UILabel()
label.text = "Hello"
label.textColor = .blue
label.font = .preferredFont(forTextStyle: .title1)
view.addSubview(label)

// SwiftUI — déclaratif (le GPS zen)
Text("Hello")
    .foregroundColor(.blue)
    .font(.title)

💡 Le cycle de vie existe toujours dans SwiftUI — il est juste géré différemment. .onAppear et .task remplacent viewDidLoad. Ils ne l’ont pas supprimé, ils l’ont juste rendu moins stressant.


3. Les blocs de construction essentiels

Tout dans SwiftUI est une View. Les modifiers la stylisent. Le State pilote les données. Et le Data Flow assure que tout reste synchronisé.

struct ExampleView: View {
    @State private var count = 0 // source de vérité locale

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

💡 Quand @State change, SwiftUI recalcule automatiquement le body. C’est magique. Enfin, c’est du code — mais ça fait le même effet.


4. Les vues essentielles

SwiftUI offre des vues natives (Text, Image, Button, Toggle, List) et des conteneurs de layout (VStack, HStack, ZStack, ScrollView, NavigationStack).

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

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

    Toggle("Notifications", isOn: $notifOn)

    HStack {
        Button("Annuler") { }
        Button("Valider") { }
    }
}

💡 VStack = vertical, HStack = horizontal, ZStack = superposition. Si vous mélangez les trois par accident, vous obtenez soit une UI géniale, soit un syndrome post traumatique.


5. Gestion de l’état

C’est là que les choses deviennent sérieuses. SwiftUI a plusieurs wrappers selon où vit votre donnée :

  • @State → état local à la vue
  • @Binding → état partagé entre deux vues
  • @StateObject / @ObservedObject → objet observable externe
  • @EnvironmentObject → état global diffusé dans l’arbre de vues

Et depuis iOS 17 : @Observable, qui simplifie tout ça en supprimant @Published.

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

// Approche moderne (iOS 17+ recommandée)
@Observable class CartViewModel {
    var items: [String] = [] // plus besoin de @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("Ajouter") { vm.addItem("Article") }
    }
}

💡 @Observable (iOS 17) vous libère du @Published par propriété. Si vous ciblez iOS 14–16, ObservableObject reste la référence — et oui, vous devrez encore écrire @Published partout. Désolé.


6. Navigation & Présentation

NavigationStack gère la navigation par pile. Les sheet et fullScreenCover ouvrent des vues modales.

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("Bienvenue à \(city)")
                }
            }
            .navigationTitle("Villes")
            .toolbar {
                Button("Info") { showSheet = true }
            }
        }
        .sheet(isPresented: $showSheet) {
            Text("Fiche détail")
        }
    }
}

💡 NavigationStack remplace NavigationView depuis iOS 16. TabView reste indispensable pour la navigation par onglets. Et NavigationSplitView existe pour les layouts à colonnes — iPad et Mac users, c’est pour vous.


7. Composants réutilisables

Extraire des vues en composants autonomes rend le code modulaire et testable. On passe les données via des propriétés simples.

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)
    }
}

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

💡 Il n’y a pas de règle magique sur le nombre de lignes pour extraire une vue. Le bon signal : si vous commencez à perdre le fil en scrollant dans votre propre body, c’est que quelqu’un devrait partir dans une sous-vue.


8. MVVM avec SwiftUI

Le pattern MVVM sépare la Vue (SwiftUI), le ViewModel (qui contient la logique) et le Model (vos données et 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")]
    }
}

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

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

💡 .task {} remplace .onAppear {} pour les appels async/await. Il gère aussi l’annulation automatiquement si la vue disparaît avant que la tâche soit terminée. Le genre d’attention qui nous facilite la vie.


9. Animations & Transitions

SwiftUI propose deux approches : implicite avec .animation(_:value:) et explicite avec withAnimation. Les deux coexistent, selon le cas.

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("Aimé ! ❤️")
                    .transition(.move(edge: .bottom).combined(with: .opacity))
            }
        }
    }
}

💡 withAnimation est explicite — vous le déclenchez dans une action. .animation(_:value:) est implicite — il réagit automatiquement à un changement. Pour les cas vraiment avancés, il y a matchedGeometryEffect, les keyframes et les phase animations (iOS 17+). Autrement dit : le terrier du lapin blanc.


10. Bonnes pratiques

Des vues petites, des structures immuables, MVVM pour scaler, et des previews pour ne pas passer sa vie dans le simulateur.

// ✅ Vues petites et focalisées
struct PriceLabel: View {
    let price: Double
    var body: some View {
        Text("\(price, format: .currency(code: "EUR"))")
            .font(.title2).bold()
    }
}

// ✅ Structures immuables pour les modèles
struct Product: Identifiable, Hashable {
    let id: UUID
    let name: String
    let price: Double
}

// ✅ Le bon wrapper selon le contexte
@State var localValue = ""   // Local à la vue
@Binding var shared: Bool    // Partagé avec une vue parente
@EnvironmentObject var store // Global dans l'arbre

💡 Évitez les hiérarchies de vues trop profondes. Préférez la composition horizontale. Votre futur vous — celui qui relira ce code à 23h un mardi — vous remerciera.


11. Preview dans Xcode

Les previews permettent de voir les changements en temps réel, sans compiler et lancer l’app à chaque fois. On peut prévisualiser plusieurs états simultanément. C’est une des rares features de développement qui rend vraiment heureux.

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

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

// Preview multi-états
#Preview("Pas cher") {
    ProductCard(title: "Café", price: 1.50)
}

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

💡 #Preview (iOS 17+) remplace l’ancien PreviewProvider et c’est nettement plus concis. Si vous utilisez encore PreviewProvider, vous n’avez rien fait de mal — mais vous pouvez vous améliorer.


12. Modifiers courants

Les modifiers s’enchaînent pour composer le style et le comportement d’une vue. L’ordre compte.padding().background() ne donne pas le même résultat que .background().padding(). C’est contre-intuitif la première fois. La dixième aussi, un peu.

Text("SwiftUI est puissant")
    // Espacement & taille
    .padding(16)
    .frame(maxWidth: .infinity)
    // Apparence
    .background(Color.orange.opacity(0.15))
    .foregroundColor(.orange)
    .font(.headline)
    // Forme
    .clipShape(RoundedRectangle(cornerRadius: 10))
    .overlay(
        RoundedRectangle(cornerRadius: 10)
            .stroke(Color.orange, lineWidth: 1))
    // Interaction
    .onTapGesture { print("Tappé !") }
    .shadow(radius: 4)

💡 Chaque modifier produit conceptuellement une nouvelle vue enveloppante. En pratique, SwiftUI optimise en interne et ne recrée pas toute la hiérarchie à chaque fois. Mais raisonnez comme si c’était le cas — ça aide à comprendre pourquoi l’ordre change tout.


SwiftUI fait peur le premier soir. Le deuxième, ça commence à cliquer. La première fois qu’une animation spring se déclenche exactement comme vous l’aviez imaginée, vous comprenez pourquoi Apple a tout misé dessus.

Le reste, c’est de la pratique. Et des previews.

J

Julien Taret

Fondateur d'AppleMousse Studio. Designer et développeur iOS depuis 2025, fait des apps comme on fait de la pâtisserie : avec des ingrédients simples et beaucoup de patience.