Azimjon Abdurasulov

12. Real Loyiha #4: Social Media (Workspace + CI/CD)

🚀 Tuist — Mukammal Qo‘llanma & Amaliyot

📱 Real Loyiha #4: Social Media App — Workspace + Cache + CI/CD

"Katta jamoalar katta loyihalar quradi. Multi-project workspace va cache — bu jamoa ishi uchun tayyor bo'lish demak!"


Loyiha Haqida

Social Media App — multi-project workspace bilan tuzilgan loyiha. Bu loyihada:

  • ✅ Workspace.swift bilan ko'p project'ni boshqarish
  • ✅ Binary cache ishlatish
  • ✅ CI/CD pipeline sozlash
  • ✅ GitHub Actions bilan avtomatik build/test

Arxitektura

SocialMediaApp (Workspace)
├── Projects/
│   ├── App/           ← Asosiy ilova
│   ├── Features/
│   │   ├── Feed/      ← Yangiliklar lentasi
│   │   ├── Chat/      ← Xabarlar
│   │   └── Stories/   ← Hikoyalar
│   └── Core/
│       ├── Networking/ ← API layer
│       ├── Storage/    ← Local storage
│       └── UI/         ← Shared UI components

1-Qadam: Workspace Strukturasi

mkdir SocialMediaTuist
cd SocialMediaTuist

# Root fayllar
touch Tuist.swift
touch Workspace.swift
mkdir -p Tuist/ProjectDescriptionHelpers

# Core projects
mkdir -p Projects/Core/Networking/Sources
mkdir -p Projects/Core/Storage/Sources
mkdir -p Projects/Core/UI/Sources

# Feature projects
mkdir -p Projects/Features/Feed/Sources
mkdir -p Projects/Features/Chat/Sources
mkdir -p Projects/Features/Stories/Sources

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

# Har bir project uchun Project.swift
for dir in Projects/Core/Networking Projects/Core/Storage Projects/Core/UI \
           Projects/Features/Feed Projects/Features/Chat Projects/Features/Stories \
           Projects/App; do
    touch $dir/Project.swift
done

2-Qadam: Root Fayllar

Tuist.swift

import ProjectDescription

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

Workspace.swift

import ProjectDescription

let workspace = Workspace(
    name: "SocialMediaApp",
    projects: [
        "Projects/App",
        "Projects/Core/Networking",
        "Projects/Core/Storage",
        "Projects/Core/UI",
        "Projects/Features/Feed",
        "Projects/Features/Chat",
        "Projects/Features/Stories",
    ]
)

Tuist/Package.swift

// swift-tools-version: 5.9
import PackageDescription

#if TUIST
import ProjectDescription

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

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

3-Qadam: ProjectDescriptionHelpers

// Tuist/ProjectDescriptionHelpers/TargetHelpers.swift
import ProjectDescription

public enum ProjectPaths {
    /// Root dan project'gacha bo'lgan yo'l
    public static func corePath(_ name: String) -> Path {
        "../../Core/\(name)"
    }
    
    public static func featurePath(_ name: String) -> Path {
        "../../Features/\(name)"
    }
}

public extension Target {
    static func coreTarget(
        name: String,
        dependencies: [TargetDependency] = []
    ) -> Target {
        .target(
            name: name,
            destinations: .iOS,
            product: .framework,
            bundleId: "com.social.\(name)",
            deploymentTargets: .iOS("17.0"),
            infoPlist: .default,
            sources: ["Sources/**"],
            dependencies: dependencies
        )
    }
    
    static func featureTarget(
        name: String,
        dependencies: [TargetDependency] = []
    ) -> Target {
        .target(
            name: name,
            destinations: .iOS,
            product: .framework,
            bundleId: "com.social.\(name)",
            deploymentTargets: .iOS("17.0"),
            infoPlist: .default,
            sources: ["Sources/**"],
            dependencies: dependencies
        )
    }
}

4-Qadam: Core Project.swift Fayllar

Projects/Core/Networking/Project.swift

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "Networking",
    targets: [
        .coreTarget(
            name: "Networking",
            dependencies: [
                .external(name: "Alamofire"),
            ]
        )
    ]
)

Projects/Core/Storage/Project.swift

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "Storage",
    targets: [
        .coreTarget(name: "Storage")
    ]
)

Projects/Core/UI/Project.swift

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "SharedUI",
    targets: [
        .coreTarget(name: "SharedUI")
    ]
)

5-Qadam: Feature Project.swift Fayllar

Projects/Features/Feed/Project.swift

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "FeedFeature",
    targets: [
        .featureTarget(
            name: "FeedFeature",
            dependencies: [
                .project(target: "Networking", path: "../../Core/Networking"),
                .project(target: "SharedUI", path: "../../Core/UI"),
            ]
        )
    ]
)

Projects/Features/Chat/Project.swift

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "ChatFeature",
    targets: [
        .featureTarget(
            name: "ChatFeature",
            dependencies: [
                .project(target: "Networking", path: "../../Core/Networking"),
                .project(target: "Storage", path: "../../Core/Storage"),
                .project(target: "SharedUI", path: "../../Core/UI"),
            ]
        )
    ]
)

Projects/Features/Stories/Project.swift

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "StoriesFeature",
    targets: [
        .featureTarget(
            name: "StoriesFeature",
            dependencies: [
                .project(target: "Networking", path: "../../Core/Networking"),
                .project(target: "SharedUI", path: "../../Core/UI"),
            ]
        )
    ]
)

