# SPM bilan CryptoTracker — [Demo: Medium]

> **Qiyinlik darajasi:** O'rta (Medium)  
> **Texnologiyalar:** Multi-Target SPM Package, Interface vs Implementation ajratilishi, Swift 6 `package` Access Modifier, Tashqi SPM qaramliklar, Kesh qatlami, Mock Testing Target.

---

## 1. Loyihaning Arxitektura Rejasi

O'rta darajadagi ushbu loyihada biz kriptovalyutalar kursini real vaqt rejimida kuzatuvchi **CryptoTracker** ilovasi uchun ko'p targetli (Multi-Target) SPM modulini quramiz.

Katta loyihalardagi eng asosiy muammo — tarmoq so'rovlari yoki ma'lumotlar bazasi o'zgarganda butun UI qayta kompilyatsiya bo'lishidir. Buni oldini olish uchun biz paketni 4 ta alohida targetga bo'lamiz:

```
CryptoTrackerPackage/
├── Package.swift
├── Sources/
│   ├── CryptoTrackerInterface/    # 1. Sof Shartnoma (DTO modellar & Protocols)
│   │   ├── CoinDTO.swift
│   │   └── CryptoRepositoryProtocol.swift
│   │
│   ├── CryptoTrackerCore/         # 2. Tarmoq va Kesh Logikasi (Implementation)
│   │   ├── API/
│   │   │   └── CryptoAPIService.swift
│   │   └── Cache/
│   │       └── CryptoLocalCache.swift
│   │
│   ├── CryptoTrackerFeature/      # 3. SwiftUI Ekranlari & ViewModel
│   │   ├── Views/
│   │   │   ├── CoinListView.swift
│   │   │   └── CoinRowView.swift
│   │   └── ViewModels/
│   │       └── CryptoListViewModel.swift
│   │
│   └── CryptoTrackerTesting/      # 4. Mock va Stublar (Faqat testlar uchun)
│       └── MockCryptoRepository.swift
│
└── Tests/
    └── CryptoTrackerFeatureTests/
        └── CryptoListViewModelTests.swift
```

### Targetlar o'rtasidagi munosabat (DAG - Directed Acyclic Graph):

