SwiftUI: Creare Interfacce Moderne per iOS

Una guida completa alla creazione di interfacce utente moderne con SwiftUI: sintassi dichiarativa, componenti, animazioni e best practice per iOS 18.

Guida SwiftUI per la creazione di interfacce iOS moderne

SwiftUI ha rivoluzionato lo sviluppo delle interfacce su tutte le piattaforme Apple. Grazie alla sua sintassi dichiarativa e all'integrazione nativa, questo framework consente di realizzare applicazioni eleganti con un numero di righe di codice notevolmente ridotto. Con iOS 18 arrivano miglioramenti significativi in termini di prestazioni e nuove funzionalita.

Why SwiftUI in 2026?

SwiftUI ha raggiunto la piena maturita con iOS 18, offrendo rendering ottimizzato, gestione della memoria migliorata e un'integrazione con UIKit semplificata. Rappresenta lo standard per le nuove applicazioni iOS.

Comprendere il Paradigma Dichiarativo

Prima di scrivere codice, e fondamentale capire cosa rende SwiftUI diverso. Con UIKit (il framework precedente), lo sviluppatore doveva indicare a iOS come costruire l'interfaccia passo dopo passo: "crea un'etichetta, posizionala qui, cambia il colore quando l'utente tocca". Questo e il paradigma imperativo.

SwiftUI funziona in modo diverso: lo sviluppatore descrive cosa vuole visualizzare, e il framework si occupa del resto. E come la differenza tra dare indicazioni stradali curva per curva (imperativo) e semplicemente indicare la destinazione (dichiarativo).

La Prima View

In SwiftUI, ogni elemento dell'interfaccia e una View. Una View e una struct che descrive cosa deve apparire sullo schermo. Ecco la prima schermata con un titolo, un sottotitolo e un pulsante:

ContentView.swiftswift
import SwiftUI

// Each screen is a struct that implements the View protocol
struct ContentView: View {

    // The "body" property describes what the view displays
    // "some View" means "a type of View, but Swift figures it out automatically"
    var body: some View {

        // VStack = "Vertical Stack": stacks elements vertically
        // spacing: 20 = 20 points of space between each element
        VStack(spacing: 20) {

            // A simple text with style modifiers
            Text("Welcome to SharpSkill")
                .font(.largeTitle)      // Large title font
                .fontWeight(.bold)      // Bold text

            Text("Prepare for your iOS interviews")
                .font(.subheadline)     // Smaller font
                .foregroundColor(.secondary)  // Gray secondary color

            // Button with an action (closure) and a label
            Button("Get Started") {
                print("Button tapped!")
            }
            .buttonStyle(.borderedProminent)  // Filled blue button style
        }
        .padding()  // Adds margins around the VStack
    }
}

Da notare la struttura: si dichiarano gli elementi desiderati (testi, pulsante), si impilano verticalmente (VStack) e si applicano gli stili tramite modifier (.font(), .padding()).

Concetto chiave: In SwiftUI non si "creano" oggetti UI, si descrive l'interfaccia desiderata. SwiftUI gestisce autonomamente la creazione, l'aggiornamento e la distruzione degli elementi reali.

Modifier: Trasformare le View

I modifier sono metodi concatenati dopo una view per trasformarla. Funzionano come filtri applicati uno dopo l'altro. L'ordine conta perche ogni modifier crea una nuova view che avvolge la precedente.

Ecco un esempio che illustra perche l'ordine e cruciale:

ModifiersOrder.swiftswift
// Example 1: padding THEN background
Text("SwiftUI")
    .padding()           // 1. Adds 16pt of space around the text
    .background(.blue)   // 2. Blue background covers text + padding
    .foregroundColor(.white)
// Result: white text on blue rectangle with margins

// Example 2: background THEN padding (reversed order!)
Text("SwiftUI")
    .background(.blue)   // 1. Tight blue background around text only
    .padding()           // 2. Transparent padding around blue background
    .foregroundColor(.white)
// Result: white text on small blue rectangle, surrounded by empty space

La differenza? Nel primo caso, il padding e "dentro" lo sfondo. Nel secondo, e "fuori". Una sottigliezza essenziale per padroneggiare i layout.

Organizzare le Interfacce con gli Stack

SwiftUI offre tre contenitori principali per organizzare le view. Si possono immaginare come scatole che dispongono il contenuto in modi diversi.

VStack, HStack e ZStack

  • VStack (Vertical Stack): impila gli elementi dall'alto verso il basso
  • HStack (Horizontal Stack): allinea gli elementi da sinistra a destra
  • ZStack (Z-axis Stack): sovrappone gli elementi uno sull'altro

