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?
| Mavzu | Tushuntirish |
|---|---|
| 5+ modul | Core (Network, DesignSystem) + Feature (Products, Cart, Profile) |
| ProjectDescriptionHelpers | Target yaratishni avtomatlashtirish |
| DesignSystem | Loyiha bo'ylab umumiy UI komponentlar |
| External deps | Alamofire + Kingfisher integratsiya |
| Micro-features | Har 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!" 🏛️