<div style="margin: 1.5rem 0; overflow-x: auto;">
  <svg viewBox="0 0 840 450" width="100%" style="background: rgba(10, 14, 23, 0.85); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 14px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
    <defs>
      <linearGradient id="dagFeatureGrad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="#0284c7" stop-opacity="0.3"/>
        <stop offset="100%" stop-color="#0369a1" stop-opacity="0.55"/>
      </linearGradient>
      <linearGradient id="dagInterfaceGrad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="#059669" stop-opacity="0.3"/>
        <stop offset="100%" stop-color="#047857" stop-opacity="0.55"/>
      </linearGradient>
      <linearGradient id="dagCoreGrad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="#d97706" stop-opacity="0.3"/>
        <stop offset="100%" stop-color="#b45309" stop-opacity="0.55"/>
      </linearGradient>
      <linearGradient id="dagTestGrad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="#7c3aed" stop-opacity="0.3"/>
        <stop offset="100%" stop-color="#6d28d9" stop-opacity="0.55"/>
      </linearGradient>
      <marker id="dagArrowBlue" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
        <path d="M 0 1 L 8 5 L 0 9 z" fill="#38bdf8"/>
      </marker>
      <marker id="dagArrowGreen" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
        <path d="M 0 1 L 8 5 L 0 9 z" fill="#34d399"/>
      </marker>
      <marker id="dagArrowAmber" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
        <path d="M 0 1 L 8 5 L 0 9 z" fill="#fbbf24"/>
      </marker>
      <marker id="dagArrowPurple" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
        <path d="M 0 1 L 8 5 L 0 9 z" fill="#a78bfa"/>
      </marker>
    </defs>
    <!-- Background Grid Dots -->
    <pattern id="dagGridDots" width="20" height="20" patternUnits="userSpaceOnUse">
      <circle cx="2" cy="2" r="1" fill="#ffffff" fill-opacity="0.04"/>
    </pattern>
    <rect width="840" height="450" fill="url(#dagGridDots)"/>
    <!-- 1. TOP: CryptoTrackerFeature -->
    <g transform="translate(240, 25)">
      <rect width="360" height="85" rx="10" fill="url(#dagFeatureGrad)" stroke="#38bdf8" stroke-width="2"/>
      <rect x="14" y="10" width="105" height="18" rx="4" fill="#0284c7"/>
      <text x="66" y="23" text-anchor="middle" fill="#ffffff" font-size="9" font-weight="700" letter-spacing="0.5">FEATURE TARGET</text>
      <text x="14" y="48" fill="#ffffff" font-size="16" font-weight="700">CryptoTrackerFeature</text>
      <text x="14" y="66" fill="#bae6fd" font-size="11">SwiftUI Views • ViewModels • UI State</text>
      <circle cx="335" cy="42" r="8" fill="#0284c7" fill-opacity="0.4"/>
      <text x="335" y="46" text-anchor="middle" fill="#38bdf8" font-size="11">UI</text>
    </g>
    <!-- Arrow from Feature to Interface -->
    <path d="M 360 110 L 360 170" stroke="#34d399" stroke-width="2" fill="none" marker-end="url(#dagArrowGreen)"/>
    <rect x="250" y="125" width="155" height="22" rx="4" fill="#064e3b" stroke="#059669" stroke-width="1"/>
    <text x="327" y="140" text-anchor="middle" fill="#a7f3d0" font-size="9.5" font-weight="600">Faqat interfeysga bog'liq</text>
    <!-- Arrow from Feature to Core (DI runtime container) -->
    <path d="M 480 110 L 480 145 L 670 145 L 670 170" stroke="#fbbf24" stroke-width="2" stroke-dasharray="4 4" fill="none" marker-end="url(#dagArrowAmber)"/>
    <rect x="520" y="134" width="115" height="22" rx="4" fill="#451a03" stroke="#d97706" stroke-width="1"/>
    <text x="577" y="149" text-anchor="middle" fill="#fde68a" font-size="9.5" font-weight="600">DI orqali ulanadi</text>
    <!-- 2. MIDDLE ROW: Testing | Interface | Core -->
    <!-- Testing Target (Left) -->
    <g transform="translate(30, 180)">
      <rect width="210" height="100" rx="10" fill="url(#dagTestGrad)" stroke="#a78bfa" stroke-width="1.8"/>
      <rect x="12" y="10" width="100" height="18" rx="4" fill="#7c3aed"/>
      <text x="62" y="23" text-anchor="middle" fill="#ffffff" font-size="8.5" font-weight="700">TESTING TARGET</text>
      <text x="12" y="48" fill="#ffffff" font-size="13.5" font-weight="700">CryptoTrackerTesting</text>
      <text x="12" y="66" fill="#ddd6fe" font-size="10.5">MockCryptoRepository</text>
      <text x="12" y="82" fill="#c4b5fd" font-size="10">Kompilyatsiya: ~0.05s</text>
    </g>
    <!-- Arrow from Testing to Interface -->
    <path d="M 240 230 L 290 230" stroke="#a78bfa" stroke-width="2" fill="none" marker-end="url(#dagArrowPurple)"/>
    <!-- Central: Interface Target (Zero Dependencies) -->
    <g transform="translate(300, 175)">
      <rect width="240" height="110" rx="10" fill="url(#dagInterfaceGrad)" stroke="#34d399" stroke-width="2"/>
      <rect x="14" y="10" width="145" height="18" rx="4" fill="#059669"/>
      <text x="86" y="23" text-anchor="middle" fill="#ffffff" font-size="8.5" font-weight="700">INTERFACE (ZERO DEPS)</text>
      <text x="14" y="48" fill="#ffffff" font-size="14" font-weight="700">CryptoTrackerInterface</text>
      <text x="14" y="67" fill="#a7f3d0" font-size="10.5">CryptoRepositoryProtocol</text>
      <text x="14" y="82" fill="#a7f3d0" font-size="10.5">CryptoCoin • CryptoPrice</text>
      <text x="14" y="98" fill="#6ee7b7" font-size="9.5" font-weight="600">⚡️ O'zgarmas shartnoma</text>
    </g>
    <!-- Arrow from Core to Interface -->
    <path d="M 600 230 L 550 230" stroke="#34d399" stroke-width="2" fill="none" marker-end="url(#dagArrowGreen)"/>
    <!-- Core Target (Right) -->
    <g transform="translate(600, 180)">
      <rect width="210" height="100" rx="10" fill="url(#dagCoreGrad)" stroke="#fbbf24" stroke-width="1.8"/>
      <rect x="12" y="10" width="90" height="18" rx="4" fill="#d97706"/>
      <text x="57" y="23" text-anchor="middle" fill="#ffffff" font-size="8.5" font-weight="700">CORE TARGET</text>
      <text x="12" y="48" fill="#ffffff" font-size="14" font-weight="700">CryptoTrackerCore</text>
      <text x="12" y="66" fill="#fde68a" font-size="10.5">URLSession • Network</text>
      <text x="12" y="82" fill="#fde68a" font-size="10.5">LiveWebSocket • Storage</text>
    </g>
    <!-- 3. BOTTOM: Explanation & Benefit Bar -->
    <g transform="translate(30, 320)">
      <rect width="780" height="105" rx="10" fill="rgba(15, 23, 42, 0.9)" stroke="rgba(255, 255, 255, 0.12)" stroke-width="1"/>
      <text x="20" y="28" fill="#38bdf8" font-size="12" font-weight="700">DAG Arxitekturasining 3 ta Asosiy Foydasi:</text>
      <circle cx="28" cy="50" r="4" fill="#34d399"/>
      <text x="42" y="54" fill="#e2e8f0" font-size="11.5"><tspan font-weight="700" fill="#34d399">Nol Kaskad Qayta Build:</tspan> Core modulidagi URL yoki API o'zgarishi Feature yoki Test modulini qayta kompilyatsiya qildirMAYDI.</text>
      <circle cx="28" cy="72" r="4" fill="#a78bfa"/>
      <text x="42" y="76" fill="#e2e8f0" font-size="11.5"><tspan font-weight="700" fill="#a78bfa">Tezkor Unit Testlar:</tspan> Testlar CryptoTrackerCore tarmog'iga ulanmaydi, MockCryptoRepository orqali &lt; 0.05 soniyada tugaydi.</text>
      <circle cx="28" cy="94" r="4" fill="#38bdf8"/>
      <text x="42" y="98" fill="#e2e8f0" font-size="11.5"><tspan font-weight="700" fill="#38bdf8">Parallel Jamoaviy Ish:</tspan> UI jamoasi (Feature) va Backend/SDK jamoasi (Core) bitta Interface shartnomasi asosida bir vaqtda mustaqil ishlay oladi.</text>
    </g>
  </svg>
