Swift

Clean Swift untuk Startup: Studi Kasus Sukses Arsitektur yang Skalabel

Clean Swift untuk Startup: Studi Kasus Implementasi

PPLG

PPLG

Penulis

06 May 2026
1 x dilihat

Di dunia startup yang bergerak cepat, membangun aplikasi yang solid dan dapat diskalakan adalah kunci kesuksesan. Salah satu tantangan terbesar adalah menjaga basis kode tetap terorganisir seiring pertumbuhan tim dan kompleksitas fitur. Di sinilah arsitektur Clean Swift hadir sebagai solusi ampuh. Artikel ini akan membawa Anda melalui studi kasus implementasi Clean Swift dalam proyek startup, menjelaskan konsep intinya, memberikan langkah-langkah praktis, dan membagikan tips yang akan membantu Anda menguasai arsitektur ini.

Mengapa Clean Swift Penting untuk Startup?

Startup seringkali beroperasi dengan sumber daya terbatas dan tenggat waktu yang ketat. Arsitektur yang dipilih harus mendukung pengembangan yang cepat, pemeliharaan yang mudah, dan kemampuan untuk beradaptasi dengan perubahan kebutuhan. Clean Swift, yang terinspirasi dari Clean Architecture Robert C. Martin dan MVVM, menawarkan beberapa keuntungan signifikan:

  • Pemisahan Tanggung Jawab (Separation of Concerns): Setiap komponen memiliki peran yang jelas, membuat kode lebih mudah dipahami dan di-debug.
  • Testabilitas Tinggi: Desain yang modular memfasilitasi penulisan unit test yang efektif, krusial untuk menjaga kualitas kode.
  • Skalabilitas: Struktur yang terorganisir dengan baik memungkinkan penambahan fitur baru tanpa mengacaukan kode yang ada.
  • Kolaborasi Tim yang Efisien: Dengan aturan yang jelas, anggota tim dapat bekerja pada bagian yang berbeda secara bersamaan dengan lebih sedikit konflik.

Konsep Inti Clean Swift

Clean Swift membagi aplikasi menjadi beberapa lapisan logis:

  1. Entities: Objek data murni yang mewakili domain bisnis aplikasi.
  2. Use Cases (Interactors): Menangani logika bisnis spesifik. Mereka berkomunikasi dengan Entities dan presenter.
  3. Interface Adapters:
    • Presenters: Mengambil data dari Use Cases dan memformatnya untuk ditampilkan oleh View Controllers.
    • Controllers (View Controllers): Mengelola interaksi pengguna dan menampilkan data yang diformat oleh Presenter.
    • Routers: Bertanggung jawab untuk navigasi antar layar.
  4. Frameworks & Drivers: Lapisan terluar yang berisi UI (UIKit/SwiftUI), database, jaringan, dll.

Struktur ini sering digambarkan dalam bentuk "Clean Architecture Diagram" atau yang lebih spesifik dalam konteks iOS, "Clean Swift Scene".

Scene dalam Clean Swift

Dalam Clean Swift, satu fitur atau layar dalam aplikasi biasanya dipecah menjadi sebuah "Scene". Setiap Scene terdiri dari kumpulan protokol dan kelas yang saling berkomunikasi:

  • ViewController: Bertanggung jawab atas tampilan dan event UI.
  • Interactor: Mengandung logika bisnis untuk Scene tersebut.
  • Presenter: Memformat data untuk ditampilkan oleh ViewController dan memicu event navigasi melalui Router.
  • Router: Menangani navigasi antar Scene.
  • Worker: (Opsional) Bertanggung jawab untuk melakukan operasi data spesifik (misalnya, panggilan API, akses database).

Studi Kasus: Fitur "Daftar Produk" pada Startup E-commerce

Mari kita ambil contoh implementasi fitur "Daftar Produk" untuk aplikasi e-commerce startup.

1. Pembuatan Scene Menggunakan Clean Swift Templates

Sebagian besar developer iOS yang menggunakan Clean Swift memanfaatkan generator template yang tersedia di Xcode. Ini sangat mempercepat proses pembuatan struktur awal untuk setiap Scene. Biasanya, generator ini akan membuat file-file berikut:

  • [NamaScene]Models.swift: Untuk request dan response data antar komponen.
  • [NamaScene]ViewController.swift: Controller utama.
  • [NamaScene]Interactor.swift: Logika bisnis.
  • [NamaScene]Presenter.swift: Pemformatan data dan trigger navigasi.
  • [NamaScene]Router.swift: Navigasi.
  • (Opsional) [NamaScene]Worker.swift: Operasi data.

Contoh file ListProductsModels.swift:

// MARK: Use cases

enum ListProducts {
    struct Request {
        // Parameter request jika ada, contoh: page, categoryID
    }
    struct Response {
        struct Product {
            let id: String
            let name: String
            let price: Double
            let imageUrl: String
        }
        let products: [Product]
    }
}

// MARK: View Controller Actions

enum ListProductsViewControllerAction {
    case loadProducts // Aksi yang dipicu oleh ViewController
}

// MARK: Presentation logic

