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.dataTask→URLSession.data(for:)(iOS 15+) - Ubah signature: hapus
completion, tambahkanasync throws - Tandai model dengan
: Sendable - Wrap callback pihak ketiga dengan
withCheckedContinuation - Ubah
classmenjadistructjika 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
classmenjadiactoruntuk interactor yang punya state - Ganti
DispatchGroupdenganasync letatauwithTaskGroup - Ganti
[weak self]capture list denganTaskscoping - Gunakan
await presenter?.method()langsung — Swift auto-hop ke@MainActorvia 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
@MainActordi class declaration ViewController - Hapus semua
DispatchQueue.main.async {}(tidak diperlukan lagi) - Ganti panggilan completion-based ke
Task { await ... } - Simpan
Taskreference untuk cancel saatviewDidDisappear
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
@preconcurrencyyang tidak dibutuhkan lagi - Pastikan semua
@unchecked Sendablememang benar thread-safe - Update dependency pihak ketiga ke versi yang sudah Swift 6 compatible