</div>

---

## 2. Ko'p Targetli `Package.swift` Manifesti

Ushbu manifestda bitta SPM to'plami ichida qanday qilib 4 ta mustaqil target yaratish va ularning o'zaro bog'liqliklarini chegaralash ko'rsatilgan:

```swift
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "CryptoTrackerPackage",
    platforms: [
        .iOS(.v17),
        .macOS(.v14)
    ],
    products: [
        // Asosiy ilova UI uchun faqat Feature kutubxonasini import qiladi
        .library(name: "CryptoTrackerFeature", targets: ["CryptoTrackerFeature"]),
        
        // Bog'lanishlar uchun Interface
        .library(name: "CryptoTrackerInterface", targets: ["CryptoTrackerInterface"]),
        
        // Haqiqiy implementatsiya (DI konteyneri uchun)
        .library(name: "CryptoTrackerCore", targets: ["CryptoTrackerCore"]),
        
        // Testlar uchun mocklar kutubxonasi
        .library(name: "CryptoTrackerTesting", targets: ["CryptoTrackerTesting"])
    ],
    dependencies: [
        // Tashqi rasmiy Apple algoritmlari (masalan, swift-collections)
        .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.1.0"))
    ],
    targets: [
        // ── TARGET 1: INTERFACE (Hech narsaga bog'liq emas!) ──
        .target(
            name: "CryptoTrackerInterface",
            dependencies: [],
            swiftSettings: [
                .enableUpcomingFeature("StrictConcurrency"),
                .swiftLanguageMode(.v6)
            ]
        ),

        // ── TARGET 2: CORE (API va Kesh) ──
        .target(
            name: "CryptoTrackerCore",
            dependencies: [
                "CryptoTrackerInterface",
                .product(name: "OrderedCollections", package: "swift-collections")
            ],
            swiftSettings: [
                .enableUpcomingFeature("StrictConcurrency"),
                .swiftLanguageMode(.v6)
            ]
        ),

        // ── TARGET 3: FEATURE (UI & ViewModel) ──
        .target(
            name: "CryptoTrackerFeature",
            dependencies: [
                "CryptoTrackerInterface" // DIQQAT: Core'ga emas, Interface'ga bog'langan!
            ],
            swiftSettings: [
                .enableUpcomingFeature("StrictConcurrency"),
                .swiftLanguageMode(.v6)
            ]
        ),

        // ── TARGET 4: TESTING HELPERS (Mocklar) ──
        .target(
            name: "CryptoTrackerTesting",
            dependencies: [
                "CryptoTrackerInterface"
            ],
            swiftSettings: [
                .enableUpcomingFeature("StrictConcurrency"),
                .swiftLanguageMode(.v6)
            ]
        ),

        // ── TARGET 5: UNIT TESTS ──
        .testTarget(
            name: "CryptoTrackerFeatureTests",
            dependencies: [
                "CryptoTrackerFeature",
                "CryptoTrackerTesting"
            ]
        )
    ]
)
```

