Azimjon Abdurasulov

09. Real Loyiha #1: ToDo App (2 modul)

🚀 Tuist — Mukammal Qo‘llanma & Amaliyot

📱 Real Loyiha #1: ToDo App — Tuist Bilan

"Har bir usta birinchi uyini quradi. Sizning birinchi Tuist loyihangiz — ToDo App!"


Loyiha Haqida

ToDo App — 2 modulli oddiy ilova. Bu loyihada siz:

  • ✅ Tuist loyiha yaratishni o'rganasiz
  • ✅ Module'larga bo'lishni mashq qilasiz
  • ✅ tuist generate workflow'ni o'zlashtirasiz

Arxitektura

┌──────────────┐
│   TodoApp    │  ← Asosiy ilova (SwiftUI)
│   (.app)     │
└──────┬───────┘
       │ depends on
┌──────▼───────┐
│  TodoLogic   │  ← Biznes logika (Framework)
│ (.framework) │
└──────────────┘

1-Qadam: Loyiha Yaratish

# Papka yaratish
mkdir TodoTuist
cd TodoTuist

# Tuist fayllarini yaratish
touch Tuist.swift
touch Project.swift
mkdir -p Tuist

2-Qadam: Tuist.swift

// Tuist.swift
import ProjectDescription

let tuist = Tuist()

3-Qadam: Project.swift

// Project.swift
import ProjectDescription

let project = Project(
    name: "TodoTuist",
    targets: [
        // 1-Modul: TodoLogic — biznes logika
        .target(
            name: "TodoLogic",
            destinations: .iOS,
            product: .framework,
            bundleId: "com.example.TodoLogic",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .default,
            sources: ["TodoLogic/Sources/**"],
            dependencies: []
        ),
        
        // 2-Modul: TodoApp — asosiy ilova
        .target(
            name: "TodoApp",
            destinations: .iOS,
            product: .app,
            bundleId: "com.example.TodoApp",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .extendingDefault(with: [
                "UILaunchScreen": [
                    "UIColorName": "",
                    "UIImageName": "",
                ],
            ]),
            sources: ["TodoApp/Sources/**"],
            dependencies: [
                .target(name: "TodoLogic")  // TodoLogic ga bog'liq
            ]
        ),
        
        // 3-Modul: Testlar
        .target(
            name: "TodoLogicTests",
            destinations: .iOS,
            product: .unitTests,
            bundleId: "com.example.TodoLogicTests",
            sources: ["TodoLogic/Tests/**"],
            dependencies: [
                .target(name: "TodoLogic")
            ]
        ),
    ]
)

4-Qadam: Papkalar va Fayllar Yaratish

# Papkalar
mkdir -p TodoLogic/Sources
mkdir -p TodoApp/Sources
mkdir -p TodoLogic/Tests

5-Qadam: TodoLogic — Biznes Logika

TodoLogic/Sources/TodoItem.swift

import Foundation

/// Bitta vazifa (task) modeli
public struct TodoItem: Identifiable, 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
    }
}

TodoLogic/Sources/TodoManager.swift

import Foundation
import Combine

/// Vazifalarni boshqaruvchi manager
/// ObservableObject — SwiftUI bilan ishlash uchun
public class TodoManager: ObservableObject {
    @Published public private(set) var items: [TodoItem] = []
    
    public init() {}
    
    /// Yangi vazifa qo'shish
    public func addItem(title: String) {
        guard !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
        let item = TodoItem(title: title)
        items.append(item)
    }
    
    /// Vazifa holatini o'zgartirish (bajarildi/bajarilmadi)
    public func toggleItem(id: UUID) {
        guard let index = items.firstIndex(where: { $0.id == id }) else { return }
        items[index].isCompleted.toggle()
    }
    
    /// Vazifani o'chirish
    public func deleteItem(id: UUID) {
        items.removeAll { $0.id == id }
    }
    
    /// Bajarilgan vazifalar soni
    public var completedCount: Int {
        items.filter(\.isCompleted).count
    }
    
    /// Bajarilmagan vazifalar soni
    public var pendingCount: Int {
        items.filter { !$0.isCompleted }.count
    }
}

6-Qadam: TodoApp — SwiftUI UI

TodoApp/Sources/TodoTuistApp.swift

import SwiftUI

@main
struct TodoTuistApp: App {
    var body: some Scene {
        WindowGroup {
            TodoListView()
        }
    }
}

TodoApp/Sources/TodoListView.swift

import SwiftUI
import TodoLogic  // ← Framework import!

struct TodoListView: View {
    @StateObject private var manager = TodoManager()
    @State private var newItemTitle = ""
    @State private var showingStats = false
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // Yangi vazifa qo'shish
                addItemSection
                
