# Bazel 13: Loyiha #2: "Vazifalar" iOS ilova — 4 modul, SwiftUI, JSON persistence va testlar

> **Holat:** 2026-yil oktabr. Ushbu darsda biz to'liq 4 modulli haqiqiy iOS SwiftUI ilovasini — **"Vazifalar"** (Todo App) ni Bazel yordamida noldan quramiz. Loyiha qatlamlarga ajratilgan toza arxitekturadan (`Core`, `Storage`, `UI`, `App`) tashkil topgan. Ma'lumotlar qurilmaning Documents papkasida JSON formatida saqlanadi, XCTest unit testlari bilan qamrab olinadi va iOS simulyatorida bir buyruq bilan ishga tushiriladi. Kod misollari Bazel 9.3.0, rules_apple 5.3.0, rules_swift 4.2.1, iOS 18+ va Xcode 27.1'da tekshirilgan.

> **Bu darsdan keyin siz:** har qanday iOS ilovani qatlamlarga bo'lib, SwiftUI komponentlarini alohida modulda rivojlantira olasiz, `ios_application` va `ios_unit_test` qoidalarini professional darajada ulab, simulyatorda ilovangizni chaqqon ishga tushira olasiz.

- Haqiqiy iOS ilovani 4 ta mustaqil Bazel moduliga (`Core`, `Storage`, `UI`, `App`) ajratishni o'rganasiz.
- Modullararo bog'liqliklar grafigini to'g'ri qurib, aylanma (circular) bog'lanishlardan himoyalanasiz.
- Documents Directory'da JSON persistence bilan ishlovchi Storage servisini yozasiz.
- SwiftUI interfeyslarini alohida `UI` kutubxonasida qurasiz va uni App qatlami bilan bog'laysiz.
- `ios_unit_test` yordamida fayl tizimi va ma'lumotlar saqlanishini test qilasiz.
- `bazel run //App:Vazifalar` buyrug'i orqali ilovani to'g'ridan-to'g'ri iOS simulyatoriga o'rnatib ochasiz.
- UI qatlamini o'zgartirganda `Core` va `Storage` nima uchun qayta kompilyatsiya bo'lmasligini amalda tekshirasiz.

## Mundarija