---

## 3. Kodlarni Yozish: Qadam-baqadam

### 3.1. Target 1: `CryptoTrackerInterface`

Ushbu modulda faqat biznes shartnomalari (Contracts) joylashadi:

```swift
// Sources/CryptoTrackerInterface/CoinDTO.swift
import Foundation

public struct CoinDTO: Identifiable, Sendable, Codable, Equatable {
    public let id: String
    public let symbol: String
    public let name: String
    public let priceUSD: Decimal
    public let change24h: Double
    
    public init(id: String, symbol: String, name: String, priceUSD: Decimal, change24h: Double) {
        self.id = id
        self.symbol = symbol
        self.name = name
        self.priceUSD = priceUSD
        self.change24h = change24h
    }
}
```

```swift
// Sources/CryptoTrackerInterface/CryptoRepositoryProtocol.swift
import Foundation

public protocol CryptoRepositoryProtocol: Sendable {
    /// Barcha tangalar ro'yxatini olish (tarmoqdan yoki keshdan)
    func fetchTopCoins(forceRefresh: Bool) async throws -> [CoinDTO]
    
    /// Bitta tanga haqida batafsil ma'lumot
    func fetchCoinDetails(id: String) async throws -> CoinDTO
}
```

---

### 3.2. Target 2: `CryptoTrackerCore`

Ushbu modulda haqiqiy tarmoq so'rovlari va lokal kesh mexanizmi yoziladi:

```swift
// Sources/CryptoTrackerCore/Cache/CryptoLocalCache.swift
import Foundation
import CryptoTrackerInterface

// Swift 6 'package' modifikatori: faqat shu paket ichidagi boshqa targetlar ko'radi!
package actor CryptoLocalCache {
    private var inMemoryCache: [String: CoinDTO] = [:]
    
    package init() {}
    
    package func save(coins: [CoinDTO]) {
        for coin in coins {
            inMemoryCache[coin.id] = coin
        }
    }
    
    package func getAll() -> [CoinDTO] {
        Array(inMemoryCache.values)
    }
}
```