6-Qadam: App Project.swift

// Projects/App/Project.swift
import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "SocialMediaApp",
    targets: [
        .target(
            name: "SocialMediaApp",
            destinations: .iOS,
            product: .app,
            bundleId: "com.social.app",
            deploymentTargets: .iOS("17.0"),
            infoPlist: .extendingDefault(with: [
                "UILaunchScreen": ["UIColorName": "", "UIImageName": ""],
            ]),
            sources: ["Sources/**"],
            resources: ["Resources/**"],
            dependencies: [
                .project(target: "FeedFeature", path: "../Features/Feed"),
                .project(target: "ChatFeature", path: "../Features/Chat"),
                .project(target: "StoriesFeature", path: "../Features/Stories"),
            ]
        )
    ]
)

7-Qadam: Source Fayllari (Qisqacha)

Projects/Core/Networking/Sources/NetworkClient.swift

import Foundation
import Alamofire

public class NetworkClient {
    public static let shared = NetworkClient()
    private init() {}
    
    public func get<T: Decodable>(_ path: String) async throws -> T {
        try await AF.request("https://api.example.com\(path)")
            .validate()
            .serializingDecodable(T.self)
            .value
    }
}

Projects/Features/Feed/Sources/FeedView.swift

import SwiftUI
import Networking
import SharedUI

public struct FeedView: View {
    public init() {}
    
    public var body: some View {
        NavigationStack {
            List {
                ForEach(0..<10, id: \.self) { i in
                    VStack(alignment: .leading, spacing: 8) {
                        HStack {
                            Image(systemName: "person.circle.fill")
                                .font(.title)
                            Text("Foydalanuvchi \(i + 1)")
                                .font(.headline)
                        }
                        Text("Bu test post matni #\(i + 1) 📝")
                        HStack(spacing: 20) {
                            Label("124", systemImage: "heart")
                            Label("32", systemImage: "bubble.left")
                            Label("8", systemImage: "arrow.2.squarepath")
                        }
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    }
                    .padding(.vertical, 4)
                }
            }
            .navigationTitle("📰 Lenta")
        }
    }
}

Projects/App/Sources/SocialMediaApp.swift

import SwiftUI
import FeedFeature
import ChatFeature
import StoriesFeature

@main
struct SocialMediaApp: App {
    var body: some Scene {
        WindowGroup {
            TabView {
                FeedView()
                    .tabItem { Label("Lenta", systemImage: "house") }
                
                Text("Chat") // ChatView()
                    .tabItem { Label("Chat", systemImage: "message") }
                
                Text("Stories") // StoriesView()
                    .tabItem { Label("Hikoyalar", systemImage: "camera") }
            }
        }
    }
}

8-Qadam: Binary Cache Ishlatish

# 1. Dependency yuklash
tuist install

# 2. Barcha modullarni cache'lash
tuist cache

# 3. Faqat FeedFeature ustida ishlash (qolganlar binary)
tuist generate FeedFeature

# Natija: Faqat FeedFeature source bo'ladi
# Networking, Storage, SharedUI → binary (tez!)

9-Qadam: CI/CD — GitHub Actions

.github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: macos-14
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Setup Tuist
        run: |
          brew tap tuist/tuist
          brew install --formula tuist
      
      - name: Cache Tuist
        uses: actions/cache@v3
        with:
          path: |
            ~/.tuist/Cache
            Tuist/.build
          key: tuist-${{ hashFiles('**/Project.swift', '**/Package.swift') }}
          restore-keys: tuist-
      
      - name: Install Dependencies
        run: tuist install
      
      - name: Generate
        run: tuist generate --no-open
      
      - name: Build
        run: |
          xcodebuild build \
            -workspace SocialMediaApp.xcworkspace \
            -scheme SocialMediaApp \
            -destination "platform=iOS Simulator,name=iPhone 16" \
            -configuration Debug
      
      - name: Test
        run: tuist test

10-Qadam: Generate va Run

tuist install
tuist generate
# Xcode ochiladi → Cmd+R

Graph ko'rish

tuist graph
┌────────────────┐
│ SocialMediaApp │
└──┬─────┬───┬───┘
   │     │   │
┌──▼──┐┌─▼──┐│
│Feed ││Chat││
└──┬──┘└─┬──┘│
   │     │   │
   │  ┌──▼───▼────┐     ┌──────────┐
   │  │  Storage   │     │ Stories  │
   │  └────────────┘     └────┬─────┘
   │                          │
   └────────┬─────────────────┘
            │
   ┌────────▼─────────┐
   │    Networking     │──► Alamofire
   └──────────────────┘
   ┌──────────────────┐
   │     SharedUI      │
   └──────────────────┘

Nimani o'rgandik?

MavzuTushuntirish
Workspace.swiftMulti-project workspace yaratish
**.project() dependency**Boshqa project'dagi target'ga bog'lanish
Binary Cachetuist cache bilan tezlashtirish
CI/CDGitHub Actions bilan avtomatik build/test
Multi-projectCore + Feature + App ajratilgan

💡 Keyingi loyiha: Real Loyiha #5: Migration Case Study — Mavjud Xcode loyihasini Tuist'ga to'liq ko'chirish.

"Multi-project workspace — katta jamoalar uchun eng yaxshi tuzilma!" 🏢

Markdown manba

iOS Lessons