Swift è il linguaggio di programmazione di Apple che combina la potenza di Objective-C con la sicurezza dei linguaggi moderni. A differenza di molti altri linguaggi, Swift offre costruzioni uniche: i tipi valore con copia automatica, gli Actor con isolamento a livello di compilatore, i Property Wrappers per riutilizzare la logica delle proprietà e la Programmazione Orientata ai Protocolli come alternativa all'ereditarietà classica. Secondo la Documentazione Apple Swift, queste caratteristiche rendono il codice più sicuro e performante.
Punti Chiave
In Swift, struct è un tipo valore, mentre class è un tipo riferimento. Questa differenza fondamentale determina il comportamento durante l'assegnazione, il passaggio alle funzioni e la gestione della memoria. Struct viene copiato interamente a ogni assegnazione, class viene passato per riferimento — viene copiato solo il puntatore all'oggetto nell'heap.
Struct implementa automaticamente la semantica di valore — ogni istanza è indipendente. La mutazione di una proprietà di struct è possibile solo tramite var, e i metodi che modificano le proprietà devono essere contrassegnati come mutating. Struct non supporta l'ereditarietà ma può conformarsi ai protocolli. La libreria standard di Swift usa struct per String, Array, Dictionary, Int, Bool — tutti i tipi fondamentali sono tipi valore.
struct User {
let id: Int
var name: String
mutating func updateName(_ newName: String) {
name = newName
}
}
var user1 = User(id: 1, name: "Alice")
var user2 = user1
user2.updateName("Bob")
// user1.name === "Alice", user2.name === "Bob"
// Copia completa — istanze indipendenti
Class viene memorizzato nell'heap e supporta l'uguaglianza per riferimento. Più variabili possono riferirsi allo stesso oggetto, le modifiche tramite un riferimento sono visibili attraverso tutti gli altri. Class supporta l'ereditarietà, permette l'override dei metodi (override), ha deinit per la pulizia delle risorse. In Swift, class è usata meno frequentemente di struct — Apple raccomanda di iniziare con struct e passare a class solo quando sono necessarie ereditarietà o semantica di riferimento.
class Car {
let model: String
var speed: Int
init(model: String, speed: Int) {
self.model = model
self.speed = speed
}
func accelerate(_ amount: Int) {
speed += amount
}
}
let car1 = Car(model: "Tesla", speed: 0)
let car2 = car1
car2.accelerate(50)
// car1.speed === 50, car2.speed === 50
// Stesso riferimento — modifiche visibili ovunque
La scelta tra struct e class dipende dalla semantica dei dati. Per configurazioni, modelli dati e stati usa struct — la copia è più sicura e prevedibile. Per oggetti con identità (sessione utente, connessione database) e quando serve ereditarietà, usa class.
Actor è un tipo riferimento che protegge il suo stato mutabile dall'accesso concorrente. Il compilatore Swift garantisce che le variabili di actor siano accessibili solo dallo stesso contesto dell'actor — qualsiasi accesso esterno causa un errore di compilazione. Questo previene le race condition a livello di linguaggio, senza lock manuali.
Tutte le proprietà e i metodi di actor sono isolati per impostazione predefinita. Chiamare un metodo di actor dall'esterno richiede await — Swift commuta l'esecuzione all'actor e viceversa. L'actor sincronizza l'accesso sequenzialmente: le richieste da diverse attività vengono messe in coda. Questo garantisce uno stato coerente all'interno dell'actor. I metodi non isolati (nonisolated) possono essere chiamati senza await ma non possono accedere alle proprietà mutabili.
actor BankAccount {
private var balance: Double
init(initialBalance: Double) {
balance = initialBalance
}
func deposit(amount: Double) {
balance += amount
}
func withdraw(amount: Double) throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
}
nonisolated func accountInfo() -> String {
return "Conto Bancario"
}
}
let account = BankAccount(initialBalance: 1000)
await account.deposit(amount: 500)
// balance = 1500 — atomico, senza race condition
Actor è utile per gestori di stato, cache e servizi di rete dove più attività leggono e scrivono dati concorrentemente. A differenza della sincronizzazione manuale tramite NSLock o DispatchQueue, l'actor viene verificato in fase di compilazione — è impossibile sbagliare. Gli errori di isolamento appaiono in fase di compilazione, non in fase di esecuzione.
Property Wrapper è un'annotazione che aggiunge comportamento a una proprietà senza modificarne la dichiarazione. SwiftUI e Combine forniscono wrapper integrati per la gestione dello stato delle viste. Il compilatore genera codice che incapsula l'accesso alla proprietà, aggiungendo logica di osservazione, sincronizzazione o trasformazione.
@Published viene usato nelle classi che conformano a ObservableObject per pubblicare automaticamente i cambiamenti. Gli abbonati Combine ricevono notifiche a ogni aggiornamento. @State è lo stato locale di una vista SwiftUI. SwiftUI ridisegna automaticamente la vista quando @State cambia. @Binding crea una connessione bidirezionale tra una vista padre e figlia senza possedere i dati.
import SwiftUI
import Combine
@main
struct CounterApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class CounterViewModel: ObservableObject {
@Published var count: Int = 0
func increment() {
count += 1
}
}
struct ContentView: View {
@State private var greeting: String = "Ciao"
@StateObject private var viewModel = CounterViewModel()
var body: some View {
VStack {
CounterDisplay(count: $viewModel.count)
Text(greeting)
Button("Incrementa", action: viewModel.increment)
}
}
}
struct CounterDisplay: View {
@Binding var count: Int
var body: some View {
Text("Conteggio: \(count)")
}
}
I Property Wrappers personalizzati vengono creati con @propertyWrapper e una struttura con un campo wrappedValue. Questo permette di riutilizzare la logica: validazione, normalizzazione, archiviazione UserDefaults. I Property Wrappers riducono significativamente il codice boilerplate e rendono esplicita l'intenzione dello sviluppatore tramite annotazione.
@propertyWrapper
struct Clamped<T: Comparable> {
private var value: T
let range: ClosedRange<T>
init(wrappedValue: T, range: ClosedRange<T>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: T {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
struct Settings {
@Clamped(range: 0...100) var volume: Int = 50
}
var s = Settings()
s.volume = 150
// s.volume === 100 — valore automaticamente limitato
Generics è un potente meccanismo di Swift per scrivere codice flessibile senza perdere la sicurezza dei tipi. Funzioni e tipi generici funzionano con qualsiasi tipo che soddisfi i vincoli. La Programmazione Orientata ai Protocolli (POP) è un paradigma Swift dove protocol + extension sostituiscono l'ereditarietà classica delle classi, fornendo composizione di comportamento.
Swift permette di vincolare i parametri generici con protocolli (clausole where). Protocollo con tipo associato è l'equivalente generico per i protocolli: il tipo concreto viene determinato al momento della conformità. Questa è la base per collezioni e algoritmi type-safe. La libreria standard di Swift usa attivamente i generics: Array, Dictionary, Optional — sono tutti tipi generici.
protocol Cacheable {
associatedtype Key: Hashable
associatedtype Value
func get(_ key: Key) -> Value?
mutating func set(_ value: Value, for key: Key)
}
struct MemoryCache<K: Hashable, V>: Cacheable {
private var storage: [K: V] = [:]
typealias Key = K
typealias Value = V
func get(_ key: K) -> V? {
return storage[key]
}
mutating func set(_ value: V, for key: K) {
storage[key] = value
}
}
func firstMatch<T: Equatable>(in array: [T], target: T) -> Int? {
for (index, item) in array.enumerated() {
if item == target {
return index
}
}
return nil
}
let numbers = [10, 20, 30, 40]
let index = firstMatch(in: numbers, target: 30)
// index === 2 — un metodo generico per tutti i tipi Equatable
L'extension di protocollo è un meccanismo chiave di POP. In un'estensione si può fornire un'implementazione predefinita dei metodi del protocollo. I tipi che conformano al protocollo ottengono questa implementazione gratuitamente e possono sovrascriverla se necessario. Questo permette di costruire gerarchie di comportamento senza ereditarietà di classi — un'alternativa all'ereditarietà multipla, che non esiste in Swift.
protocol Drivable {
var speed: Double { get set }
func drive()
}
extension Drivable {
func drive() {
if speed > 0 {
print("Guidando a \(speed) km/h")
}
}
}
struct Bicycle: Drivable {
var speed: Double = 15
}
struct Plane: Drivable {
var speed: Double = 900
func drive() {
print("Volando a \(speed) km/h")
}
}
let bike = Bicycle()
bike.drive() // "Guidando a 15.0 km/h" — predefinita
let plane = Plane()
plane.drive() // "Volando a 900.0 km/h" — sovrascritta
POP con extension permette la composizione attraverso molteplici protocolli — un tipo può conformarsi a diversi protocolli, ottenendo le loro implementazioni. Questo è più flessibile dell'ereditarietà singola delle classi, dove l'intera gerarchia è fissa. La libreria standard di Swift è costruita su POP: CustomStringConvertible, Equatable, Hashable, Codable — sono tutti protocolli con estensioni.
Enum in Swift è una costruzione significativamente più potente che in altri linguaggi. Ogni caso può avere valori associati — un insieme arbitrario di dati di qualsiasi tipo. Combinato con extension e pattern matching, enum diventa la base per modellare stati, risultati di operazioni e macchine a stati finiti.
I valori associati permettono a ogni caso di memorizzare dati unici. Ad esempio, il caso loading non contiene dati, success contiene il risultato, failure contiene un errore. Il pattern matching tramite switch estrae i valori associati e gestisce ogni caso. Il compilatore verifica l'esaurività (exhaustive switch) — questo elimina gli stati dimenticati.
enum NetworkResult<T> {
case loading
case success(T)
case failure(Error)
}
enum MediaFile {
case image(url: URL, width: Int, height: Int)
case video(url: URL, duration: Double)
case audio(url: URL, bitrate: Int)
}
func handle(_ result: NetworkResult<String>) {
switch result {
case .loading:
print("Caricamento...")
case .success(let data):
print("Ricevuto: \(data)")
case .failure(let error):
print("Errore: \(error.localizedDescription)")
}
}
let file = MediaFile.video(url: URL(string: "https://example.com/video.mp4")!, duration: 120)
if case .video(let url, let duration) = file {
print("Video \(url) durata \(duration)s")
}
Swift permette di aggiungere metodi, proprietà calcolate e conformità ai protocolli tramite extension per enum. Questo non richiede di modificare la dichiarazione originale — il codice rimane pulito e modulare. I metodi nell'estensione possono lavorare con valori associati, calcolare dati derivati e implementare logica di business.
extension MediaFile {
var fileName: String {
switch self {
case .image(let url, _, _),
.video(let url, _),
.audio(let url, _):
return url.lastPathComponent
}
}
var description: String {
switch self {
case .image(_, let w, let h):
return "Immagine \(w)x\(h)"
case .video(_, let d):
return "Video \(d)s"
case .audio(_, let b):
return "Audio \(b) kbps"
}
}
}
extension MediaFile: Equatable {
static func == (lhs: MediaFile, rhs: MediaFile) -> Bool {
return lhs.fileName == rhs.fileName
}
}
let files: [MediaFile] = [file]
files.forEach { print($0.description) }
// "Video 120.0s"
Enum con valori associati + extension è il sostituto delle classi sealed di Kotlin o delle unioni discriminate di altri linguaggi. Swift usa questa combinazione per Result (un tipo standard nella libreria standard), Optional (Optional è un enum con .none e .some) e la gestione degli stati in SwiftUI. Il pattern matching garantisce che tutti gli stati siano gestiti — gli errori a runtime sono impossibili.
Domande Frequenti
Struct — tipo valore, copiato nell'assegnazione, non supporta ereditarietà. Class — tipo riferimento, passato per riferimento, supporta ereditarietà, deinit e controllo d'identità (===). Apple raccomanda di iniziare con struct e passare a class solo quando è necessaria la semantica di riferimento.
Actor — tipo riferimento thread-safe. Il compilatore impedisce l'accesso concorrente allo stato mutabile da diversi contesti. Usa Actor per gestori di stato, cache e servizi accessibili da più attività. Chiamare metodi di actor richiede await.
Principali: @State — stato locale della vista; @Binding — connessione ai dati senza possesso; @Published — pubblicazione delle modifiche in ObservableObject; @StateObject — possesso di ObservableObject; @EnvironmentObject — dipendenza dall'ambiente. I wrapper personalizzati si creano tramite @propertyWrapper.
POP — un paradigma dove protocol + extension definiscono il comportamento invece dell'ereditarietà di classe. Un tipo si conforma a più protocolli, ottenendo le loro implementazioni gratuitamente. È composizione invece di ereditarietà — più flessibile e sicuro. La libreria standard di Swift (Equatable, Codable) è costruita su POP.
Enum in Swift supporta valori associati — ogni caso può memorizzare dati arbitrari di tipi diversi. Combinato con il pattern matching (switch), fornisce un potente modello per stati, errori e Domain-Driven Design. Optional e Result sono enum standard di Swift con valori associati.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.