```swift
// Sources/CryptoTrackerCore/API/RealCryptoRepository.swift
import Foundation
import CryptoTrackerInterface

public final class RealCryptoRepository: CryptoRepositoryProtocol {
    private let cache = CryptoLocalCache()
    private let session: URLSession
    
    public init(session: URLSession = .shared) {
        self.session = session
    }
    
    public func fetchTopCoins(forceRefresh: Bool) async throws -> [CoinDTO] {
        if !forceRefresh {
            let cached = await cache.getAll()
            if !cached.isEmpty {
                return cached
            }
        }
        
        // Haqiqiy CoinGecko yoki Binance API URL
        guard let url = URL(string: "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd") else {
            throw URLError(.badURL)
        }
        
        let (data, response) = try await session.data(from: url)
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        
        // JSON parsing... (Demo sifatida statik DTO qaytarish namunasi)
        let coins = [
            CoinDTO(id: "bitcoin", symbol: "BTC", name: "Bitcoin", priceUSD: 68450.00, change24h: 3.45),
            CoinDTO(id: "ethereum", symbol: "ETH", name: "Ethereum", priceUSD: 3620.50, change24h: -1.20),
            CoinDTO(id: "solana", symbol: "SOL", name: "Solana", priceUSD: 182.30, change24h: 8.75)
        ]
        
        await cache.save(coins: coins)
        return coins
    }
    
    public func fetchCoinDetails(id: String) async throws -> CoinDTO {
        let all = try await fetchTopCoins(forceRefresh: false)
        guard let coin = all.first(where: { $0.id == id }) else {
            throw URLError(.resourceUnavailable)
        }
        return coin
    }
}
```

---

### 3.3. Target 3: `CryptoTrackerFeature` (SwiftUI & ViewModel)

Bu yerda eng muhim qoida: `CryptoTrackerFeature` o'z ichida `CryptoTrackerCore` ni **import qilmaydi**! U faqat `CryptoTrackerInterface` bilan muloqot qiladi:

```swift
// Sources/CryptoTrackerFeature/ViewModels/CryptoListViewModel.swift
import SwiftUI
import Observation
import CryptoTrackerInterface // Faqat interfeys!

@Observable
@MainActor
public final class CryptoListViewModel {
    public private(set) var coins: [CoinDTO] = []
    public private(set) var isLoading: Bool = false
    public var errorMessage: String? = nil
    
    // Protokol orqali ineksiya qilinadi
    private let repository: any CryptoRepositoryProtocol
    
    public init(repository: any CryptoRepositoryProtocol) {
        self.repository = repository
    }
    
    public func loadCoins(forceRefresh: Bool = false) async {
        isLoading = true
        errorMessage = nil
        do {
            self.coins = try await repository.fetchTopCoins(forceRefresh: forceRefresh)
        } catch {
            self.errorMessage = "Ma'lumotlarni yuklab bo'lmadi: \(error.localizedDescription)"
        }
        isLoading = false
    }
}
```

```swift
// Sources/CryptoTrackerFeature/Views/CoinListView.swift
import SwiftUI
import CryptoTrackerInterface

public struct CoinListView: View {
    @State private var viewModel: CryptoListViewModel
    
    public init(repository: any CryptoRepositoryProtocol) {
        _viewModel = State(initialValue: CryptoListViewModel(repository: repository))
    }
    
    public var body: some View {
        NavigationStack {
            List(viewModel.coins) { coin in
                HStack {
                    VStack(alignment: .leading, spacing: 4) {
                        Text(coin.name).font(.headline)
                        Text(coin.symbol).font(.caption).foregroundColor(.secondary)
                    }
                    Spacer()
                    VStack(alignment: .trailing, spacing: 4) {
                        Text("$\(coin.priceUSD)").font(.headline)
                        Text(String(format: "%+.2f%%", coin.change24h))
                            .font(.caption)
                            .foregroundColor(coin.change24h >= 0 ? .green : .red)
                    }
                }
            }
            .navigationTitle("CryptoTracker (SPM)")
            .refreshable {
                await viewModel.loadCoins(forceRefresh: true)
            }
            .task {
                await viewModel.loadCoins()
            }
        }
    }
}
```