struct ListProductsPresentationModel {
    struct ProductViewModel {
        let id: String
        let name: String
        let formattedPrice: String
        let imageUrl: URL?
    }
    let products: [ProductViewModel]
}

2. Alur Kerja Data: Dari Interactor ke ViewController

Mari kita lihat bagaimana data produk mengalir.

A. ListProductsInteractor.swift

protocol ListProductsBusinessLogic {
    func fetchProducts(request: ListProducts.Request)
}

protocol ListProductsDataStore {
    // Data store jika diperlukan untuk menyimpan state
}

class ListProductsInteractor: ListProductsBusinessLogic, ListProductsDataStore {
    var presenter: ListProductsPresentationLogic?
    var worker: ListProductsWorker? // Jika menggunakan worker

    // MARK: Fetch products

    func fetchProducts(request: ListProducts.Request) {
        // Inisialisasi worker jika belum
        if worker == nil {
            worker = ListProductsWorker()
        }

        worker?.fetchProducts(request: request, completion: { (result) in
            DispatchQueue.main.async { // Pastikan kembali ke main thread untuk UI update
                switch result {
                case .success(let products):
                    // Format response untuk presenter
                    let response = ListProducts.Response(products: products.map {
                        ListProducts.Response.Product(id: $0.id, name: $0.name, price: $0.price, imageUrl: $0.imageUrl)
                    })
                    self.presenter?.presentProducts(response: response)
                case .failure(let error):
                    // Handle error, kirim ke presenter untuk ditampilkan
                    self.presenter?.presentError(error: error)
                }
            }
        })
    }
}

B. ListProductsWorker.swift (Contoh Panggilan API)

import Foundation

// Dummy product structure untuk worker
struct ProductAPIModel {
    let id: String
    let name: String
    let price: Double
    let imageUrl: String
}

class ListProductsWorker {
    func fetchProducts(request: ListProducts.Request, completion: @escaping (Result<[ProductAPIModel], Error>) -> Void) {
        // Simulasi panggilan API jaringan
        DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
            let dummyProducts = [
                ProductAPIModel(id: "P001", name: "Kemeja Lengan Panjang", price: 150000.0, imageUrl: "https://example.com/images/shirt.jpg"),
                ProductAPIModel(id: "P002", name: "Celana Jeans Slim Fit", price: 250000.0, imageUrl: "https://example.com/images/jeans.jpg"),
                ProductAPIModel(id: "P003", name: "Sepatu Sneakers Trendy", price: 350000.0, imageUrl: "https://example.com/images/sneakers.jpg")
            ]
            completion(.success(dummyProducts))
            // Untuk simulasi error: completion(.failure(NSError(domain: "APIError", code: 100, userInfo: [NSLocalizedDescriptionKey: "Gagal memuat produk"])))
        }
    }
}

C. ListProductsPresenter.swift

protocol ListProductsPresentationLogic {
    func presentProducts(response: ListProducts.Response)
    func presentError(error: Error)
}

class ListProductsPresenter: ListProductsPresentationLogic {
    weak var viewController: ListProductsDisplayLogic? // Hubungan weak untuk menghindari retain cycle

    // MARK: Presenting logic

    func presentProducts(response: ListProducts.Response) {
        let viewModel = ListProductsPresentationModel(
            products: response.products.map {
                ListProductsPresentationModel.ProductViewModel(
                    id: $0.id,
                    name: $0.name,
                    formattedPrice: "Rp \($0.price.formattedWithSeparator())", // Custom formatter
                    imageUrl: URL(string: $0.imageUrl)
                )
            }
        )
        viewController?.displayProducts(viewModel: viewModel)
    }

    func presentError(error: Error) {
        // Logika menampilkan error, bisa berupa alert atau pesan di UI
        let errorMessage = (error as NSError).localizedDescription
        viewController?.displayError(message: errorMessage)
    }
}

// Helper extension untuk format harga (simulasi)
extension Double {
    func formattedWithSeparator() -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.groupingSeparator = "." // Menggunakan titik sebagai pemisah ribuan
        formatter.decimalSeparator = ","   // Menggunakan koma sebagai pemisah desimal
        return formatter.string(from: NSNumber(value: self)) ?? "\(self)"
    }
}

D. ListProductsViewController.swift

protocol ListProductsDisplayLogic: class {
    func displayProducts(viewModel: ListProductsPresentationModel)
    func displayError(message: String)
}

class ListProductsViewController: UIViewController, ListProductsDisplayLogic {
    var interactor: ListProductsBusinessLogic?
    var router: (NSObjectProtocol & ListProductsRoutingLogic & ListProductsDataPassing)?

