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_applicationvaios_unit_testqoidalarini 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
UIkutubxonasida qurasiz va uni App qatlami bilan bog'laysiz. ios_unit_testyordamida fayl tizimi va ma'lumotlar saqlanishini test qilasiz.bazel run //App:Vazifalarbuyrug'i orqali ilovani to'g'ridan-to'g'ri iOS simulyatoriga o'rnatib ochasiz.- UI qatlamini o'zgartirganda
CorevaStoragenima uchun qayta kompilyatsiya bo'lmasligini amalda tekshirasiz.
Mundarija
- Loyiha arxitekturasi va talablar
- Workspace va papkalar tuzilishi
- Core moduli: Vazifa modellari va Filter
- Storage moduli: JSON Persistence servisi
- StorageTests: Ma'lumotlar bazasini tekshirish
- UI moduli: SwiftUI komponentlari va dizayn
- App moduli: Asosiy ilova qobig'i va Info.plist
- Simulyatorda ishga tushirish
- Modulli build tezligi tahlili
- Amaliy mashqlar
- O'zingizni tekshiring
- Xulosa
- 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:
- **
//Core:TaskCore(swift_library):** Platformaga bog'liq bo'lmagan asosiy modellar:TaskItem,Priority,TaskStatus. Hech qanday boshqa modulga bog'liq emas. - **
//Storage:Storage(swift_library):** JSON faylga yozish va o'qish mexanizmi. FaqatCorega bog'liq. - **
//UI:TaskUI(swift_library):** SwiftUI komponentlari (TaskRowView,TaskListView,AddSheet). FaqatCorega bog'liq! - **
//App:Vazifalar(ios_application):** Barcha modullarni birlashtiruvchi Composition Root.Info.plist, App Lifecycle va yakuniy.appbundle.
Loyiha #2: "Vazifalar" iOS ilovasi (4 modul) arxitekturasi
Bu tuzilmaning go'zalligi:
- Dizayner yoki frontendchi
UImodulida ishlayotgandaStorageqayta qurilmaydi. - Backendchi
Storageda ma'lumotlar saqlanishini o'zgartirgandaUItegilmaydi. - 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:
- **Faqat
UImodulini o'zgartiramiz** (UI/Sources/TaskRowView.swiftga bittapaddingqo'shamiz): TaskCore: Cached (0s)Storage: Cached (0s)TaskUI: Recompiled (0.62s)AppLibva Link: Recompiled (0.58s)- Umumiy vaqt: 1.2 soniya!
- Monolit loyihada bo'lsa: butun 12 ta fayl qayta tekshirilar, DerivedData qizir va 8–15 soniya ketgan bo'lardi.
Amaliy mashqlar {#mashqlar}
- O'chirish (Delete) funksiyasini qo'shing.
TaskListViewdaonDeleteclosure qo'shing vaListda.onDelete(perform:)orqali vazifani ro'yxatdan va JSON xotiradan o'chiradigan qiling. - Qidiruv (Search) tizimi.
UImodulidasearchablemodifier'i orqali vazifalarni nomi bo'yicha filter qiladigan imkoniyat qo'shing. - ⭐ Boshqa simulyatorda sinab ko'ring. Ilovani
iPad Pro 13-inch (M4)simulyatoridabazel runbilan ochib, iPad layout'ida tekshiring.
O'zingizni tekshiring {#checklist}
- [ ]
ios_applicationvaswift_libraryqoidalari o'rtasidagi farqni tushunaman. - [ ] Nima uchun
UImoduliStoragemoduliga bog'liq bo'lmasligi kerakligini bilaman (Clean Architecture). - [ ]
ios_unit_testorqali iOS platformasiga xos testlarni qura olaman. - [ ]
bazel run //App:Vazifalarbuyrug'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
Manbalar {#manbalar}
- rules_apple — ios_application docs — iOS ilovalarni yig'ish qoidalari.
- Apple Documentation — Building Apps with SwiftUI — SwiftUI arxitekturasi.
- Bazel — Multi-module iOS Projects — Ko'p modulli loyihalarni boshqarish.