cat swift/uikit-appkit-quando-servono.md

Lezione 66 50 min

UIKit e AppKit quando servono davvero

Capire quando integrare UIKit o AppKit in un progetto SwiftUI e come farlo senza contaminare dominio e casi d'uso.

SwiftUI è il default ragionevole per molte nuove interfacce Apple. Ma UIKit e AppKit non sono spariti.

Servono ancora quando:

  • una API esiste solo in UIKit/AppKit;
  • hai componenti legacy;
  • devi integrare un controller complesso;
  • serve controllo fine non ancora comodo in SwiftUI;
  • stai migrando una app esistente;
  • devi incorporare viste native specifiche.

La regola del corso: UIKit e AppKit sono adattatori di interfaccia, non luoghi dove mettere dominio.

Integrare UIKit in SwiftUI

Per incorporare una view UIKit:

import SwiftUI
import UIKit

struct CameraPreview: UIViewRepresentable {
  func makeUIView(context: Context) -> PreviewView {
    PreviewView()
  }

  func updateUIView(_ uiView: PreviewView, context: Context) {
  }
}

Per un controller:

struct DocumentScannerView: UIViewControllerRepresentable {
  let onScan: ([ScannedPage]) -> Void

  func makeUIViewController(context: Context) -> ScannerViewController {
    let controller = ScannerViewController()
    controller.delegate = context.coordinator
    return controller
  }

  func updateUIViewController(_ controller: ScannerViewController, context: Context) {
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(onScan: onScan)
  }

  final class Coordinator: NSObject, ScannerDelegate {
    let onScan: ([ScannedPage]) -> Void

    init(onScan: @escaping ([ScannedPage]) -> Void) {
      self.onScan = onScan
    }

    func scannerDidFinish(pages: [ScannedPage]) {
      onScan(pages)
    }
  }
}

Il coordinator traduce delegate UIKit in evento SwiftUI.

Integrare SwiftUI in UIKit

In app legacy, puoi portare SwiftUI dentro UIKit:

let view = ProductDetailScreen(viewModel: viewModel)
let controller = UIHostingController(rootView: view)
navigationController?.pushViewController(controller, animated: true)

Questo permette migrazioni progressive.

Non devi riscrivere tutta l’app per usare SwiftUI in una feature nuova.

AppKit

Su macOS, l’integrazione segue lo stesso principio:

struct LegacyTableView: NSViewRepresentable {
  func makeNSView(context: Context) -> NSTableView {
    NSTableView()
  }

  func updateNSView(_ tableView: NSTableView, context: Context) {
  }
}

AppKit resta importante per:

  • menu avanzati;
  • finestre personalizzate;
  • documenti;
  • drag and drop complesso;
  • integrazioni storiche macOS;
  • controlli molto specifici.

Dove mettere il codice bridge

Organizzazione consigliata:

Presentation/
  SwiftUI/
  UIKitAdapters/
  AppKitAdapters/

Application/
  UseCases/

Domain/
  Models/

Il bridge non deve importare infrastruttura se non serve. Deve parlare con view model o callback.

Evitare domain leakage

Questo è un cattivo segnale:

final class CheckoutViewController: UIViewController {
  func pay() {
    let dto = PaymentDTO(...)
    let url = URL(string: "https://api.example.com/pay")!
    ...
  }
}

Il controller sta facendo UI, DTO, networking e dominio.

Meglio:

@MainActor
final class CheckoutViewModel {
  private let checkout: CheckoutUseCase

  func pay() async {
    ...
  }
}

Il controller o la view SwiftUI chiama il view model.

Delegate e async

Molte API UIKit/AppKit sono basate su delegate. Puoi adattarle a closure o async sequence.

Closure:

final class PickerCoordinator: NSObject, UIImagePickerControllerDelegate {
  let onImage: (UIImage) -> Void

  init(onImage: @escaping (UIImage) -> Void) {
    self.onImage = onImage
  }
}

Async stream concettuale:

struct SelectionEvents {
  let stream: AsyncStream<Selection>
}

L’obiettivo è non far trapelare delegate ovunque.

Thread e MainActor

UIKit e AppKit sono UI framework. Gli aggiornamenti devono stare sul main actor.

@MainActor
func showResult(_ result: ScanResult) {
  state = .loaded(result)
}

Non passare oggetti UI dentro task concorrenti. Estrai dati, poi inviali al dominio.

Quando usare ancora UIKit puro

UIKit puro ha senso quando:

  • il team ha una app grande già UIKit;
  • serve compatibilità con componenti esistenti;
  • la schermata è costruita intorno a un controller legacy;
  • SwiftUI introduce workaround peggiori del problema;
  • stai facendo manutenzione evolutiva.

Il corso non deve trasformare UIKit in un nemico. Deve insegnare giudizio.

Checklist

Integra UIKit/AppKit quando:

  • esiste un bisogno reale;
  • il bridge è isolato;
  • dominio e casi d’uso restano fuori;
  • delegate vengono tradotti in eventi chiari;
  • aggiornamenti UI stanno sul main actor;
  • la migrazione può essere progressiva;
  • il codice legacy non detta la forma del dominio.

Esercizio

Prendi una feature SwiftUI e immagina di dover usare un componente UIKit.

Costruisci:

  1. un wrapper UIViewControllerRepresentable;
  2. un Coordinator;
  3. una closure di output;
  4. un view model che riceve l’evento;
  5. un caso d’uso chiamato dal view model.

Obiettivo: il componente UIKit deve restare al bordo. Il dominio non deve sapere che UIKit esiste.