Azimjon Abdurasulov

13. [Demo Easy] SPM bilan ToDo App (Local Package & Resurslar)

Arxitektura

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

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

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:

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:

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

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:

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.

Markdown manba

iOS Lessons