Azimjon Abdurasulov

11. Real Loyiha #3: E-Commerce App (5+ modul)

🚀 Tuist — Mukammal Qo‘llanma & Amaliyot

🛒 Real Loyiha #3: E-Commerce App — TMA Arxitektura

"Katta loyihalar — katta mas'uliyat. TMA arxitektura bu mas'uliyatni engillashtiradi!"


Loyiha Haqida

E-Commerce App — 5+ modulli micro-features arxitektura bilan. Bu eng katta amaliy loyiha — siz haqiqiy production-level tuzilmani qurasiz.

Arxitektura

┌──────────────────────────────────────────┐
│              ECommerceApp                 │
│               (.app)                      │
└───┬──────────┬───────────┬───────────────┘
    │          │           │
┌───▼────┐ ┌──▼─────┐ ┌───▼────────┐
│Products│ │  Cart   │ │  Profile   │
│Feature │ │ Feature │ │  Feature   │
└───┬────┘ └──┬─────┘ └───┬────────┘
    │          │           │
    └────┬─────┘           │
         │                 │
┌────────▼────────┐        │
│  CoreNetwork    │────────┘
│  (.framework)   │
└────────┬────────┘
         │
┌────────▼────────┐
│  DesignSystem   │
│  (.framework)   │
└─────────────────┘

1-Qadam: Loyiha Tuzilmasi

mkdir ECommerceTuist
cd ECommerceTuist

# Tuist fayllar
touch Tuist.swift
touch Project.swift
mkdir -p Tuist/ProjectDescriptionHelpers

# Modullar
for mod in CoreNetwork DesignSystem ProductsFeature CartFeature ProfileFeature; do
    mkdir -p Modules/$mod/Sources
done

mkdir -p Modules/DesignSystem/Resources

# App
mkdir -p App/Sources
mkdir -p App/Resources

2-Qadam: Tuist Konfiguratsiya

Tuist.swift

import ProjectDescription

let tuist = Tuist(
    project: .tuist(
        generationOptions: .options(
            enforceExplicitDependencies: true
        )
    )
)

Tuist/Package.swift

// swift-tools-version: 5.9
import PackageDescription

#if TUIST
import ProjectDescription

let packageSettings = PackageSettings(
    productTypes: [
        "Alamofire": .framework,
        "Kingfisher": .framework,
    ]
)
#endif

let package = Package(
    name: "ECommerceTuist",
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.9.0"),
        .package(url: "https://github.com/onevcat/Kingfisher", from: "7.12.0"),
    ]
)

3-Qadam: ProjectDescriptionHelpers

// Tuist/ProjectDescriptionHelpers/FeatureTarget.swift
import ProjectDescription

public extension Target {
    /// Feature module yaratish
    static func feature(
        name: String,
        dependencies: [TargetDependency] = []
    ) -> Target {
        .target(
            name: name,
            destinations: .iOS,
            product: .framework,
            bundleId: "com.example.\(name)",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .default,
            sources: ["Modules/\(name)/Sources/**"],
            dependencies: dependencies
        )
    }
    
    /// Core module yaratish (resource bilan)
    static func coreModule(
        name: String,
        resources: ResourceFileElements? = nil,
        dependencies: [TargetDependency] = []
    ) -> Target {
        .target(
            name: name,
            destinations: .iOS,
            product: .framework,
            bundleId: "com.example.\(name)",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .default,
            sources: ["Modules/\(name)/Sources/**"],
            resources: resources,
            dependencies: dependencies
        )
    }
}

