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.

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.
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:
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:
// 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 spaceLa 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.
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.
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:
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.
// 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:
// 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.
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:
// 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:
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:
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:
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.
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:
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.
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.
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
withAnimationper transizioni fluide automatiche - Async/await: caricare i dati con
.taske 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
Condividi
Articoli correlati

Performance SwiftUI: Ottimizzazione di LazyVStack e Liste Complesse
Tecniche di ottimizzazione per LazyVStack e liste SwiftUI. Ridurre il consumo di memoria, migliorare le performance di scroll ed evitare errori comuni.

ViewModifier personalizzati in SwiftUI: pattern riutilizzabili per Design System
Costruisci ViewModifier personalizzati in SwiftUI per un design system coerente. Pattern, best practice ed esempi pratici per stilizzare le view iOS in modo efficiente.

SwiftUI @Observable vs @State: Quando Usare Cosa nel 2026
Padroneggia le differenze tra @Observable e @State in SwiftUI per scegliere lo strumento di gestione dello stato giusto per le app iOS.