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 generateworkflow'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?
| Mavzu | Tushuntirish |
|---|---|
tuist init / manual setup | Loyiha yaratish |
.framework target | Mustaqil modul (TodoLogic) |
.app target | Asosiy ilova (TodoApp) |
.target(name:) dependency | Modullar arasi bog'lanish |
tuist generate | Xcode loyiha yaratish |
tuist test | Testlarni ishga tushirish |
public modifier | Module 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!" 🏆