4-Qadam: Project.swift

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "ECommerceTuist",
    targets: [
        // ═══════════════════════════════════
        // CORE MODULLAR
        // ═══════════════════════════════════
        
        // DesignSystem — UI komponentlar va ranglar
        .coreModule(
            name: "DesignSystem",
            resources: ["Modules/DesignSystem/Resources/**"],
            dependencies: []
        ),
        
        // CoreNetwork — API client
        .coreModule(
            name: "CoreNetwork",
            dependencies: [
                .external(name: "Alamofire"),
            ]
        ),
        
        // ═══════════════════════════════════
        // FEATURE MODULLAR
        // ═══════════════════════════════════
        
        // ProductsFeature — mahsulotlar ro'yxati
        .feature(
            name: "ProductsFeature",
            dependencies: [
                .target(name: "CoreNetwork"),
                .target(name: "DesignSystem"),
                .external(name: "Kingfisher"),
            ]
        ),
        
        // CartFeature — savatcha
        .feature(
            name: "CartFeature",
            dependencies: [
                .target(name: "DesignSystem"),
            ]
        ),
        
        // ProfileFeature — profil
        .feature(
            name: "ProfileFeature",
            dependencies: [
                .target(name: "CoreNetwork"),
                .target(name: "DesignSystem"),
            ]
        ),
        
        // ═══════════════════════════════════
        // ASOSIY ILOVA
        // ═══════════════════════════════════
        
        .target(
            name: "ECommerceApp",
            destinations: .iOS,
            product: .app,
            bundleId: "com.example.ECommerceApp",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .extendingDefault(with: [
                "UILaunchScreen": [
                    "UIColorName": "",
                    "UIImageName": "",
                ],
            ]),
            sources: ["App/Sources/**"],
            resources: ["App/Resources/**"],
            dependencies: [
                .target(name: "ProductsFeature"),
                .target(name: "CartFeature"),
                .target(name: "ProfileFeature"),
            ]
        ),
    ]
)

5-Qadam: DesignSystem — UI Kutubxona

Modules/DesignSystem/Sources/AppColors.swift

import SwiftUI

/// Loyiha bo'ylab ishlatiladigan ranglar
public enum AppColors {
    public static let primary = Color(hex: "2563EB")
    public static let secondary = Color(hex: "7C3AED")
    public static let success = Color(hex: "10B981")
    public static let warning = Color(hex: "F59E0B")
    public static let danger = Color(hex: "EF4444")
    public static let background = Color(hex: "F8FAFC")
    public static let cardBackground = Color.white
}

extension Color {
    init(hex: String) {
        let scanner = Scanner(string: hex)
        var rgbValue: UInt64 = 0
        scanner.scanHexInt64(&rgbValue)
        self.init(
            red: Double((rgbValue >> 16) & 0xFF) / 255,
            green: Double((rgbValue >> 8) & 0xFF) / 255,
            blue: Double(rgbValue & 0xFF) / 255
        )
    }
}

Modules/DesignSystem/Sources/AppButton.swift

import SwiftUI

/// Qayta ishlatilishi mumkin button
public struct AppButton: View {
    let title: String
    let style: ButtonStyle
    let action: () -> Void
    
    public enum ButtonStyle {
        case primary, secondary, destructive
    }
    
    public init(_ title: String, style: ButtonStyle = .primary, action: @escaping () -> Void) {
        self.title = title
        self.style = style
        self.action = action
    }
    
    public var body: some View {
        Button(action: action) {
            Text(title)
                .font(.headline)
                .foregroundStyle(.white)
                .frame(maxWidth: .infinity)
                .padding(.vertical, 14)
                .background(backgroundColor)
                .cornerRadius(12)
        }
    }
    
    private var backgroundColor: Color {
        switch style {
        case .primary: AppColors.primary
        case .secondary: AppColors.secondary
        case .destructive: AppColors.danger
        }
    }
}

6-Qadam: CoreNetwork — API Client

Modules/CoreNetwork/Sources/Product.swift

import Foundation

/// Mahsulot modeli
public struct Product: Codable, Identifiable {
    public let id: Int
    public let title: String
    public let price: Double
    public let description: String
    public let category: String
    public let image: String
    public let rating: Rating
    
    public struct Rating: Codable {
        public let rate: Double
        public let count: Int
    }
}

Modules/CoreNetwork/Sources/APIClient.swift

import Foundation
import Alamofire

/// API client — barcha network call'lar shu yerdan
public class APIClient {
    public static let shared = APIClient()
    private let baseURL = "https://fakestoreapi.com"
    