Costruiamo una card profilo utente combinando tutti e tre gli stack. L'obiettivo: mostrare una foto con un badge di verifica, quindi il nome e il ruolo dell'utente.

ProfileCard.swiftswift
struct ProfileCard: View {
    var body: some View {
        // Main HStack: photo on left, info on right
        HStack(spacing: 16) {

            // ZStack to overlay the badge on the photo
            ZStack(alignment: .bottomTrailing) {
                // Profile image (circle)
                Image("avatar")
                    .resizable()
                    .frame(width: 60, height: 60)
                    .clipShape(Circle())

                // Green "verified" badge at bottom right
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.green)
                    .background(Circle().fill(.white))  // White circle behind
            }

            // VStack to stack name and role vertically
            VStack(alignment: .leading, spacing: 4) {
                Text("Marie Dupont")
                    .font(.headline)

                Text("iOS Developer")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            // Spacer pushes everything to the left
            Spacer()

            // Chevron on right indicates it's tappable
            Image(systemName: "chevron.right")
                .foregroundColor(.gray)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

Il trucco qui e Spacer(): e un elemento invisibile che occupa tutto lo spazio disponibile. Senza di esso, gli elementi sarebbero centrati. Con esso, vengono spinti a sinistra e il chevron resta ancorato a destra.

Xcode Tip

Utilizzare ⌘ + click su qualsiasi view in Xcode per accedere all'inspector visuale. Si possono aggiungere modifier senza scrivere codice!

Gestire lo Stato con @State e @Binding

La gestione dello stato e il cuore di SwiftUI. Lo stato e qualsiasi dato che puo cambiare e che deve aggiornare l'interfaccia. Quando lo stato cambia, SwiftUI ricalcola automaticamente le view interessate.

@State: Lo Stato Locale di una View

@State e un property wrapper che comunica a SwiftUI: "osserva questa variabile e aggiorna la view quando cambia". E perfetto per lo stato locale di una singola view.

Ecco un contatore interattivo per comprendere il meccanismo:

CounterView.swiftswift
struct CounterView: View {
    // @State creates a "source of truth" for this view
    // private because state shouldn't be modified from outside
    @State private var count = 0

    var body: some View {
        VStack(spacing: 30) {
            // This Text updates automatically when count changes
            Text("\(count)")
                .font(.system(size: 72, weight: .bold))

            HStack(spacing: 40) {
                // Decrement button
                Button(action: {
                    count -= 1  // Modifies state → view refreshes
                }) {
                    Image(systemName: "minus.circle.fill")
                        .font(.largeTitle)
                }

                // Increment button
                Button(action: {
                    count += 1
                }) {
                    Image(systemName: "plus.circle.fill")
                        .font(.largeTitle)
                }
            }
        }
    }
}

Quando si tocca un pulsante, count cambia. SwiftUI rileva questa modifica e riesegue body per aggiornare la visualizzazione. Non e necessario aggiornare manualmente l'etichetta: e tutto automatico.

@Binding: Condividere lo Stato tra View

A volte una view figlia ha bisogno di modificare lo stato della view genitore. E qui che entra in gioco @Binding: crea una connessione bidirezionale a un @State esistente.

Ecco un esempio concreto: un campo di input per il nome utente con validazione in tempo reale.

UsernameValidation.swiftswift
// Parent view: owns the state
struct SignupForm: View {
    @State private var username = ""       // Source of truth
    @State private var isValid = false     // Validation state

    var body: some View {
        VStack(spacing: 20) {
            // We pass BINDINGS (with $) to the child view
            UsernameField(username: $username, isValid: $isValid)

            Button("Create Account") {
                // Submit the form
            }
            .disabled(!isValid)  // Disabled if invalid
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

La view figlia riceve i binding e puo modificarli:

swift
// Child view: receives and modifies state via @Binding
struct UsernameField: View {
    @Binding var username: String   // Connection to parent's @State
    @Binding var isValid: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            TextField("Username", text: $username)
                .textFieldStyle(.roundedBorder)
                .onChange(of: username) { oldValue, newValue in
                    // Validation: at least 3 characters
                    isValid = newValue.count >= 3
                }

            // Visual feedback
            HStack {
                Image(systemName: isValid ? "checkmark.circle" : "xmark.circle")
                Text("Minimum 3 characters")
            }
            .font(.caption)
            .foregroundColor(isValid ? .green : .red)
        }
    }
}

Quando l'utente digita nel TextField, username viene modificato tramite il binding. La view genitore rileva la modifica e puo utilizzarla. Una comunicazione bidirezionale pulita.

Warning

Utilizzare @State solo per stati semplici e locali. Per dati condivisi tra piu schermate o logica complessa, e preferibile ricorrere a @Observable (iOS 17+) o ai pattern MVVM.

Pronto a superare i tuoi colloqui su iOS?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Visualizzare Liste Dinamiche

Le liste sono onnipresenti nelle app mobile. SwiftUI mette a disposizione List per visualizzare collezioni di dati con lo stile nativo iOS (separatori, azioni di scorrimento, ecc.).

Creare una Lista Semplice

Per visualizzare una lista servono due cose: i dati e un modo per identificarli. Il protocollo Identifiable permette a SwiftUI di sapere quale view corrisponde a quale dato.

Per prima cosa si definisce il modello dati:

Models/Interview.swiftswift
// Identifiable lets SwiftUI track each element
struct Interview: Identifiable {
    let id = UUID()           // Auto-generated unique identifier
    let technology: String
    let difficulty: String
    let questionCount: Int
}

Ora la lista. L'idea e iterare sui dati con ForEach e creare una riga per ogni elemento:

InterviewListView.swiftswift
struct InterviewListView: View {
    // Data to display (in reality, this would come from an API)
    @State private var interviews = [
        Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25),
        Interview(technology: "Android", difficulty: "Advanced", questionCount: 30),
        Interview(technology: "React", difficulty: "Beginner", questionCount: 20)
    ]

    var body: some View {
        // NavigationStack enables the navigation bar
        NavigationStack {
            List {
                // ForEach iterates over each interview
                // Thanks to Identifiable, no need to specify id:
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
            }
            .navigationTitle("My Interviews")
        }
    }
}

Ecco la view per ogni riga, estratta in un componente separato per chiarezza:

InterviewRow.swiftswift
struct InterviewRow: View {
    let interview: Interview

    var body: some View {
        HStack {
            // Left column: title and subtitle
            VStack(alignment: .leading, spacing: 4) {
                Text(interview.technology)
                    .font(.headline)

                Text(interview.difficulty)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            Spacer()

            // Badge with question count
            Text("\(interview.questionCount) Q")
                .font(.caption)
                .fontWeight(.medium)
                .padding(.horizontal, 8)
                .padding(.vertical, 4)
                .background(Color.blue.opacity(0.1))
                .cornerRadius(8)
        }
        .padding(.vertical, 4)
    }
}

Estrarre InterviewRow in una struct separata rende il codice piu leggibile e riutilizzabile. Si tratta di una best practice consolidata in SwiftUI.

Aggiungere Azioni: Eliminazione e Riordino

Le liste iOS supportano nativamente l'eliminazione (swipe a sinistra) e il riordino (drag and drop). SwiftUI rende queste operazioni banali con .onDelete e .onMove:

InterviewListView.swift (enhanced version)swift
struct InterviewListView: View {
    @State private var interviews = [/* ... data ... */]

    var body: some View {
        NavigationStack {
            List {
                ForEach(interviews) { interview in
                    InterviewRow(interview: interview)
                }
                // Swipe to delete
                .onDelete(perform: deleteInterview)
                // Drag and drop to reorder
                .onMove(perform: moveInterview)
            }
            .navigationTitle("My Interviews")
            .toolbar {
                // "Edit" button that activates edit mode
                EditButton()
            }
        }
    }

    // Deletes elements at specified indices
    private func deleteInterview(at offsets: IndexSet) {
        interviews.remove(atOffsets: offsets)
    }

    // Moves elements from one position to another
    private func moveInterview(from source: IndexSet, to destination: Int) {
        interviews.move(fromOffsets: source, toOffset: destination)
    }
}

Con sole 4 righe di codice aggiuntive (.onDelete, .onMove, EditButton e le due funzioni), si ottiene una lista completamente interattiva. Questa e la potenza di SwiftUI.

Animare le Interfacce

SwiftUI eccelle nelle animazioni. A differenza di UIKit dove le animazioni richiedevano molto codice, qui tutto e dichiarativo: si descrive lo stato finale e SwiftUI anima la transizione.

Animazioni Implicite con withAnimation

Il modo piu semplice per animare e racchiudere un cambio di stato in withAnimation. SwiftUI rileva cosa cambia e anima automaticamente le proprieta visive interessate.

AnimatedCard.swiftswift
struct AnimatedCard: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(.blue)
                // Dimensions change based on state
                .frame(
                    width: isExpanded ? 300 : 150,
                    height: isExpanded ? 200 : 100
                )

            Button(isExpanded ? "Collapse" : "Expand") {
                // withAnimation animates ALL visual changes
                // resulting from this state change
                withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
                    isExpanded.toggle()
                }
            }
            .padding(.top)
        }
    }
}

