# SPM bilan ToDo App — [Demo: Easy]

> **Qiyinlik darajasi:** Oson (Easy)  
> **Texnologiyalar:** Swift 6.0, Xcode 16/27, Local Swift Package, Observation Framework (`@Observable`), `Bundle.module` resurslari, Izolyatsiyalangan Unit Testlar.

---

## 1. Loyihaning Maqsadi va Arxitekturasi

Ushbu amaliy loyihada biz Xcode ilovasi ichida to'liq mustaqil **Local Swift Package** yaratamiz. Maqsad — ilovaning biznes logikasi (Model, ViewModel, Storage) va maxsus resurslarini (ikonkalar, ranglar) asosiy ilovadan ajratib, alohida SPM moduliga joylash.

```
TodoListSPM/ (Asosiy Loyiha)
├── TodoListSPM.xcodeproj
├── TodoListSPM/ (App Target)
│   ├── TodoListSPMApp.swift
│   └── ContentView.swift (Faqat UI render)
└── Packages/
    └── TodoLogicPackage/ (Lokal SPM Paketi)
        ├── Package.swift
        ├── Sources/
        │   └── TodoLogicPackage/
        │       ├── Models/
        │       │   └── TodoItem.swift
        │       ├── Storage/
        │       │   └── TodoStorage.swift
        │       ├── Resources/
        │       │   └── Assets.xcassets (Status ikonkalari va ranglar)
        │       └── ViewModels/
        │           └── TodoViewModel.swift
        └── Tests/
            └── TodoLogicPackageTests/
                └── TodoViewModelTests.swift
```

### Nega hatto kichik ToDo ilovada ham SPM kerak?
1. **Tezkor Unit Testlar:** `TodoViewModelTests` ni ishga tushirish uchun og'ir iOS Simulyatorini (Simulator boot) kutish shart emas! SPM unit testlari to'g'ridan-to'g'ri macOS platformasida soniyaning o'ndan birida (0.1 soniya) ishlab tugaydi.
2. **Inkapsulyatsiya:** Ma'lumotlarni saqlash mexanizmi (`TodoStorage`) faqat paket ichida ko'rinadi, tashqi UI uni to'g'ridan-to'g'ri buzib qo'ya olmaydi.
3. **Qayta ishlatuvchanlik:** Xuddi shu `TodoLogicPackage` paketini ertaga macOS yoki watchOS ilovasida 100% o'zgarishsiz ishlatish mumkin.

---

## 2. 1-Qadam: Xcode'da Loyiha va Local Package Yaratish

### 2.1. Asosiy App yaratish
1. Xcode ni oching -> **File -> New -> Project -> iOS App**.
2. **Product Name:** `TodoListSPM`.
3. **Interface:** SwiftUI, **Language:** Swift.

### 2.2. Local Swift Package yaratish
1. Xcode menyusidan: **File -> New -> Package...**
2. Nomiga: `TodoLogicPackage` deb yozing.
3. Saqlash joyi (Location): `TodoListSPM` papkasi ichini tanlang.
4. **Muhim:** Pastdagi **"Add to:"** va **"Group:"** parametrlarida `TodoListSPM` tanlangan bo'lishi kerak.
5. Create tugmasini bosing. Xcode Project Navigator'da paket ramzi bilan `TodoLogicPackage` paydo bo'ladi.

---

## 3. 2-Qadam: `Package.swift` Manifestini Sozlash

Paket ildizidagi `Package.swift` faylini ochib, zamonaviy Swift 6 standartlariga moslaymiz:

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

let package = Package(
    name: "TodoLogicPackage",
    defaultLocalization: "en",
    platforms: [
        .iOS(.v17),
        .macOS(.v14)
    ],
    products: [
        // Asosiy ilovaga eksport qilinadigan kutubxona
        .library(
            name: "TodoLogicPackage",
            targets: ["TodoLogicPackage"]
        )
    ],
    targets: [
        .target(
            name: "TodoLogicPackage",
            dependencies: [],
            resources: [
                // Ikonka va ranglarni o'z ichiga olgan Asset Catalog
                .process("Resources")
            ],
            swiftSettings: [
                .enableUpcomingFeature("StrictConcurrency"),
                .swiftLanguageMode(.v6)
            ]
        ),
        .testTarget(
            name: "TodoLogicPackageTests",
            dependencies: ["TodoLogicPackage"]
        )
    ]
)
```

---

## 4. 3-Qadam: Biznes Logika va Modellarni Yozish

Paket ichida `Sources/TodoLogicPackage/` ostida quyidagi fayllarni yaratamiz:

### 4.1. Model (`Models/TodoItem.swift`)
```swift
import Foundation

public struct TodoItem: Identifiable, Codable, Sendable, Equatable {
    public let id: UUID
    public var title: String
    public var isCompleted: Bool
    public let createdAt: Date
    
    public init(
        id: UUID = UUID(),
        title: String,
        isCompleted: Bool = false,
        createdAt: Date = Date()
    ) {
        self.id = id
        self.title = title
        self.isCompleted = isCompleted
        self.createdAt = createdAt
    }
}
```

### 4.2. Local Storage (`Storage/TodoStorage.swift`)
Biz saqlash logikasini paket tashqarisidan yashiramiz (`package` yoki `internal`):

```swift
import Foundation