    private init() {}
    
    /// Mahsulotlar ro'yxatini olish
    public func fetchProducts() async throws -> [Product] {
        try await AF.request("\(baseURL)/products")
            .validate()
            .serializingDecodable([Product].self)
            .value
    }
    
    /// Bitta mahsulotni olish
    public func fetchProduct(id: Int) async throws -> Product {
        try await AF.request("\(baseURL)/products/\(id)")
            .validate()
            .serializingDecodable(Product.self)
            .value
    }
    
    /// Kategoriyalar
    public func fetchCategories() async throws -> [String] {
        try await AF.request("\(baseURL)/products/categories")
            .validate()
            .serializingDecodable([String].self)
            .value
    }
}

7-Qadam: ProductsFeature

Modules/ProductsFeature/Sources/ProductsViewModel.swift

import Foundation
import CoreNetwork

@MainActor
public class ProductsViewModel: ObservableObject {
    @Published public var products: [Product] = []
    @Published public var isLoading = false
    @Published public var errorMessage: String?
    
    public init() {}
    
    public func loadProducts() async {
        isLoading = true
        do {
            products = try await APIClient.shared.fetchProducts()
        } catch {
            errorMessage = error.localizedDescription
        }
        isLoading = false
    }
}

Modules/ProductsFeature/Sources/ProductsListView.swift

import SwiftUI
import CoreNetwork
import DesignSystem
import Kingfisher

public struct ProductsListView: View {
    @StateObject private var viewModel = ProductsViewModel()
    
    public init() {}
    
    public var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView("Yuklanmoqda...")
                } else {
                    productGrid
                }
            }
            .navigationTitle("🛍️ Mahsulotlar")
            .task {
                await viewModel.loadProducts()
            }
        }
    }
    
    private var productGrid: some View {
        ScrollView {
            LazyVGrid(columns: [
                GridItem(.flexible()),
                GridItem(.flexible()),
            ], spacing: 16) {
                ForEach(viewModel.products) { product in
                    ProductCardView(product: product)
                }
            }
            .padding()
        }
    }
}

struct ProductCardView: View {
    let product: Product
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Rasm (Kingfisher bilan)
            KFImage(URL(string: product.image))
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(height: 120)
                .frame(maxWidth: .infinity)
                .background(Color.gray.opacity(0.05))
                .cornerRadius(8)
            
            Text(product.title)
                .font(.caption)
                .lineLimit(2)
            
            Text("$\(String(format: "%.2f", product.price))")
                .font(.headline)
                .foregroundStyle(AppColors.primary)  // DesignSystem dan
            
            HStack(spacing: 2) {
                Image(systemName: "star.fill")
                    .font(.caption2)
                    .foregroundStyle(.yellow)
                Text("\(String(format: "%.1f", product.rating.rate))")
                    .font(.caption2)
            }
        }
        .padding(12)
        .background(AppColors.cardBackground)  // DesignSystem dan
        .cornerRadius(12)
        .shadow(color: .black.opacity(0.05), radius: 4, y: 2)
    }
}

8-Qadam: CartFeature

Modules/CartFeature/Sources/CartItem.swift

import Foundation

public struct CartItem: Identifiable {
    public let id = UUID()
    public let productName: String
    public let price: Double
    public var quantity: Int
    
    public init(productName: String, price: Double, quantity: Int = 1) {
        self.productName = productName
        self.price = price
        self.quantity = quantity
    }
    
    public var totalPrice: Double { price * Double(quantity) }
}

Modules/CartFeature/Sources/CartView.swift

import SwiftUI
import DesignSystem

public class CartManager: ObservableObject {
    @Published public var items: [CartItem] = []
    
    public init() {
        // Demo data
        items = [
            CartItem(productName: "iPhone 16 Pro", price: 999, quantity: 1),
            CartItem(productName: "AirPods Pro", price: 249, quantity: 2),
        ]
    }
    
    public var totalAmount: Double {
        items.reduce(0) { $0 + $1.totalPrice }
    }
}