Quando si tocca il pulsante, isExpanded cambia valore. Grazie a withAnimation, il cambiamento di dimensione del rettangolo viene animato con un effetto a molla. Non e necessario specificare cosa animare: SwiftUI lo determina autonomamente.

Transizioni: Animare Comparsa e Scomparsa

Le transizioni definiscono come una view appare o scompare. Per impostazione predefinita si tratta di una dissolvenza (opacity), ma e possibile personalizzarla:

TransitionDemo.swiftswift
struct TransitionDemo: View {
    @State private var showDetails = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Show Details") {
                withAnimation(.easeInOut(duration: 0.3)) {
                    showDetails.toggle()
                }
            }

            // This view appears/disappears with a transition
            if showDetails {
                DetailCard()
                    // Asymmetric transition: different for entry and exit
                    .transition(
                        .asymmetric(
                            insertion: .scale.combined(with: .opacity),  // Entry: zoom + fade
                            removal: .slide                               // Exit: slide
                        )
                    )
            }
        }
    }
}

struct DetailCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Interview Details")
                .font(.headline)
            Text("25 questions • 45 minutes")
                .foregroundColor(.secondary)
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(16)
    }
}

In questo caso, la card appare con uno zoom dal centro (scale + opacity) e scompare scivolando lateralmente (slide). Queste micro-animazioni rendono l'interfaccia viva e professionale.