    // MARK: Object lifecycle

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        setup() // Panggil setup untuk menginisialisasi DI
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup() // Panggil setup untuk menginisialisasi DI
    }

    // MARK: Setup

    private func setup() {
        let viewController = self
        let interactor = ListProductsInteractor()
        let presenter = ListProductsPresenter()
        let router = ListProductsRouter()
        viewController.interactor = interactor
        viewController.router = router
        interactor.presenter = presenter
        presenter.viewController = viewController
        router.viewController = viewController
        router.dataStore = interactor
    }

    // MARK: Routing

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let scene = segue.identifier {
            let selector = NSSelectorFromString("routeTo\(scene)WithSegue:")
            if let method = router?.perform(selector, with: segue) {
                do {
                    try method.invoke()
                } catch { print(error) }
            }
        }
    }

    // MARK: View lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        loadProducts()
    }

    // MARK: Do something
    
    func loadProducts() {
        let request = ListProducts.Request()
        interactor?.fetchProducts(request: request)
    }

    // MARK: Display logic

    func displayProducts(viewModel: ListProductsPresentationModel) {
        // Update UI Anda di sini menggunakan data dari viewModel
        print("Produk berhasil dimuat: \(viewModel.products.count)")
        for product in viewModel.products {
            print("- \(product.name) - \(product.formattedPrice)")
        }
        // Contoh: update tabel view atau collection view
    }

    func displayError(message: String) {
        // Tampilkan pesan error kepada pengguna
        print("Error: \(message)")
        // Contoh: show alert
    }
}

D. ListProductsRouter.swift

protocol ListProductsRoutingLogic {
    func routeToProductDetail(segue: UIStoryboardSegue?)
    // Tambahkan protokol untuk navigasi lain jika ada
}

protocol ListProductsDataPassing {
    var dataStore: ListProductsDataStore? { get }
}

class ListProductsRouter: NSObject, ListProductsRoutingLogic, ListProductsDataPassing {
    weak var viewController: ListProductsViewController?
    var dataStore: ListProductsDataStore?

    // MARK: Routing

    func routeToProductDetail(segue: UIStoryboardSegue?) {
        if let segue = segue {
            let destinationVC = segue.destination as! ProductDetailViewController // Ganti dengan nama VC Anda
            var destinationDS = destinationVC.router!.dataStore! // Dapatkan data store dari destination
            passDataToProductDetail(source: dataStore!, destination: &destinationDS)
        }
    }

    // MARK: Navigation
    
    private func passDataToProductDetail(source: ListProductsDataStore, destination: inout ProductDetailDataStore) {
        // Pindahkan data dari source ke destination jika diperlukan
        // Contoh: destination.productID = // ... ambil dari source
    }

    // MARK: Matching Segue

    func routeToProductDetail(segue: UIStoryboardSegue?) {
        if let segue = segue {
            let destinationVC = segue.destination as! ProductDetailViewController // Ganti dengan nama VC Anda
            var destinationDS = destinationVC.router!.dataStore!
            passDataToProductDetail(source: dataStore!, destination: &destinationDS)
        }
    }
}

3. Tips Praktis untuk Pemula di Startup

  • Mulai dari yang Kecil: Jangan mencoba mengaplikasikan Clean Swift ke seluruh proyek sekaligus. Pilih satu fitur penting dan terapkan di sana terlebih dahulu.
  • Manfaatkan Generators: Jika menggunakan Xcode, cari plugin atau script generator Clean Swift. Ini akan menghemat banyak waktu dan mengurangi kesalahan ketik.
  • Definisikan Model dengan Jelas: Models.swift adalah tulang punggung komunikasi antar lapisan. Pastikan Anda mendefinisikan request, response, dan view model dengan cermat.
  • Gunakan weak pada Referensi ke ViewController: Ini sangat penting di Presenter untuk menghindari retain cycles yang dapat menyebabkan kebocoran memori.
  • Jangan Berlebihan dengan Abstraksi: Clean Swift bisa terlihat rumit pada awalnya. Jangan membuat lapisan atau worker tambahan jika tidak benar-benar diperlukan. Mulai dengan struktur dasar Interactor, Presenter, dan Router.
  • Uji Setiap Lapisan: Manfaatkan testabilitas Clean Swift. Tulis unit test untuk Interactor (logika bisnis) dan Presenter (pemformatan data).
  • Dokumentasikan Alur: Saat onboarding anggota tim baru, jelaskan secara visual alur kerja data dalam satu Scene Clean Swift.

Kesimpulan

Implementasi Clean Swift dalam proyek startup dapat memberikan fondasi yang kuat untuk pertumbuhan aplikasi. Dengan memisahkan tanggung jawab, meningkatkan testabilitas, dan mendorong struktur kode yang terorganisir, Clean Swift memungkinkan tim startup untuk mengembangkan fitur dengan cepat sambil mempertahankan kualitas dan skalabilitas jangka panjang. Meskipun ada kurva belajar, manfaat yang ditawarkan dalam hal pemeliharaan dan kolaborasi menjadikannya pilihan arsitektur yang sangat berharga. Mulailah dengan memahami konsep inti, memanfaatkan alat bantu, dan secara bertahap terapkan dalam proyek Anda untuk merasakan kekuatan penuh dari Clean Swift.

0.0

Berikan Rating

Komentar (0)

Silakan login untuk memberikan komentar.

Login Sekarang

Belum ada komentar. Jadilah yang pertama!

Pembaca (0)

Belum ada user yang membaca artikel ini.