cat swift/swift-oltre-ios-nucleo-condiviso.md
Swift oltre iOS: un nucleo, più destinazioni
Capire come lo stesso nucleo Swift può vivere in app native, server, CLI e librerie senza riscrivere il dominio ogni volta.
Swift non è solo “il linguaggio per fare app iPhone”.
Nel corso lo trattiamo come linguaggio di sistema e di prodotto:
- app iOS e macOS con SwiftUI;
- integrazione UIKit e AppKit quando serve;
- backend con Vapor;
- CLI e automazioni con Swift Package Manager;
- librerie pubblicabili con API stabile e DocC.
Il punto non è imparare quattro mondi separati. Il punto è capire quale parte resta uguale.
Il nucleo condiviso
Un buon nucleo Swift contiene:
- tipi di dominio;
- regole;
- errori;
- protocolli applicativi;
- casi d’uso;
- test;
- mapping quando appartiene al dominio;
- contratti pubblici.
Esempio:
public struct TaskItem: Equatable, Sendable, Identifiable {
public let id: ID
public var title: String
public var isCompleted: Bool
public struct ID: RawRepresentable, Equatable, Hashable, Sendable {
public let rawValue: String
public init(_ rawValue: String) {
self.rawValue = rawValue
}
public init(rawValue: String) {
self.rawValue = rawValue
}
}
}
Questo tipo può vivere in:
- una view SwiftUI;
- una route Vapor;
- un comando CLI;
- una libreria riusabile.
Non contiene View, Request, FileManager o URLSession.
Destinazioni diverse, adattatori diversi
Lo stesso caso d’uso:
public protocol TaskRepository: Sendable {
func all() async throws -> [TaskItem]
func save(_ task: TaskItem) async throws
}
public struct CompleteTaskUseCase: Sendable {
private let repository: TaskRepository
public init(repository: TaskRepository) {
self.repository = repository
}
public func execute(id: TaskItem.ID) async throws {
var tasks = try await repository.all()
guard let index = tasks.firstIndex(where: { $0.id == id }) else {
throw TaskError.notFound(id)
}
tasks[index].isCompleted = true
try await repository.save(tasks[index])
}
}
Può essere usato da una app, un server o una CLI.
Cambiano gli adattatori:
SwiftUI app -> LocalTaskRepository / HTTPTaskRepository
Vapor app -> DatabaseTaskRepository
CLI -> JSONFileTaskRepository
Library -> protocollo pubblico e implementazioni opzionali
Perché partire dal nucleo
Se parti dalla UI, rischi di modellare il dominio come stato grafico.
Se parti dal database, rischi di modellare il dominio come tabelle.
Se parti dall’API, rischi di modellare il dominio come JSON.
Swift ti da tipi ricchi. Usali per rappresentare il problema prima della destinazione.
Esempio: task manager
Dominio:
public enum TaskPriority: String, Codable, Sendable {
case low
case normal
case high
}
public struct TaskDraft: Equatable, Sendable {
public let title: String
public let priority: TaskPriority
public init(title: String, priority: TaskPriority) throws {
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmedTitle.isEmpty == false else {
throw TaskError.emptyTitle
}
self.title = trimmedTitle
self.priority = priority
}
}
Questa regola deve valere ovunque:
- form SwiftUI;
- endpoint HTTP;
- comando terminale;
- test di libreria.
Non duplicarla nelle destinazioni.
Application layer
I casi d’uso stanno sopra il dominio e sotto le interfacce.
public struct CreateTaskUseCase: Sendable {
private let repository: TaskRepository
private let idGenerator: @Sendable () -> TaskItem.ID
public init(
repository: TaskRepository,
idGenerator: @escaping @Sendable () -> TaskItem.ID
) {
self.repository = repository
self.idGenerator = idGenerator
}
public func execute(draft: TaskDraft) async throws -> TaskItem {
let task = TaskItem(
id: idGenerator(),
title: draft.title,
isCompleted: false
)
try await repository.save(task)
return task
}
}
L’ID generator è iniettato per testare senza UUID() reale.
Quattro tracce
Nel modulo vedremo:
iOS/macOS
SwiftUI, stato, lifecycle, MainActor
UIKit/AppKit
integrazione pragmatica quando SwiftUI non basta
Server
Vapor, route, database, deploy
CLI/tooling
ArgumentParser, file system, automazioni
Library authoring
package, API pubblica, SemVer, DocC
Ogni traccia usa Swift in modo diverso, ma non dovrebbe reinventare il dominio.
Il rischio dei corsi applicativi
Molti corsi Swift diventano immediatamente corsi iOS.
Questo produce due problemi:
- lo studente confonde Swift con SwiftUI;
- le regole finiscono nelle view.
Qui facciamo l’opposto:
- dominio;
- casi d’uso;
- adattatori;
- interfaccia o runtime.
SwiftUI, Vapor e CLI arrivano dopo, come destinazioni.
Checklist
Prima di scegliere una traccia, chiedi:
- quali tipi sono davvero del dominio?
- quali regole devono valere ovunque?
- quali dipendenze cambiano tra app, server e CLI?
- quale parte posso testare senza UI o rete?
- quale parte deve essere pubblica?
- quale parte è solo un adattatore?
Esercizio
Scegli un dominio piccolo:
- task manager;
- catalogo prodotti;
- prenotazioni;
- budget personale;
- quiz;
- inventario.
Definisci:
- tre tipi di dominio;
- due errori;
- un protocollo repository;
- un caso d’uso;
- un test del caso d’uso con fake repository.
Poi scrivi una riga per ogni destinazione:
In SwiftUI questo caso d'uso viene chiamato da...
In Vapor viene chiamato da...
In CLI viene chiamato da...
In una libreria viene esposto come...