Section 21/221 menit

15. Real Use Cases

15. Real Use Cases

Use Case 1: Menyelidiki dan Memperbaiki Slow File Compilation

Tim menemukan bahwa file CheckoutViewModel.swift membutuhkan 8 detik untuk dikompilasi — jauh di atas normal.

swift
// CheckoutViewModel.swift — SEBELUM optimasi
// File ini membutuhkan 8.2 detik untuk dikompilasi

import Foundation
import Combine

class CheckoutViewModel: ObservableObject {
    @Published var items: [CartItem] = []
    @Published var total: Decimal = 0
    @Published var discount: Decimal = 0
    @Published var tax: Decimal = 0
    @Published var finalTotal: Decimal = 0

    // ❌ Expression sangat kompleks — inference hell
    var summary: String {
        "Total: \(NumberFormatter.localizedString(from: items.map { $0.price * Decimal($0.quantity) }.reduce(0, +) * (1 - discount) * (1 + tax) as NSDecimalNumber, number: .currency)) (\(items.count) items, \(String(format: "%.0f%%", Double(discount) * 100)) off)"
    }

    // ❌ Generic function overused
    func calculate<T: Numeric & Comparable>(_ values: [T], using operation: (T, T) -> T) -> T {
        values.reduce(values[0], operation)
    }

    // ❌ Nested ternary dalam computed property
    var checkoutState: String {
        items.isEmpty ? "empty" : total < 0 ? "invalid" : discount > 1 ? "over-discounted" : tax < 0 ? "invalid-tax" : "ready"
    }
}
swift
// CheckoutViewModel.swift — SETELAH optimasi
// File ini sekarang membutuhkan 0.4 detik untuk dikompilasi

import Foundation
import Combine

final class CheckoutViewModel: ObservableObject {
    @Published var items: [CartItem] = []
    @Published var total: Decimal = 0
    @Published var discount: Decimal = 0
    @Published var tax: Decimal = 0
    @Published var finalTotal: Decimal = 0

    // ✓ Tipe eksplisit, expression dipecah
    var summary: String {
        let subtotal: Decimal = items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
        let discounted: Decimal = subtotal * (1 - discount)
        let withTax: Decimal = discounted * (1 + tax)
        let formatted: String = NumberFormatter.localizedString(from: withTax as NSDecimalNumber, number: .currency)
        let discountPct: Int = Int(Double(truncating: discount as NSDecimalNumber) * 100)
        return "\(formatted) (\(items.count) items, \(discountPct)% off)"
    }

    // ✓ Fungsi konkret, bukan generic berlebihan
    private func sumPrices(_ items: [CartItem]) -> Decimal {
        items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
    }

    // ✓ Guard + early return menggantikan nested ternary
    var checkoutState: CheckoutState {
        guard !items.isEmpty else { return .empty }
        guard total >= 0 else { return .invalid("Total negatif") }
        guard discount <= 1 else { return .invalid("Diskon melebihi 100%") }
        guard tax >= 0 else { return .invalid("Pajak negatif") }
        return .ready
    }
}

enum CheckoutState {
    case empty
    case invalid(String)
    case ready
}

Langkah investigasi yang digunakan:

swift
# 1. Aktifkan warning untuk slow compilation
# Di Build Settings → OTHER_SWIFT_FLAGS (Debug):
# -Xfrontend -warn-long-function-bodies=200
# -Xfrontend -warn-long-expression-type-checking=200

# 2. Build dan cari warning
xcodebuild build -scheme MyApp 2>&1 | grep "expression took"
# Output: CheckoutViewModel.swift:18: warning: expression took 3847ms to type-check

# 3. Identifikasi lokasi exact → perbaiki

Use Case 2: Setup CI Pipeline dengan Full Caching

Tim pindah dari CircleCI (no caching) ke GitHub Actions dengan caching lengkap. Build time CI turun dari 22 menit ke 8 menit.

swift
# .github/workflows/pr-check.yml
# PR Check: fast feedback, hanya unit test

name: PR Check

on:
  pull_request:
    branches: [main, develop]

concurrency:
  group: pr-${{ github.event.pull_request.number }}
  cancel-in-progress: true  # Batalkan run lama jika PR diupdate