                // Vazifalar ro'yxati
                if manager.items.isEmpty {
                    emptyStateView
                } else {
                    itemListView
                }
            }
            .navigationTitle("📝 Vazifalar")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: { showingStats.toggle() }) {
                        Image(systemName: "chart.bar")
                    }
                }
            }
            .alert("📊 Statistika", isPresented: $showingStats) {
                Button("OK") {}
            } message: {
                Text("""
                Jami: \(manager.items.count)
                Bajarilgan: \(manager.completedCount) ✅
                Kutilmoqda: \(manager.pendingCount) ⏳
                """)
            }
        }
    }
    
    // MARK: - Views
    
    private var addItemSection: some View {
        HStack(spacing: 12) {
            TextField("Yangi vazifa...", text: $newItemTitle)
                .textFieldStyle(.roundedBorder)
                .onSubmit(addItem)
            
            Button(action: addItem) {
                Image(systemName: "plus.circle.fill")
                    .font(.title2)
                    .foregroundStyle(.blue)
            }
            .disabled(newItemTitle.trimmingCharacters(in: .whitespaces).isEmpty)
        }
        .padding()
    }
    
    private var emptyStateView: some View {
        VStack(spacing: 16) {
            Spacer()
            Image(systemName: "checkmark.circle")
                .font(.system(size: 60))
                .foregroundStyle(.green)
            Text("Hech qanday vazifa yo'q!")
                .font(.title2)
                .foregroundStyle(.secondary)
            Text("Yuqoridagi maydonga yangi vazifa yozing")
                .font(.subheadline)
                .foregroundStyle(.tertiary)
            Spacer()
        }
    }
    
    private var itemListView: some View {
        List {
            ForEach(manager.items) { item in
                TodoItemRow(item: item) {
                    manager.toggleItem(id: item.id)
                }
            }
            .onDelete { indexSet in
                for index in indexSet {
                    manager.deleteItem(id: manager.items[index].id)
                }
            }
        }
        .listStyle(.insetGrouped)
    }
    
    // MARK: - Actions
    
    private func addItem() {
        manager.addItem(title: newItemTitle)
        newItemTitle = ""
    }
}

// MARK: - TodoItemRow

struct TodoItemRow: View {
    let item: TodoItem
    let onToggle: () -> Void
    
    var body: some View {
        HStack {
            Button(action: onToggle) {
                Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
                    .foregroundStyle(item.isCompleted ? .green : .gray)
                    .font(.title3)
            }
            .buttonStyle(.plain)
            
            VStack(alignment: .leading, spacing: 4) {
                Text(item.title)
                    .strikethrough(item.isCompleted)
                    .foregroundStyle(item.isCompleted ? .secondary : .primary)
                
                Text(item.createdAt, style: .relative)
                    .font(.caption)
                    .foregroundStyle(.tertiary)
            }
        }
        .padding(.vertical, 4)
    }
}

7-Qadam: Unit Test

TodoLogic/Tests/TodoManagerTests.swift

import XCTest
@testable import TodoLogic

final class TodoManagerTests: XCTestCase {
    var manager: TodoManager!
    
    override func setUp() {
        manager = TodoManager()
    }
    
    func testAddItem() {
        manager.addItem(title: "Test vazifa")
        XCTAssertEqual(manager.items.count, 1)
        XCTAssertEqual(manager.items.first?.title, "Test vazifa")
        XCTAssertFalse(manager.items.first?.isCompleted ?? true)
    }
    
    func testAddEmptyItem() {
        manager.addItem(title: "")
        manager.addItem(title: "   ")
        XCTAssertEqual(manager.items.count, 0, "Bo'sh vazifa qo'shilmasligi kerak")
    }
    
    func testToggleItem() {
        manager.addItem(title: "Test")
        let id = manager.items[0].id
        
        manager.toggleItem(id: id)
        XCTAssertTrue(manager.items[0].isCompleted)
        
        manager.toggleItem(id: id)
        XCTAssertFalse(manager.items[0].isCompleted)
    }
    
    func testDeleteItem() {
        manager.addItem(title: "O'chiriladi")
        let id = manager.items[0].id
        
        manager.deleteItem(id: id)
        XCTAssertEqual(manager.items.count, 0)
    }
    
    func testStats() {
        manager.addItem(title: "A")
        manager.addItem(title: "B")
        manager.addItem(title: "C")
        
        manager.toggleItem(id: manager.items[0].id)
        
        XCTAssertEqual(manager.completedCount, 1)
        XCTAssertEqual(manager.pendingCount, 2)
    }
}

8-Qadam: Generate va Run!

# Xcode loyiha yaratish
tuist generate

# Yoki terminaldan build
tuist build

# Testlarni ishga tushirish
tuist test

Natija: Xcode ochiladi, Cmd+R bosing — simulatorda ToDo App ishlaydi! 🎉


Loyiha Struturasi (Yakuniy)

TodoTuist/
├── Tuist.swift
├── Project.swift
├── TodoLogic/
│   ├── Sources/
│   │   ├── TodoItem.swift
│   │   └── TodoManager.swift
│   └── Tests/
│       └── TodoManagerTests.swift
└── TodoApp/
    └── Sources/
        ├── TodoTuistApp.swift
        └── TodoListView.swift

Nimani o'rgandik?

MavzuTushuntirish
tuist init / manual setupLoyiha yaratish
.framework targetMustaqil modul (TodoLogic)
.app targetAsosiy ilova (TodoApp)
.target(name:) dependencyModullar arasi bog'lanish
tuist generateXcode loyiha yaratish
tuist testTestlarni ishga tushirish
public modifierModule API — boshqa modullar ko'rishi uchun

💡 Keyingi loyiha: Real Loyiha #2: Weather App — 3 modulli loyiha, external dependency (Alamofire), API integration.

"Birinchi loyihani qurdingiz! Endi murakkab loyihalar sizni kutmoqda!" 🏆

Markdown manba

iOS Lessons