Azimjon Abdurasulov

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

📦 Bazel iOS — Mukammal Qo‘llanma & Amaliyot

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
  2. Workspace va papkalar tuzilishi
  3. Core moduli: Vazifa modellari va Filter
  4. Storage moduli: JSON Persistence servisi
  5. StorageTests: Ma'lumotlar bazasini tekshirish
  6. UI moduli: SwiftUI komponentlari va dizayn
  7. App moduli: Asosiy ilova qobig'i va Info.plist
  8. Simulyatorda ishga tushirish
  9. Modulli build tezligi tahlili
  10. Amaliy mashqlar
  11. O'zingizni tekshiring
  12. Xulosa
  13. Manbalar

1. Loyiha arxitekturasi va talablar {#arxitektura}

Oddiy monolit Xcode loyihasida hamma fayllar bitta papkada yotadi. Natijada ListViewdagi 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 Corega bog'liq.
  3. **//UI:TaskUI (swift_library):** SwiftUI komponentlari (TaskRowView, TaskListView, AddSheet). Faqat Corega 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

Bu tuzilmaning go'zalligi:

  • Dizayner yoki frontendchi UI modulida ishlayotganda Storage qayta qurilmaydi.
  • Backendchi Storageda 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}

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:

# 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

import Foundation

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

Core/Sources/TaskItem.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

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

import Foundation
import TaskCore

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

Storage/Sources/JSONStorage.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

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

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:

bazel test //Storage:StorageTests

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

UI/Sources/TaskRowView.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

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

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

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 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

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:

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

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

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.swiftga bitta padding qo'shamiz):
  2. TaskCore: Cached (0s)
  3. Storage: Cached (0s)
  4. TaskUI: Recompiled (0.62s)
  5. AppLib va Link: Recompiled (0.58s)
  6. Umumiy vaqt: 1.2 soniya!
  7. 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. TaskListViewda onDelete closure qo'shing va Listda .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}


Manbalar {#manbalar}

  1. rules_apple — ios_application docs — iOS ilovalarni yig'ish qoidalari.
  2. Apple Documentation — Building Apps with SwiftUI — SwiftUI arxitekturasi.
  3. Bazel — Multi-module iOS Projects — Ko'p modulli loyihalarni boshqarish.

Markdown manba

iOS Lessons