jobs:
  build-and-test:
    runs-on: macos-15
    timeout-minutes: 25

    steps:
      - uses: actions/checkout@v4

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true  # Otomatis cache gems via bundler

      - name: Cache CocoaPods
        uses: actions/cache@v4
        id: pods-cache
        with:
          path: Pods
          key: pods-v2-${{ runner.os }}-${{ hashFiles('Podfile.lock') }}
          restore-keys: pods-v2-${{ runner.os }}-

      - name: Cache SPM
        uses: actions/cache@v4
        with:
          path: ~/Library/Developer/Xcode/DerivedData/**/SourcePackages
          key: spm-v2-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}
          restore-keys: spm-v2-${{ runner.os }}-

      - name: Install Pods
        if: steps.pods-cache.outputs.cache-hit != 'true'
        run: bundle exec pod install

      - name: Build (no tests)
        run: |
          xcodebuild build \
            -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 16' \
            -configuration Debug \
            CODE_SIGNING_ALLOWED=NO \
            | xcbeautify --is-ci

      - name: Unit Tests
        run: |
          xcodebuild test \
            -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 16' \
            -configuration Debug \
            -only-testing:MyAppTests \
            -resultBundlePath TestResults.xcresult \
            CODE_SIGNING_ALLOWED=NO \
            | xcbeautify --is-ci

      - name: Upload test results
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: test-results-${{ github.run_id }}
          path: TestResults.xcresult
          retention-days: 7

Use Case 3: Migrasi ke Local SPM Package untuk Mempercepat Incremental Build

Team yang punya monolithic target dengan 400 file memecahnya menjadi local packages. Incremental build turun dari 45 detik ke 8 detik untuk perubahan di satu modul.

swift
// Package.swift di root project
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyAppModules",
    platforms: [.iOS(.v16)],
    products: [
        .library(name: "CoreDomain", targets: ["CoreDomain"]),
        .library(name: "NetworkKit", targets: ["NetworkKit"]),
        .library(name: "UIComponents", targets: ["UIComponents"]),
        .library(name: "HomeFeature", targets: ["HomeFeature"]),
    ],
    dependencies: [
        // External dependencies via SPM
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.8.0"),
    ],
    targets: [
        // Layer 1: Domain (tidak ada dependency internal)
        .target(
            name: "CoreDomain",
            dependencies: [],
            path: "Sources/CoreDomain"
        ),

        // Layer 2: Network (depend on CoreDomain)
        .target(
            name: "NetworkKit",
            dependencies: [
                "CoreDomain",
                .product(name: "Alamofire", package: "Alamofire"),
            ],
            path: "Sources/NetworkKit"
        ),

        // Layer 2: UI (depend on CoreDomain)
        .target(
            name: "UIComponents",
            dependencies: ["CoreDomain"],
            path: "Sources/UIComponents",
            resources: [
                .process("Resources"),
            ]
        ),

        // Layer 3: Feature (depend on semua layer 2)
        .target(
            name: "HomeFeature",
            dependencies: ["NetworkKit", "UIComponents", "CoreDomain"],
            path: "Sources/HomeFeature"
        ),

        // Tests
        .testTarget(
            name: "CoreDomainTests",
            dependencies: ["CoreDomain"],
            path: "Tests/CoreDomainTests"
        ),
        .testTarget(
            name: "NetworkKitTests",
            dependencies: ["NetworkKit"],
            path: "Tests/NetworkKitTests"
        ),
    ]
)
swift
// Sources/CoreDomain/User.swift
// Domain model — tidak bergantung pada framework apapun

public struct User: Identifiable, Sendable {
    public let id: UUID
    public var name: String
    public var email: String
    public var tier: Tier

    public enum Tier: String, Sendable {
        case free, premium, enterprise
    }

    public init(id: UUID = .init(), name: String, email: String, tier: Tier = .free) {
        self.id = id
        self.name = name
        self.email = email
        self.tier = tier
    }
}
swift
// Sources/NetworkKit/UserRepository.swift
// Network layer — menggunakan CoreDomain types

import Foundation
import CoreDomain
import Alamofire

public protocol UserRepositoryProtocol: Sendable {
    func fetchUser(id: UUID) async throws -> User
    func updateUser(_ user: User) async throws -> User
}

public final class UserRepository: UserRepositoryProtocol {
    private let session: Session

    public init(session: Session = .default) {
        self.session = session
    }

    public func fetchUser(id: UUID) async throws -> User {
        let response: UserResponse = try await session
            .request("https://api.example.com/users/\(id)")
            .validate()
            .serializingDecodable(UserResponse.self)
            .value
        return response.toDomain()
    }

    public func updateUser(_ user: User) async throws -> User {
        let body = UpdateUserRequest(user: user)
        let response: UserResponse = try await session
            .request(
                "https://api.example.com/users/\(user.id)",
                method: .put,
                parameters: body,
                encoder: JSONParameterEncoder.default
            )
            .validate()
            .serializingDecodable(UserResponse.self)
            .value
        return response.toDomain()
    }
}

Cara menambahkan local package ke Xcode project:

swift
1. File → Add Package Dependencies
2. Pilih "Add Local..."
3. Navigasi ke folder yang berisi Package.swift
4. Xcode otomatis menambahkan sebagai local dependency
5. Di target → General → Frameworks, Libraries → tambahkan modul yang diperlukan