public struct CartView: View {
    @StateObject private var cart = CartManager()
    
    public init() {}
    
    public var body: some View {
        NavigationStack {
            VStack {
                if cart.items.isEmpty {
                    Text("Savatcha bo'sh")
                        .foregroundStyle(.secondary)
                } else {
                    List {
                        ForEach(cart.items) { item in
                            HStack {
                                VStack(alignment: .leading) {
                                    Text(item.productName).font(.headline)
                                    Text("$\(String(format: "%.2f", item.price)) × \(item.quantity)")
                                        .font(.caption)
                                        .foregroundStyle(.secondary)
                                }
                                Spacer()
                                Text("$\(String(format: "%.2f", item.totalPrice))")
                                    .bold()
                                    .foregroundStyle(AppColors.primary)
                            }
                        }
                    }
                    
                    // Total va button
                    VStack(spacing: 12) {
                        HStack {
                            Text("Jami:").font(.title3)
                            Spacer()
                            Text("$\(String(format: "%.2f", cart.totalAmount))")
                                .font(.title2).bold()
                                .foregroundStyle(AppColors.primary)
                        }
                        
                        AppButton("Buyurtma berish", style: .primary) {
                            // TODO: Checkout
                        }
                    }
                    .padding()
                }
            }
            .navigationTitle("🛒 Savatcha")
        }
    }
}

9-Qadam: ProfileFeature

Modules/ProfileFeature/Sources/ProfileView.swift

import SwiftUI
import DesignSystem

public struct ProfileView: View {
    public init() {}
    
    public var body: some View {
        NavigationStack {
            List {
                // Avatar
                Section {
                    HStack(spacing: 16) {
                        Image(systemName: "person.circle.fill")
                            .font(.system(size: 60))
                            .foregroundStyle(AppColors.primary)
                        VStack(alignment: .leading) {
                            Text("Azimjon")
                                .font(.title2).bold()
                            Text("azimjon@example.com")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                        }
                    }
                    .padding(.vertical, 8)
                }
                
                Section("Sozlamalar") {
                    Label("Buyurtmalar", systemImage: "bag")
                    Label("Yetkazib berish", systemImage: "truck.box")
                    Label("To'lov usullari", systemImage: "creditcard")
                    Label("Bildirishnomalar", systemImage: "bell")
                }
                
                Section {
                    AppButton("Chiqish", style: .destructive) { }
                        .listRowInsets(EdgeInsets())
                        .listRowBackground(Color.clear)
                }
            }
            .navigationTitle("👤 Profil")
        }
    }
}

10-Qadam: App — Yig'ish

App/Sources/ECommerceApp.swift

import SwiftUI
import ProductsFeature
import CartFeature
import ProfileFeature

@main
struct ECommerceApp: App {
    var body: some Scene {
        WindowGroup {
            TabView {
                ProductsListView()
                    .tabItem {
                        Label("Mahsulotlar", systemImage: "bag")
                    }
                
                CartView()
                    .tabItem {
                        Label("Savatcha", systemImage: "cart")
                    }
                
                ProfileView()
                    .tabItem {
                        Label("Profil", systemImage: "person")
                    }
            }
        }
    }
}

11-Qadam: Build va Run

tuist install    # Alamofire, Kingfisher yuklash
tuist generate   # Xcode loyiha yaratish
# Dependency graph ko'rish
tuist graph

Nimani o'rgandik?

MavzuTushuntirish
5+ modulCore (Network, DesignSystem) + Feature (Products, Cart, Profile)
ProjectDescriptionHelpersTarget yaratishni avtomatlashtirish
DesignSystemLoyiha bo'ylab umumiy UI komponentlar
External depsAlamofire + Kingfisher integratsiya
Micro-featuresHar bir feature alohida modul

💡 Keyingi loyiha: Real Loyiha #4: Social Media App — Workspace, cache, CI/CD.

"5 ta modul bilan E-Commerce qurishni o'rgandingiz. Endi siz — arxitektor!" 🏛️

Markdown manba

iOS Lessons