Section 10/132 menit

10. Migration Guide

10. Migration Guide

Strategi: Layer by Layer, Bukan Big Bang

Jangan coba migrasi seluruh project sekaligus. Migrasi per layer dimulai dari yang paling "dalam" (Worker/Service), karena layer tersebut paling sedikit dependency-nya.

swift
Layer luar (ViewController)  ←── migrasi terakhir
          ↑
Layer tengah (Interactor/Presenter) ←── setelah Worker selesai
          ↑
Layer dalam (Worker/Service)  ←── MULAI DI SINI

Phase 1 — Identifikasi & Persiapan

Aktifkan concurrency warning di Xcode:

swift
Build Settings → Other Swift Flags → tambahkan:
-warn-concurrency
-Xfrontend -strict-concurrency=targeted

Atau di Package.swift:

swift
.target(
    name: "MyApp",
    swiftSettings: [
        .unsafeFlags(["-warn-concurrency",
                      "-Xfrontend", "-strict-concurrency=targeted"])
    ]
)

Checklist Phase 1:

  • Audit kode: cari semua DispatchQueue, DispatchGroup, completion handler
  • Catat semua class yang diakses dari multiple thread
  • Identifikasi data model yang perlu Sendable
  • Tentukan target iOS minimum (iOS 15 diperlukan untuk async/await)
  • Aktifkan concurrency warning di Xcode/SPM

Phase 2 — Migrasi Layer Worker/Service

Layer ini paling mudah karena biasanya stateless.

Sebelum:

swift
class UserService {
    func fetchUser(id: String,
                   completion: @escaping (Result<User, Error>) -> Void) {
        URLSession.shared.dataTask(with: makeURL(id: id)) { data, _, error in
            if let error = error { completion(.failure(error)); return }
            guard let data = data else { completion(.failure(AppError.noData)); return }
            do {
                let user = try JSONDecoder().decode(User.self, from: data)
                completion(.success(user))
            } catch {
                completion(.failure(error))
            }
        }.resume()
    }
}

Sesudah:

swift
struct UserService {  // struct, bukan class — stateless
    func fetchUser(id: String) async throws -> User {
        let (data, _) = try await URLSession.shared.data(from: makeURL(id: id))
        return try JSONDecoder().decode(User.self, from: data)
    }
}

Checklist Phase 2:

  • Ganti URLSession.dataTaskURLSession.data(for:) (iOS 15+)
  • Ubah signature: hapus completion, tambahkan async throws
  • Tandai model dengan : Sendable
  • Wrap callback pihak ketiga dengan withCheckedContinuation
  • Ubah class menjadi struct jika tidak punya mutable state

Phase 3 — Migrasi Layer Interactor/UseCase

Sebelum:

swift
class ProfileInteractor {
    var presenter: ProfilePresentationLogic?

    func fetchProfile(request: ProfileRequest) {
        let group = DispatchGroup()
        var fetchedUser: User?
        var fetchedPosts: [Post] = []

        group.enter()
        UserService().fetchUser(id: request.userId) { result in
            fetchedUser = try? result.get()
            group.leave()
        }

        group.enter()
        PostService().fetchPosts { result in
            fetchedPosts = (try? result.get()) ?? []
            group.leave()
        }

        group.notify(queue: .main) { [weak self] in
            guard let user = fetchedUser else { return }
            let response = ProfileResponse(user: user, posts: fetchedPosts)
            self?.presenter?.presentProfile(response: response)
        }
    }
}

Sesudah:

swift
actor ProfileInteractor {
    weak var presenter: ProfilePresentationLogic?
    private let userService = UserService()
    private let postService = PostService()

    func fetchProfile(request: ProfileRequest) async {
        do {
            async let user = userService.fetchUser(id: request.userId)
            async let posts = postService.fetchPosts(userId: request.userId)
            let response = ProfileResponse(user: try await user,
                                           posts: try await posts)
            // Swift otomatis hop ke @MainActor karena method di protocol adalah @MainActor
            await presenter?.presentProfile(response: response)
        } catch {
            await presenter?.presentError(error: error)
        }
    }
}

Checklist Phase 3:

  • Ubah class menjadi actor untuk interactor yang punya state
  • Ganti DispatchGroup dengan async let atau withTaskGroup
  • Ganti [weak self] capture list dengan Task scoping
  • Gunakan await presenter?.method() langsung — Swift auto-hop ke @MainActor via protocol

Phase 4 — Migrasi Layer ViewController

Sebelum:

swift
class ProfileViewController: UIViewController {
    func fetchProfile() {
        showLoading()
        interactor.fetchProfile(request: request) // completion-based
    }

    // Dipanggil dari interactor via completion
    func displayProfile(viewModel: ProfileViewModel) {
        DispatchQueue.main.async {  // harus manual dispatch ke main
            self.hideLoading()
            self.nameLabel.text = viewModel.displayName
        }
    }
}

Sesudah:

swift
@MainActor  // Seluruh class di main thread
class ProfileViewController: UIViewController {
    func fetchProfile() {
        showLoading()
        Task {
            await interactor.fetchProfile(request: request)
        }
    }

    // Dipanggil dari presenter yang sudah @MainActor
    func displayProfile(viewModel: ProfileViewModel) {
        // Tidak perlu DispatchQueue.main.async — sudah di @MainActor
        hideLoading()
        nameLabel.text = viewModel.displayName
    }
}

Checklist Phase 4:

  • Tambahkan @MainActor di class declaration ViewController
  • Hapus semua DispatchQueue.main.async {} (tidak diperlukan lagi)
  • Ganti panggilan completion-based ke Task { await ... }
  • Simpan Task reference untuk cancel saat viewDidDisappear

Phase 5 — Persiapan Swift 6 (Opsional, Per-Target)

Aktifkan Swift 6 mode per-target atau per-file untuk validasi lebih ketat:

Per-target di Xcode:

swift
Build Settings → Swift Language Version → Swift 6

Per-file (eksperimental):

swift
// Tambahkan di atas file
// swift-language-version: 6

Checklist Phase 5:

  • Enable Swift 6 mode di satu target non-produksi dulu
  • Fix semua warning yang kini menjadi error
  • Ganti @preconcurrency yang tidak dibutuhkan lagi
  • Pastikan semua @unchecked Sendable memang benar thread-safe
  • Update dependency pihak ketiga ke versi yang sudah Swift 6 compatible