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.swiftbilan 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?
| Mavzu | Tushuntirish |
|---|---|
| Workspace.swift | Multi-project workspace yaratish |
**.project() dependency** | Boshqa project'dagi target'ga bog'lanish |
| Binary Cache | tuist cache bilan tezlashtirish |
| CI/CD | GitHub Actions bilan avtomatik build/test |
| Multi-project | Core + 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!" 🏢