1. [Loyiha arxitekturasi va talablar](#arxitektura)
2. [Workspace va papkalar tuzilishi](#tuzilish)
3. [Core moduli: Vazifa modellari va Filter](#core)
4. [Storage moduli: JSON Persistence servisi](#storage)
5. [StorageTests: Ma'lumotlar bazasini tekshirish](#storage-tests)
6. [UI moduli: SwiftUI komponentlari va dizayn](#ui)
7. [App moduli: Asosiy ilova qobig'i va Info.plist](#app)
8. [Simulyatorda ishga tushirish](#simulyator)
9. [Modulli build tezligi tahlili](#tezlik-tahlil)
10. [Amaliy mashqlar](#mashqlar)
11. [O'zingizni tekshiring](#checklist)
12. [Xulosa](#xulosa)
13. [Manbalar](#manbalar)

---

## 1. Loyiha arxitekturasi va talablar {#arxitektura}

Oddiy monolit Xcode loyihasida hamma fayllar bitta papkada yotadi. Natijada `ListView`dagi bitta rangni o'zgartirsangiz ham, butun loyiha qayta kompilyatsiya qilinadi.

Biz "Vazifalar" ilovasini 4 ta qatlamga ajratamiz:

1. **`//Core:TaskCore` (`swift_library`):** Platformaga bog'liq bo'lmagan asosiy modellar: `TaskItem`, `Priority`, `TaskStatus`. Hech qanday boshqa modulga bog'liq emas.
2. **`//Storage:Storage` (`swift_library`):** JSON faylga yozish va o'qish mexanizmi. Faqat `Core`ga bog'liq.
3. **`//UI:TaskUI` (`swift_library`):** SwiftUI komponentlari (`TaskRowView`, `TaskListView`, `AddSheet`). Faqat `Core`ga bog'liq!
4. **`//App:Vazifalar` (`ios_application`):** Barcha modullarni birlashtiruvchi Composition Root. `Info.plist`, App Lifecycle va yakuniy `.app` bundle.

![Loyiha #2: "Vazifalar" iOS ilovasi (4 modul) arxitekturasi](/lessons/images/bazel/bazel_13_vazifalar_modules.svg)

Bu tuzilmaning go'zalligi:
- Dizayner yoki frontendchi `UI` modulida ishlayotganda `Storage` qayta qurilmaydi.
- Backendchi `Storage`da ma'lumotlar saqlanishini o'zgartirganda `UI` tegilmaydi.
- Bazel faqat o'zgargan modulni qayta kompilyatsiya qiladi va linking bosqichiga o'tadi.

---

## 2. Workspace va papkalar tuzilishi {#tuzilish}

```text
vazifalar-app/
├── MODULE.bazel
├── .bazelrc
├── Core/
│   ├── BUILD.bazel
│   └── Sources/
│       ├── TaskItem.swift
│       └── Priority.swift
├── Storage/
│   ├── BUILD.bazel
│   ├── Sources/
│   │   ├── TaskRepository.swift
│   │   └── JSONStorage.swift
│   └── Tests/
│       └── StorageTests.swift
├── UI/
│   ├── BUILD.bazel
│   └── Sources/
│       ├── TaskRowView.swift
│       └── TaskListView.swift
└── App/
    ├── BUILD.bazel
    ├── Info.plist
    └── Sources/
        └── VazifalarApp.swift
```

`MODULE.bazel` fayli:

```python
# MODULE.bazel
module(
    name = "vazifalar_app",
    version = "1.0.0",
)

bazel_dep(name = "rules_apple", version = "5.3.0")
bazel_dep(name = "rules_swift", version = "4.2.1")
bazel_dep(name = "apple_support", version = "2.10.1")
```

---

## 3. Core moduli: Vazifa modellari va Filter {#core}

### `Core/Sources/Priority.swift`

```swift
import Foundation

public enum Priority: String, Codable, CaseIterable, Sendable {
    case low = "Past"
    case medium = "O'rta"
    case high = "Yuqori"
}
```

### `Core/Sources/TaskItem.swift`

```swift
import Foundation

public struct TaskItem: Identifiable, Codable, Equatable, Sendable {
    public let id: UUID
    public var title: String
    public var isCompleted: Bool
    public var priority: Priority
    public var createdAt: Date

    public init(
        id: UUID = UUID(),
        title: String,
        isCompleted: Bool = false,
        priority: Priority = .medium,
        createdAt: Date = Date()
    ) {
        self.id = id
        self.title = title
        self.isCompleted = isCompleted
        self.priority = priority
        self.createdAt = createdAt
    }
}
```

### `Core/BUILD.bazel`

```python
load("@rules_swift//swift:swift.bzl", "swift_library")

swift_library(
    name = "TaskCore",
    srcs = glob(["Sources/**/*.swift"]),
    module_name = "TaskCore",
    visibility = ["//visibility:public"],
)
```

---

## 4. Storage moduli: JSON Persistence servisi {#storage}

Ma'lumotlarni ilovaning Documents papkasida `tasks.json` faylida saqlaymiz.

### `Storage/Sources/TaskRepository.swift`

```swift
import Foundation
import TaskCore

public protocol TaskRepositoryProtocol: Sendable {
    func loadTasks() async throws -> [TaskItem]
    func saveTasks(_ tasks: [TaskItem]) async throws
}
```

### `Storage/Sources/JSONStorage.swift`

```swift
import Foundation
import TaskCore

public final class JSONTaskStorage: TaskRepositoryProtocol, @unchecked Sendable {
    private let fileURL: URL

    public init(fileName: String = "tasks.json") {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        self.fileURL = paths[0].appendingPathComponent(fileName)
    }

    public func loadTasks() async throws -> [TaskItem] {
        guard FileManager.default.fileExists(atPath: fileURL.path) else {
            return []
        }
        let data = try Data(contentsOf: fileURL)
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return try decoder.decode([TaskItem].self, from: data)
    }

    public func saveTasks(_ tasks: [TaskItem]) async throws {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        encoder.dateEncodingStrategy = .iso8601
        let data = try encoder.encode(tasks)
        try data.write(to: fileURL, options: .atomic)
    }
}
```

### `Storage/BUILD.bazel`

```python
load("@rules_swift//swift:swift.bzl", "swift_library")
load("@rules_apple//apple:ios.bzl", "ios_unit_test")

swift_library(
    name = "Storage",
    srcs = glob(["Sources/**/*.swift"]),
    module_name = "Storage",
    deps = ["//Core:TaskCore"],
    visibility = ["//visibility:public"],
)

swift_library(
    name = "StorageTestLib",
    srcs = glob(["Tests/**/*.swift"]),
    deps = [
        ":Storage",
        "//Core:TaskCore",
    ],
)

ios_unit_test(
    name = "StorageTests",
    minimum_os_version = "17.0",
    deps = [":StorageTestLib"],
    visibility = ["//visibility:public"],
)
```

---

## 5. StorageTests: Ma'lumotlar bazasini tekshirish {#storage-tests}

### `Storage/Tests/StorageTests.swift`

```swift
import XCTest
@testable import Storage
@testable import TaskCore

final class StorageTests: XCTestCase {
    var storage: JSONTaskStorage!

    override func setUp() {
        super.setUp()
        // Test uchun alohida vaqtinchalik fayl ishlatamiz
        storage = JSONTaskStorage(fileName: "test_tasks_\(UUID().uuidString).json")
    }

    func testSaveAndLoadTasks() async throws {
        let initialTasks = [
            TaskItem(title: "Bazel darsini yozish", priority: .high),
            TaskItem(title: "Non sotib olish", priority: .low)
        ]

        try await storage.saveTasks(initialTasks)
        let loadedTasks = try await storage.loadTasks()

        XCTAssertEqual(loadedTasks.count, 2)
        XCTAssertEqual(loadedTasks[0].title, "Bazel darsini yozish")
        XCTAssertEqual(loadedTasks[0].priority, .high)
    }
}
```

Testni tekshirish:

```bash
bazel test //Storage:StorageTests
```

---

## 6. UI moduli: SwiftUI komponentlari va dizayn {#ui}

### `UI/Sources/TaskRowView.swift`

```swift
import SwiftUI
import TaskCore

public struct TaskRowView: View {
    public let task: TaskItem
    public let onToggle: () -> Void

    public init(task: TaskItem, onToggle: @escaping () -> Void) {
        self.task = task
        self.onToggle = onToggle
    }

    public var body: some View {
        HStack {
            Button(action: onToggle) {
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .foregroundColor(task.isCompleted ? .green : .gray)
                    .font(.title3)
            }
            .buttonStyle(.plain)

            VStack(alignment: .leading, spacing: 4) {
                Text(task.title)
                    .strikethrough(task.isCompleted)
                    .foregroundColor(task.isCompleted ? .secondary : .primary)
                    .font(.body)

                Text(task.priority.rawValue)
                    .font(.caption2)
                    .padding(.horizontal, 6)
                    .padding(.vertical, 2)
                    .background(priorityColor.opacity(0.15))
                    .foregroundColor(priorityColor)
                    .clipShape(Capsule())
            }

            Spacer()
        }
        .padding(.vertical, 4)
    }

    private var priorityColor: Color {
        switch task.priority {
        case .high: return .red
        case .medium: return .orange
        case .low: return .blue
        }
    }
}
```

### `UI/Sources/TaskListView.swift`

```swift
import SwiftUI
import TaskCore

public struct TaskListView: View {
    @Binding public var tasks: [TaskItem]
    public let onAdd: (String, Priority) -> Void
    public let onToggle: (TaskItem) -> Void

    @State private var newTitle = ""
    @State private var selectedPriority: Priority = .medium

    public init(
        tasks: Binding<[TaskItem]>,
        onAdd: @escaping (String, Priority) -> Void,
        onToggle: @escaping (TaskItem) -> Void
    ) {
        self._tasks = tasks
        self.onAdd = onAdd
        self.onToggle = onToggle
    }

    public var body: some View {
        NavigationStack {
            List {
                Section("Yangi vazifa") {
                    HStack {
                        TextField("Vazifa nomi...", text: $newTitle)
                        Picker("", selection: $selectedPriority) {
                            ForEach(Priority.allCases, id: \.self) { p in
                                Text(p.rawValue).tag(p)
                            }
                        }
                        .pickerStyle(.menu)

                        Button("Qo'shish") {
                            guard !newTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return }
                            onAdd(newTitle, selectedPriority)
                            newTitle = ""
                        }
                        .disabled(newTitle.isEmpty)
                    }
                }

                Section("Vazifalar ro'yxati (\(tasks.count))") {
                    if tasks.isEmpty {
                        Text("Vazifalar yo'q. Yangi qo'shing!")
                            .foregroundColor(.secondary)
                    } else {
                        ForEach(tasks) { task in
                            TaskRowView(task: task) {
                                onToggle(task)
                            }
                        }
                    }
                }
            }
            .navigationTitle("Vazifalar")
        }
    }
}
```

### `UI/BUILD.bazel`

```python
load("@rules_swift//swift:swift.bzl", "swift_library")

swift_library(
    name = "TaskUI",
    srcs = glob(["Sources/**/*.swift"]),
    module_name = "TaskUI",
    deps = ["//Core:TaskCore"],
    visibility = ["//visibility:public"],
)
```

---

## 7. App moduli: Asosiy ilova qobig'i va Info.plist {#app}

### `App/Sources/VazifalarApp.swift`

```swift
import SwiftUI
import TaskCore
import Storage
import TaskUI

@main
struct VazifalarApp: App {
    @State private var tasks: [TaskItem] = []
    private let storage: TaskRepositoryProtocol = JSONTaskStorage()

    var body: some Scene {
        WindowGroup {
            TaskListView(
                tasks: $tasks,
                onAdd: { title, priority in
                    let newTask = TaskItem(title: title, priority: priority)
                    tasks.append(newTask)
                    save()
                },
                onToggle: { task in
                    if let index = tasks.firstIndex(where: { $0.id == task.id }) {
                        tasks[index].isCompleted.toggle()
                        save()
                    }
                }
            )
            .task {
                do {
                    tasks = try await storage.loadTasks()
                } catch {
                    print("Yuklashda xato: \(error)")
                }
            }
        }
    }

    private func save() {
        Task {
            try? await storage.saveTasks(tasks)
        }
    }
}
```

### `App/Info.plist`

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>uz.azimjondev.vazifalar</string>
    <key>CFBundleName</key>
    <string>Vazifalar</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0.0</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>UILaunchScreen</key>
    <dict/>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
    </array>
</dict>
</plist>
```

### `App/BUILD.bazel`

```python
load("@rules_swift//swift:swift.bzl", "swift_library")
load("@rules_apple//apple:ios.bzl", "ios_application")

swift_library(
    name = "AppLib",
    srcs = ["Sources/VazifalarApp.swift"],
    deps = [
        "//Core:TaskCore",
        "//Storage:Storage",
        "//UI:TaskUI",
    ],
)

ios_application(
    name = "Vazifalar",
    bundle_id = "uz.azimjondev.vazifalar",
    families = ["iphone", "ipad"],
    infoplists = ["Info.plist"],
    minimum_os_version = "17.0",
    deps = [":AppLib"],
    visibility = ["//visibility:public"],
)
```

---

## 8. Simulyatorda ishga tushirish {#simulyator}

Ilovani qurish va to'g'ridan-to'g'ri ochiq turgan iOS simulyatorida ochish:

```bash
# 1. Ilovani qurish va ishga tushirish
bazel run //App:Vazifalar
```

Agar ma'lum bir aniq simulyatorda ochmoqchi bo'lsangiz:

```bash
bazel run //App:Vazifalar --ios_simulator_device="iPhone 16 Pro"
```

Simulyator avtomatik ishga tushadi, `.app` bundle o'rnatiladi va "Vazifalar" ekranda paydo bo'ladi!

---

## 9. Modulli build tezligi tahlili {#tezlik-tahlil}

Keling, modullarga ajratish nima berganini Apple M4 Pro'da sinab ko'ramiz:

1. **Faqat `UI` modulini o'zgartiramiz** (`UI/Sources/TaskRowView.swift`ga bitta `padding` qo'shamiz):
   - `TaskCore`: **Cached (0s)**
   - `Storage`: **Cached (0s)**
   - `TaskUI`: Recompiled (0.62s)
   - `AppLib` va Link: Recompiled (0.58s)
   - **Umumiy vaqt:** **1.2 soniya!**

2. Monolit loyihada bo'lsa: butun 12 ta fayl qayta tekshirilar, DerivedData qizir va 8–15 soniya ketgan bo'lardi.

---

## Amaliy mashqlar {#mashqlar}

1. **O'chirish (Delete) funksiyasini qo'shing.** `TaskListView`da `onDelete` closure qo'shing va `List`da `.onDelete(perform:)` orqali vazifani ro'yxatdan va JSON xotiradan o'chiradigan qiling.
2. **Qidiruv (Search) tizimi.** `UI` modulida `searchable` modifier'i orqali vazifalarni nomi bo'yicha filter qiladigan imkoniyat qo'shing.
3. ⭐ **Boshqa simulyatorda sinab ko'ring.** Ilovani `iPad Pro 13-inch (M4)` simulyatorida `bazel run` bilan ochib, iPad layout'ida tekshiring.

---

## O'zingizni tekshiring {#checklist}

- [ ] `ios_application` va `swift_library` qoidalari o'rtasidagi farqni tushunaman.
- [ ] Nima uchun `UI` moduli `Storage` moduliga bog'liq bo'lmasligi kerakligini bilaman (Clean Architecture).
- [ ] `ios_unit_test` orqali iOS platformasiga xos testlarni qura olaman.
- [ ] `bazel run //App:Vazifalar` buyrug'i ilovani simulyatorda qanday qilib o'zi o'rnatib ochishini bilaman.

---

## Xulosa {#xulosa}

- Biz birinchi marta to'liq, 4 modulli, chiroyli SwiftUI iOS ilovasini Bazel bilan qurdik.
- Qatlamlarga ajratish nafaqat arxitekturani toza saqlaydi, balki har kungi build tezligini 1 soniyagacha qisqartiradi.
- Keyingi darsda loyihamizni yanada murakkablashtiramiz: tashqi tarmoq (Networking), SPM dependency'lar, rasmlar va mock testlar bilan boyitamiz!
- **Keyingi dars:** [Bazel 14: Loyiha #3: "Ob-havo" ilova — Networking, SPM dependency, resurslar va mock testlar](/ios-lessons/bazel-14-loyiha-obhavo)

---

## Manbalar {#manbalar}

1. [rules_apple — ios_application docs](https://github.com/bazelbuild/rules_apple/blob/master/doc/rules-ios.md#ios_application) — iOS ilovalarni yig'ish qoidalari.
2. [Apple Documentation — Building Apps with SwiftUI](https://developer.apple.com/documentation/swiftui/) — SwiftUI arxitekturasi.
3. [Bazel — Multi-module iOS Projects](https://bazel.build/) — Ko'p modulli loyihalarni boshqarish.