// Paket ichidagi targetlar uchun ochiq, ammo App ko'rmaydi
package actor TodoStorage {
    private let userDefaultsKey = "saved_todo_items_cache"
    
    package init() {}
    
    package func save(items: [TodoItem]) throws {
        let data = try JSONEncoder().encode(items)
        UserDefaults.standard.set(data, forKey: userDefaultsKey)
    }
    
    package func load() -> [TodoItem] {
        guard let data = UserDefaults.standard.data(forKey: userDefaultsKey),
              let items = try? JSONDecoder().decode([TodoItem].self, from: data) else {
            return []
        }
        return items
    }
}
```

### 4.3. ViewModel (`ViewModels/TodoViewModel.swift`)
Swift 6 va iOS 17+ ning zamonaviy `@Observable` makrosi bilan yozamiz:

```swift
import SwiftUI
import Observation

@Observable
@MainActor
public final class TodoViewModel {
    public private(set) var items: [TodoItem] = []
    public var newItemTitle: String = ""
    public var errorMessage: String? = nil
    
    private let storage = TodoStorage()
    
    public init() {
        Task {
            await loadInitialItems()
        }
    }
    
    public func addItem() {
        let trimmed = newItemTitle.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }
        
        let item = TodoItem(title: trimmed)
        items.insert(item, at: 0)
        newItemTitle = ""
        persistItems()
    }
    
    public func toggleItem(_ item: TodoItem) {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index].isCompleted.toggle()
            persistItems()
        }
    }
    
    public func deleteItem(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
        persistItems()
    }
    
    private func loadInitialItems() async {
        self.items = await storage.load()
    }
    
    private func persistItems() {
        let currentItems = self.items
        Task {
            do {
                try await storage.save(items: currentItems)
            } catch {
                await MainActor.run {
                    self.errorMessage = "Saqlashda xatolik yuz berdi"
                }
            }
        }
    }
}
```

---

## 5. 4-Qadam: `Bundle.module` Orqali Resurslarni Boshqarish

Paket ichida `Sources/TodoLogicPackage/Resources/Assets.xcassets` yarating va unga quyidagi rangni qo'shing:
- Name: `TodoAccentColor` (Masalan, yoqimli zangori rang).

Endi paket ichida yordamchi UI extension yozamiz:

```swift
// Sources/TodoLogicPackage/UI/Theme.swift
import SwiftUI

public extension Color {
    static var todoAccent: Color {
        // DIQQAT: Bundle.module orqali olinadi!
        Color("TodoAccentColor", bundle: .module)
    }
}
```

---

## 6. 5-Qadam: Asosiy App Targetiga Bog'lash

1. Xcode da eng yuqoridagi **TodoListSPM** ko'k target ikonkasiga bosing.
2. **General** bo'limiga o'ting -> **Frameworks, Libraries, and Embedded Content**.
3. `+` tugmasini bosib, ro'yxatdan `TodoLogicPackage` ni tanlang.
4. `ContentView.swift` ni oching:

```swift
import SwiftUI
import TodoLogicPackage // SPM modulimizni import qilamiz!

struct ContentView: View {
    @State private var viewModel = TodoViewModel()
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 16) {
                // Yangi vazifa qo'shish qatori
                HStack {
                    TextField("Yangi vazifa yozing...", text: $viewModel.newItemTitle)
                        .textFieldStyle(.roundedBorder)
                    
                    Button {
                        viewModel.addItem()
                    } label: {
                        Image(systemName: "plus.circle.fill")
                            .font(.title2)
                            .foregroundColor(.todoAccent)
                    }
                }
                .padding(.horizontal)
                
                // Vazifalar ro'yxati
                List {
                    ForEach(viewModel.items) { item in
                        HStack {
                            Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundColor(item.isCompleted ? .green : .gray)
                                .onTapGesture {
                                    viewModel.toggleItem(item)
                                }
                            
                            Text(item.title)
                                .strikethrough(item.isCompleted, color: .secondary)
                                .foregroundColor(item.isCompleted ? .secondary : .primary)
                        }
                    }
                    .onDelete(perform: viewModel.deleteItem)
                }
                .listStyle(.insetGrouped)
            }
            .navigationTitle("SPM ToDo")
        }
    }
}
```

---

## 7. 6-Qadam: Izolyatsiyalangan Unit Testlar (0.1 soniyalik tezlik)

Paket ichidagi `Tests/TodoLogicPackageTests/TodoViewModelTests.swift` faylida test yozamiz:

```swift
import XCTest
@testable import TodoLogicPackage

@MainActor
final class TodoViewModelTests: XCTestCase {
    
    func testAddItem_SuccessfullyAppends() {
        // Given
        let vm = TodoViewModel()
        vm.newItemTitle = "SPM Masterclass o'rganish"
        
        // When
        vm.addItem()
        
        // Then
        XCTAssertEqual(vm.items.count, 1)
        XCTAssertEqual(vm.items.first?.title, "SPM Masterclass o'rganish")
        XCTAssertFalse(vm.items.first?.isCompleted ?? true)
    }
    
    func testToggleItem_InvertsStatus() {
        // Given
        let vm = TodoViewModel()
        vm.newItemTitle = "Non sotib olish"
        vm.addItem()
        guard let item = vm.items.first else {
            XCTFail("Element topilmadi")
            return
        }
        
        // When
        vm.toggleItem(item)
        
        // Then
        XCTAssertTrue(vm.items.first?.isCompleted ?? false)
    }
}
```

### Testni ishga tushirish:
Terminalda paket papkasiga kiring va buyruq bering:
```bash
cd Packages/TodoLogicPackage
swift test
```
Natija:
```
Test Suite 'All tests' passed at 2026-10-15 23:45:00.012.
Executed 2 tests, with 0 failures in 0.042 seconds.
```
> **Xulosa:** Simulyator ochmasdan, butun iOS ilovasini build qilmasdan, 0.04 sekundda barcha biznes logika tekshirildi! Bu SPM ning kichik ilovalarda ham beradigan eng katta yutug'idir.
