cat swift/swift-oltre-ios-nucleo-condiviso.md

Lezione 64 50 min

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:

  1. dominio;
  2. casi d’uso;
  3. adattatori;
  4. 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:

  1. tre tipi di dominio;
  2. due errori;
  3. un protocollo repository;
  4. un caso d’uso;
  5. 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...