cat swift/struttura-package-swift.md

Lezione 40 45 min

Struttura di un package Swift

Sources, Tests, target, prodotti e nomi: organizzare un package in modo leggibile evita caos quando il codice cresce.

Un package Swift ben strutturato comunica il progetto prima ancora di leggere il codice.

La struttura minima generata da SwiftPM è semplice, ma appena il progetto cresce devi prendere decisioni:

  • quanti target?
  • quali prodotti?
  • dove metto il dominio?
  • dove metto networking?
  • dove metto test fake e fixture?
  • cosa deve essere pubblico?

Struttura base

CouponKit/
  Package.swift
  Sources/
    CouponKit/
      Coupon.swift
  Tests/
    CouponKitTests/
      CouponTests.swift

Il nome della cartella dentro Sources corrisponde di solito al nome del target.

Il nome della cartella dentro Tests corrisponde al nome del test target.

Package.swift

Un manifest essenziale:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
  name: "CouponKit",
  platforms: [
    .macOS(.v14),
    .iOS(.v17)
  ],
  products: [
    .library(name: "CouponKit", targets: ["CouponKit"])
  ],
  targets: [
    .target(name: "CouponKit"),
    .testTarget(name: "CouponKitTests", dependencies: ["CouponKit"])
  ]
)

La riga swift-tools-version indica la versione degli strumenti SwiftPM richiesta per leggere il manifest.

Target singolo o più target?

All’inizio basta un target:

Sources/CouponKit/

Ma se il package cresce, puoi separare:

Sources/
  CouponCore/
  CouponHTTP/
  CouponCLI/

Esempio:

  • CouponCore: dominio puro;
  • CouponHTTP: client API;
  • CouponCLI: eseguibile demo.

Questa separazione riduce dipendenze inutili. Il dominio non deve dipendere da HTTP.

Prodotti

Un product è ciò che il package espone.

products: [
  .library(name: "CouponCore", targets: ["CouponCore"]),
  .library(name: "CouponHTTP", targets: ["CouponHTTP"]),
  .executable(name: "coupon-cli", targets: ["CouponCLI"])
]

Non tutti i target devono diventare product pubblici. Alcuni possono essere solo dettagli interni.

Dipendenze tra target

targets: [
  .target(name: "CouponCore"),
  .target(
    name: "CouponHTTP",
    dependencies: ["CouponCore"]
  ),
  .executableTarget(
    name: "CouponCLI",
    dependencies: ["CouponCore", "CouponHTTP"]
  )
]

La direzione conta:

CouponHTTP può dipendere da CouponCore.

CouponCore non dovrebbe dipendere da CouponHTTP.

Layout dei file

Evita file enormi come:

CouponKit.swift

Meglio:

Sources/CouponCore/
  Coupon.swift
  Sconto.swift
  RegolaCoupon.swift
  CouponEngine.swift
  ErroreCoupon.swift

Il file system deve aiutare a navigare il dominio.

Test layout

Tests/CouponCoreTests/
  CouponEngineTests.swift
  ScontoTests.swift
  Fixtures/

I test dovrebbero seguire la struttura del codice. Se hai più target, crea test target separati quando ha senso.

Fixtures

Per dati di test puoi usare helper Swift:

extension Coupon {
  static let welcome10 = Coupon(...)
}

Oppure file JSON in Resources, se stai testando decoding.

Non mettere fixture casuali dentro i test senza nome: rendono difficile capire l’intenzione.

Resources

SwiftPM supporta risorse nei target:

.testTarget(
  name: "CouponHTTPTests",
  dependencies: ["CouponHTTP"],
  resources: [
    .process("Fixtures")
  ]
)

Utile per JSON, immagini, file campione.

Checklist

Per una struttura sana:

  • il dominio vive in un target senza dipendenze inutili;
  • networking e UI non contaminano il core;
  • i nomi dei target sono chiari;
  • i product esposti sono intenzionali;
  • i test seguono i target;
  • i file sono piccoli e nominati per concetto;
  • le fixture sono organizzate.

Esercizio

Riorganizza CouponKit in:

Sources/CouponCore
Sources/CouponHTTP
Sources/CouponCLI
Tests/CouponCoreTests
Tests/CouponHTTPTests

Poi aggiorna Package.swift con:

  • una library CouponCore;
  • una library CouponHTTP;
  • un executable coupon-cli;
  • test target per core e HTTP.