---

### 3.4. Target 4: `CryptoTrackerTesting` (Mocklar)

```swift
// Sources/CryptoTrackerTesting/MockCryptoRepository.swift
import Foundation
import CryptoTrackerInterface

public final class MockCryptoRepository: CryptoRepositoryProtocol, @unchecked Sendable {
    public var stubbedCoins: [CoinDTO] = []
    public var shouldThrowError: Bool = false
    
    public init(stubbedCoins: [CoinDTO] = []) {
        self.stubbedCoins = stubbedCoins
    }
    
    public func fetchTopCoins(forceRefresh: Bool) async throws -> [CoinDTO] {
        if shouldThrowError {
            throw URLError(.notConnectedToInternet)
        }
        return stubbedCoins
    }
    
    public func fetchCoinDetails(id: String) async throws -> CoinDTO {
        if let coin = stubbedCoins.first(where: { $0.id == id }) {
            return coin
        }
        throw URLError(.resourceUnavailable)
    }
}
```

---

## 4. Izolyatsiyalangan Unit Testlar

`Tests/CryptoTrackerFeatureTests/CryptoListViewModelTests.swift` faylida test yozamiz:

```swift
import XCTest
@testable import CryptoTrackerFeature
import CryptoTrackerTesting
import CryptoTrackerInterface

@MainActor
final class CryptoListViewModelTests: XCTestCase {
    
    func testLoadCoins_Success_PopulatesCoins() async {
        // Given: Mock ma'lumotlar bilan tayyorlash
        let mockRepo = MockCryptoRepository(stubbedCoins: [
            CoinDTO(id: "ton", symbol: "TON", name: "Toncoin", priceUSD: 5.40, change24h: 12.4)
        ])
        let vm = CryptoListViewModel(repository: mockRepo)
        
        // When: Yuklash
        await vm.loadCoins()
        
        // Then: Natijani tekshirish
        XCTAssertEqual(vm.coins.count, 1)
        XCTAssertEqual(vm.coins.first?.symbol, "TON")
        XCTAssertNil(vm.errorMessage)
        XCTAssertFalse(vm.isLoading)
    }
    
    func testLoadCoins_Failure_SetsErrorMessage() async {
        // Given: Xatolik qaytaruvchi mock
        let mockRepo = MockCryptoRepository()
        mockRepo.shouldThrowError = true
        let vm = CryptoListViewModel(repository: mockRepo)
        
        // When
        await vm.loadCoins()
        
        // Then
        XCTAssertTrue(vm.coins.isEmpty)
        XCTAssertNotNil(vm.errorMessage)
    }
}
```

### Testni terminalda ishga tushirish:
```bash
swift test --filter CryptoTrackerFeatureTests
```
Natija:
```
Executed 2 tests, with 0 failures in 0.051 seconds.
```

---

## 5. Asosiy App Targetida Barchasini Birlashtirish (Dependency Injection)

Asosiy iOS ilovangiz (`CryptoApp.swift`):

```swift
import SwiftUI
import CryptoTrackerFeature
import CryptoTrackerCore

@main
struct CryptoApp: App {
    // Haqiqiy repository bitta joyda initsializatsiya qilinadi
    private let repository = RealCryptoRepository()
    
    var body: some Scene {
        WindowGroup {
            CoinListView(repository: repository)
        }
    }
}
```

---

## 6. Medium Demo Xulosasi

| Muammo | Monolitda | Bizning Ko'p Targetli SPM Arxitekturamizda |
| :--- | :--- | :--- |
| **Tarmoq kodi o'zgarsa** | Butun UI va ViewModel recompile bo'ladi | Faqat `CryptoTrackerCore` qayta yig'iladi (UI keshdan olinadi!) |
| **Testlar tezligi** | Simulyator kutish bilan 25–40 sekund | `MockCryptoRepository` bilan **0.05 sekund** |
| **Kodni yashirish** | Hamma narsa bir-birini ko'radi | `CryptoLocalCache` faqat paket ichiga yashirilgan (`package` keyword) |