Performance

SwiftUI ottimizza automaticamente le animazioni. Preferire .spring() per un effetto naturale ed evitare animazioni troppo lunghe (> 0.5s) che possono frustrare l'utente.

Caricare Dati Asincroni

Nelle applicazioni reali, i dati provengono spesso da un'API. Swift Concurrency (async/await) si integra perfettamente con SwiftUI attraverso il modifier .task.

Il Pattern Loading / Error / Success

Ecco il pattern standard per visualizzare dati provenienti da un'API. Si gestiscono tre stati: caricamento, errore e successo.

AsyncDataView.swiftswift
struct AsyncDataView: View {
    @State private var questions: [Question] = []
    @State private var isLoading = true
    @State private var errorMessage: String?

    var body: some View {
        Group {
            if isLoading {
                // State: loading in progress
                ProgressView("Loading...")

            } else if let error = errorMessage {
                // State: error
                VStack(spacing: 16) {
                    Image(systemName: "exclamationmark.triangle")
                        .font(.largeTitle)
                        .foregroundColor(.orange)
                    Text(error)
                    Button("Retry") {
                        Task { await loadQuestions() }
                    }
                }

            } else {
                // State: success, display data
                List(questions) { question in
                    Text(question.title)
                }
            }
        }
        // .task runs automatically when the view appears
        .task {
            await loadQuestions()
        }
        // Pull-to-refresh
        .refreshable {
            await loadQuestions()
        }
    }

    private func loadQuestions() async {
        isLoading = true
        errorMessage = nil

        do {
            // Async API call
            questions = try await QuestionService.shared.fetchQuestions()
        } catch {
            errorMessage = "Unable to load questions"
        }

        isLoading = false
    }
}

Il modifier .task e essenziale: lancia un task asincrono quando la view appare e lo cancella automaticamente quando scompare. Nessun rischio di memory leak.

Conclusione

SwiftUI e diventato indispensabile per lo sviluppo iOS moderno. Con iOS 18, il framework ha raggiunto una maturita che lo rende perfettamente adatto alle applicazioni in produzione.

Punti Chiave

  • Paradigma dichiarativo: descrivere cosa si vuole, non come costruirlo
  • @State e @Binding: gestire lo stato in modo reattivo e propagarlo tra le view
  • Stack: combinare VStack, HStack e ZStack per layout flessibili
  • List: visualizzare collezioni con codice minimale e interazioni native
  • Animazioni: utilizzare withAnimation per transizioni fluide automatiche
  • Async/await: caricare i dati con .task e gestire gli stati di caricamento/errore

Checklist

  • Comprendere la differenza tra imperativo (UIKit) e dichiarativo (SwiftUI)
  • Padroneggiare i modifier e il loro ordine di applicazione
  • Sapere quando utilizzare @State vs @Binding vs @Observable
  • Costruire layout con gli Stack
  • Implementare liste con azioni (elimina, sposta)
  • Animare i cambiamenti di stato con withAnimation

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

SwiftUI apre le porte alla creazione di applicazioni eleganti su tutto l'ecosistema Apple. Il modo migliore per imparare e mettere in pratica: creare un piccolo progetto personale e sperimentare con ogni concetto presentato in questo articolo.

Tag

#swiftui
#ios
#swift
#ui
#apple

Condividi

Articoli correlati