Azimjon Abdurasulov

06. Modular Architecture (TMA)

🚀 Tuist — Mukammal Qo‘llanma & Amaliyot

Tuist 06: Modular arxitektura (TMA) — µFeature'lar, Interface/Implementation, Testing va Example ilovalar

Holat: 2026-yil oktabr. Bu dars Tuist tavsiya qiladigan The Modular Architecture (TMA) ni noldan quradi: µFeature anatomiyasi, Interface/Implementation ajratish, Testing va Example target'lar, dependency injection va "Interface o'zgarmasa, dependent qayta qurilmaydi" degan asosiy da'voni real o'lchov bilan tekshiradi. Kod misollari Tuist 4.211.1 va Xcode 27.1 bilan Apple M4 Pro'da tekshirilgan.

Bu darsdan keyin siz: bitta feature'ni 5 ta target (Interface, Source, Testing, Tests, Example) ga ajratib, ProjectDescriptionHelpers factory bilan avtomatlashtira olasiz va modullar orasidagi bog'lanishni Interface orqali boshqarib, build vaqtini kamaytirasiz.

  • Monolit (bitta target) loyihaning aniq og'riqlarini va modullarga bo'lish ularni qanday yechishini tushunasiz.
  • µFeature / TMA ning 5 target anatomiyasini va har birining vazifasini bilasiz.
  • "Source'ga emas, Interface'ga bog'lan" qoidasini va u build'ni nega tezlatishini mexanizm darajasida tushunasiz.
  • Module.feature(...) factory yozib, 2 ta feature + 1 core modulni bitta Project.swift dan generatsiya qilasiz.
  • Dependency injection'ni composition root'da yig'asiz; statik/dinamik product type'ni Environment orqali almashtirasiz.
  • tuist inspect dependencies bilan Interface chegarasini majburlaysiz va implicit bog'liqliklarni ushlaysiz.

Mundarija

  1. Monolit azobi: nega modullarga bo'lamiz?
  2. TMA nima va "µFeature" qayerdan keldi?
  3. Modul anatomiyasi: 5 ta target
  4. Asosiy qoida: Interface'ga bog'lan, Source'ga emas
  5. Loyihani quramiz: ProjectDescriptionHelpers factory
  6. Modullar kodi: Interface, Source, Testing, Tests, Example
  7. Dependency injection: implementatsiyani bir joyda yig'ish
  8. Statik va dinamik: product type va dynamic configuration
  9. O'lchov: Interface o'zgarmasa, dependent qayta qurilmaydi
  10. Chegarani majburlash: tuist inspect dependencies
  11. Example app bilan tez iteratsiya
  12. Katta miqyosda: Monzo, Toss, monday.com
  13. Kamchiliklar va qachon TMA kerak emas
  14. Amaliy mashqlar
  15. O'zingizni tekshiring
  16. Xulosa
  17. Manbalar

1. Monolit azobi: nega modullarga bo'lamiz? {#nega-modul}

Avval muammoni his qilaylik. Tasavvur qiling: butun ilovangiz — barcha view, view model, service, model — bitta Xcode target'da. Loyiha 10 ming satrgacha hammasi joyida. Lekin kod 100 mingga, keyin 300 mingga o'sadi. Endi:

  • Bitta kichik o'zgarish kiritasiz — Xcode butun target'ni qayta kompilyatsiya qiladi, chunki hamma narsa bitta modulda. Bir qatorlik tuzatish uchun 45 sekund kutasiz.
  • SwiftUI Preview'lar sekinlashadi, ba'zan umuman ishlamaydi — Preview butun target'ni build qilishi kerak.
  • Kod o'zaro chigallashadi: har kim hamma narsani importsiz ko'radi, implicit bog'liqlik ("spaghetti") paydo bo'ladi, holat (state) oldindan aytib bo'lmas bo'lib qoladi.
  • Yangi dasturchi loyihaga kirsa, qayerdan boshlashni bilmaydi — chegara yo'q.

Bu faraz emas. Asana jamoasi aynan shu holatga tushgan: ~10 muhandis ~300 ming satr Swift kodni deyarli bitta targetda yuritardi. Ularning muhandislik blogiga ko'ra (2023-yil 21-fevral), o'zgarish kiritib simulyatorda ko'rishgacha ketadigan vaqt odatda ~45 sekund edi (eski Intel Mac'larda ~90 s). Jamoa loyihani Tuist bilan µFeature'larga bo'lgandan keyin, Scalyr'da yozib olingan median build vaqti 15 sekundga tushdi, Inbox modulidagi bir qatorlik o'zgarish esa 30–50 s dan ~4 sekundga qisqardi.

🚀 Motivatsiya: Modullarga bo'lishning asosiy yutug'i — selektiv kompilyatsiya. Siz o'zgartirgan feature'ni va faqat unga kerakli qismlarnigina kompilyatsiya qilasiz, butun ilovani emas. Butun ilova faqat eng oxirida, integratsiya paytida build bo'ladi. Bir qatorlik tuzatishni 45 s o'rniga 4 s'da ko'rish — bu kuniga o'nlab marta takrorlanadigan tsikl.

Diqqat: Asana ogohlantiradi — **bitta katta AsanaCore modulini ajratish o'zi hech qanday vaqt tejamadi**; yutuq feature'larni mustaqil modullarga bo'lishdan keldi. Ya'ni "modularizatsiya" degani "bir nechta katta bo'lak" emas, balki mustaqil, kichik feature modullari degani. Buni qanday to'g'ri qilishni TMA o'rgatadi.

⚠️ Diqqat: Modularizatsiya tekin emas. Asana sanab o'tgan narxlar: git checkout 5–15 sekund sekinlashdi (chunki checkout'da loyiha qayta generatsiya qilinadi), minglab simvolni skript bilan public qilishga to'g'ri keldi, va migratsiya ~3 oy davom etdi. Bu darsning oxirida bu kamchiliklarga qaytamiz.


2. TMA nima va "µFeature" qayerdan keldi? {#tma-nima}

TMA (The Modular Architecture) — Tuist jamoasi tavsiya qiladigan, ilovani mustaqil featurelardan qurish usuli. Bu feature'lar bir-biri bilan aniq va qisqa APIlar orqali bog'lanadi. TMA'ning rasmiy hujjatidagi ta'rif:

"Its core idea is to build your apps by building independent features that are interconnected using clear and concise APIs."

Avval bu arxitektura µFeatures (micro features) deb atalardi. Tuist hujjatida aytilishicha, nomi The Modular Architecture (TMA) ga o'zgartirildi — chunki yangi nom uning maqsadi va printsiplarini yaxshiroq aks ettiradi. Shuning uchun eski maqolalarda "µFeatures" so'zini uchratsangiz, bu aynan TMA — bir xil narsa.

Asosiy printsip {#asosiy-printsip}

TMA'ning yuragi bitta jumlada (hujjatdan):

Dasturchilar o'z feature'larini tez build qila, test qila va sinab ko'ra olishi kerak — asosiy ilovadan mustaqil ravishda — shu bilan birga Xcode'ning UI preview, code completion va debugging kabi imkoniyatlari ishonchli ishlashi kerak.

Bu jumlada 3 ta fe'l bor: build, test, try (sinab ko'rish). Va bitta shart: Xcode'ning qulayliklari buzilmasligi kerak. TMA'ning har bir qarori shu maqsadga xizmat qiladi. Keling, strukturani ko'ramiz.

🏢 Real-world: TMA faqat kichik jamoalar uchun emas. Toss (2026-yil case study) ~1000 modulni yuritadi va ularning aksariyati aynan Interface / Implementation / Model / Testing target'larga bo'lingan. Ya'ni bu dars o'rgatadigan ajratish — eng katta iOS loyihalarda ham ishlaydigan naqsh. (Manba: Tuist case study, "Toss: Building less to ship faster".)


3. Modul anatomiyasi: 5 ta target {#anatomiya}

TMA'da bitta feature = bitta modul, va u (Xcode ma'nosidagi) 5 ta targetdan iborat bo'ladi. Rasmiy hujjatdagi jadval:

TargetBog'liqligiMazmuni
**Feature** (Source)FeatureInterfaceFeature'ning asosiy kodi va resurslari (view, view model, service, rasm, font)
**FeatureInterface**—Public interface va modellar (protocol'lar, struct'lar)
**FeatureTests**Feature, FeatureTestingUnit va integration testlar
**FeatureTesting**FeatureInterfaceTest ma'lumotlari va mock'lar — boshqa feature'lar ham ishlatadi
**FeatureExample**FeatureTesting, FeatureExample (demo) ilova

µFeature modulining 5 ta target'i va ularning ichki bog'liqliklari: Interface — poydevor, Source va Testing unga, Tests va Example esa Source va Testing'ga tayanadi

Diagrammada ko'rinib turibdi: Interface — poydevor, u hech kimga bog'lanmaydi. Source (implementatsiya) va Testing (mock'lar) Interface'ga tayanadi. Tests va Example esa Source va Testing'dan foydalanadi. Endi har birini alohida ko'rib chiqamiz.

Interface — public yuza {#interface-target}

Bu target faqat public protocol va modellarni saqlaydi — feature'ning "shartnomasi" (contract). Boshqa modullar aynan shu target bilan gaplashadi, implementatsiyani umuman ko'rmaydi. Masalan, CatalogInterfaceda CatalogServing protokoli va Product modeli bo'ladi, lekin ularni qanday amalga oshirish — Catalog (Source) target'ida, yashirin.

Source — implementatsiya {#source-target}

Feature'ning haqiqiy kodi: view, view model, service, resurslar. U o'z Interface'ini implement qiladi va boshqa modullarning Interfacelariga bog'lanadi (Source'lariga emas — bu keyingi bo'limning mavzusi).

Testing — qayta ishlatiladigan mock'lar {#testing-target}

Bu — TMA'ning eng ko'p e'tibordan chetda qoladigan, lekin eng foydali qismi. FeatureTesting target'i mock'lar va test ma'lumotlarini saqlaydi. Muhimi: bu alohida target bo'lgani uchun, uni boshqa modullar ham import qila oladi. Masalan, Home feature'ining testi Catalogning haqiqiy implementatsiyasini emas, CatalogTestingdagi tayyor mock'ni ishlatadi. Shu dars davomida buni real kodda ko'rasiz.

💡 Pro tip: Hujjat yana bitta foydadan eslatadi — Feature target'i FeatureTestingni Development Asset sifatida ulasa, SwiftUI Preview'lar mock ma'lumot bilan ishlaydi. Ya'ni Preview'da real server kerak emas, mock'dan ko'rsatadi.

Tests — unit/integration testlar {#tests-target}

Oddiy test target. @testable import Feature bilan implementatsiyani ichidan ko'radi, mock'ni FeatureTestingdan oladi.

Example — mustaqil mini-ilova {#example-target}

FeatureExample — kichik, mustaqil .app. U faqat shu feature'ni ishga tushiradi, mock ma'lumot bilan. Dasturchi butun ilovani build qilmasdan, faqat shu feature'ni simulyatorda ochib ko'radi. Turli til, ekran o'lchami, holatlarni shu yerda sinaydi. Bu TMA'ning "try" (sinab ko'rish) qismini amalga oshiradi.

⚠️ Diqqat (hujjatdan): 5 ta target o'rniga, Testing kodini compiler direktivalari (#if DEBUG) bilan Feature yoki FeatureInterface ichiga qo'yish mumkin — grafik soddalashadi. Lekin unda ilovaga kerak bo'lmagan test kodini ham kompilyatsiya qilasiz. Tuist buni tavsiya qilmaydi: mock'lar alohida target'da tursa, release build'ga tushmaydi.


4. Asosiy qoida: Interface'ga bog'lan, Source'ga emas {#qoida}

TMA'ning butun kuchi bitta qoidada: modul boshqa modulga bog'lansa, uning Source'iga emas, Interface'iga bog'lanadi.

❌ NOTO'G'RI:  Home  →  Catalog         (Source'ga — implementatsiyaga)
✅ TO'G'RI:    Home  →  CatalogInterface (Interface'ga — shartnomaga)

Nega bu shunchalik muhim? Ikkita sabab bor, va ikkalasi ham o'lchash mumkin.

1-sabab: coupling (bog'lanish) kamayadi. Agar Home to'g'ridan-to'g'ri Catalogning implementatsiyasiga bog'lansa, u Catalogning ichki detallarini biladi — bu kuchli bog'lanish. CatalogInterfacega bog'lansa, Home faqat protocol'ni (shartnomani) biladi, ichkarida nima bo'layotgani uni qiziqtirmaydi. Bu — Dependency Inversion printsipi.

2-sabab: clean build tezlashadi. Hujjatdan to'g'ridan-to'g'ri:

"It speeds up clean builds because they only have to compile the implementation of our feature, and the interfaces of direct and transitive dependencies."

Ya'ni Homeni build qilish uchun kompilyator Homening implementatsiyasini + bog'liq modullarning faqat Interfacelarini kompilyatsiya qiladi, ularning og'ir implementatsiyalarini emas. Interface target odatda bir necha protocol va struct — juda tez compile bo'ladi.

Bu g'oya yangi emas. Tuist hujjati uni SwiftRockning "Reducing iOS Build Times by using Interface Modules" maqolasiga bog'laydi — interface modullari orqali iOS build vaqtini kamaytirish g'oyasi aynan shu yerdan.

App qatlami Feature'larni, Feature'lar bir-birining Interface'ini, hammasi Core Interface'ini ishlatadigan qatlamli arxitektura

Diagrammada eng muhimi — sariq strelka: Home (Source) CatalogInterfacega boradi, Catalog (Source) ga emas. App esa boshqacha — u composition root bo'lgani uchun barcha Source (implementatsiya) modullarni yig'adi.

To'liq bog'lanish qoidalari jadvali {#qoida-jadval}

Bizning loyihamizdagi (quyida quramiz) real bog'lanishlar:

KimdanKimgaNega
Home (Source) →CatalogInterfaceCatalog'ni faqat shartnoma orqali biladi
Home (Source) →DesignSystemInterfaceTheme'ni protocol orqali oladi
Home (Source) →HomeInterfaceo'z yuzasini (HomeScreenFactory) implement qiladi
HomeTesting →HomeInterfacemock protocol'ni implement qiladi
HomeTests →Home, CatalogInterface, CatalogTestingimplementatsiyani test qiladi, Catalog mock'ini ishlatadi
TmaDemo (App) →barcha Source + Interfacecomposition root — implementatsiyalarni yig'adi

🧠 Esda tuting: Faqat App (yoki boshqa "entry point" — App Clip, extension) implementatsiya modullariga bog'lanadi, chunki kimdir oxir-oqibat CatalogService() ni yaratishi kerak. Oddiy feature'lar esa doim Interface orqali gaplashadi. Shuning uchun App'ni "composition root" (yig'ish nuqtasi) deymiz.


5. Loyihani quramiz: ProjectDescriptionHelpers factory {#factory}

Har bir feature uchun 5 ta target'ni qo'lda yozish — zerikarli va xatoga moyil. Agar 10 ta feature bo'lsa, 50 ta target yozasiz, har birida bir xil destinations, bundleId naqshi, sources yo'li. Buni DRY qilib, bitta funksiyaga aylantiramiz.

Tuist buning uchun project description helpers beradi: Tuist/ProjectDescriptionHelpers/ papkasidagi barcha .swift fayllar bitta ProjectDescriptionHelpers moduliga kompilyatsiya qilinadi, va uni manifestlarda import qilish mumkin.

💡 Pro tip (hujjatdan): Ko'p tashkilotlar Tuist'ni aynan shuning uchun yaxshi ko'radi — project description helpers platform jamoasiga o'z konvensiyalarini Swiftda kodlashtirish imkonini beradi. YAML asosidagi generatorlarda buning uchun alohida shablon tili o'ylab topishga to'g'ri keladi; bu yerda — oddiy Swift funksiyasi.

Mana bizning Module.feature(...) factory'si. U feature nomini oladi va 5 ta Target qaytaradi:

// Tuist/ProjectDescriptionHelpers/Module.swift
import ProjectDescription

// Bitta "feature" = 5 ta target: Interface, Source, Testing, Tests, Example.
// Bu factory har bir feature uchun shu 5 talikni bir xil qoida bilan yasaydi,
// shunda Project.swift qisqa qoladi va konvensiya butun loyihada bir xil bo'ladi.
public enum Module {
    public static func feature(
        name: String,
        // Source target boshqa modullarning Interface'lariga bog'lanadi.
        dependencies: [TargetDependency] = [],
        // Interface'ning o'zi ba'zan boshqa Interface'ga bog'lanadi (umumiy modellar).
        interfaceDependencies: [TargetDependency] = [],
        // Tests'ga qo'shimcha (masalan boshqa modulning Testing mock'lari).
        testDependencies: [TargetDependency] = [],
        // Example app'ga konkret implementatsiyalar kerak bo'ladi.
        exampleDependencies: [TargetDependency] = [],
        hasExample: Bool = true
    ) -> [Target] {
        var targets: [Target] = []

        // 1. Interface — faqat public protocol va model. Hech kimga bog'lanmaydi (yoki boshqa Interface'ga).
        targets.append(.target(
            name: "\(name)Interface",
            destinations: .iOS,
            product: .framework,
            bundleId: "uz.azimjondev.tma.\(name)Interface",
            deploymentTargets: .iOS("17.0"),
            sources: ["Modules/\(name)/Interface/**"],
            dependencies: interfaceDependencies
        ))

        // 2. Source — implementatsiya. Faqat o'z Interface'iga + boshqa modul Interface'lariga.
        targets.append(.target(
            name: name,
            destinations: .iOS,
            product: .framework,
            bundleId: "uz.azimjondev.tma.\(name)",
            deploymentTargets: .iOS("17.0"),
            sources: ["Modules/\(name)/Sources/**"],
            dependencies: [.target(name: "\(name)Interface")] + dependencies
        ))

        // 3. Testing — mock'lar va test ma'lumotlari. Boshqa modullar ham import qila oladi.
        targets.append(.target(
            name: "\(name)Testing",
            destinations: .iOS,
            product: .framework,
            bundleId: "uz.azimjondev.tma.\(name)Testing",
            deploymentTargets: .iOS("17.0"),
            sources: ["Modules/\(name)/Testing/**"],
            dependencies: [.target(name: "\(name)Interface")]
        ))

        // 4. Tests — unit testlar. @testable orqali modulning o'zi ko'rinadi.
        // Interface yoki mock kerak bo'lsa, test aynan nimani import qilsa, shuni
        // testDependencies'da aniq sanab beramiz.
        targets.append(.target(
            name: "\(name)Tests",
            destinations: .iOS,
            product: .unitTests,
            bundleId: "uz.azimjondev.tma.\(name)Tests",
            deploymentTargets: .iOS("17.0"),
            sources: ["Modules/\(name)/Tests/**"],
            dependencies: [.target(name: name)] + testDependencies
        ))

        // 5. Example — feature'ni butun ilovasiz, mock bilan alohida run qilish uchun kichik app.
        if hasExample {
            targets.append(.target(
                name: "\(name)Example",
                destinations: .iOS,
                product: .app,
                bundleId: "uz.azimjondev.tma.\(name)Example",
                deploymentTargets: .iOS("17.0"),
                infoPlist: .extendingDefault(with: ["UILaunchScreen": [:]]),
                sources: ["Modules/\(name)/Example/**"],
                dependencies: [.target(name: name)] + exampleDependencies
            ))
        }

        return targets
    }
}

🧠 Esda tuting: Factory har bir target uchun product ni aniq belgilaydi: Interface/Source/Testing — .framework, Tests — .unitTests, Example — .app. Bu Product enum'ining real case'lari (Tuist ProjectDescription/Product.swift manbasidan): app, framework, staticFramework, staticLibrary, dynamicLibrary, unitTests, uiTests, bundle, macro, appExtension, appClip va boshqalar.

Endi Project.swift qisqa va o'qiladigan bo'ladi — har bir feature bitta chaqiruv:

// Project.swift
import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "TmaDemo",
    targets:
        // DesignSystem — core modul, example shart emas.
        Module.feature(
            name: "DesignSystem",
            testDependencies: [
                .target(name: "DesignSystemInterface"),
                .target(name: "DesignSystemTesting"),
            ],
            hasExample: false
        )
        +
        // Catalog — DesignSystem'ning faqat Interface'iga bog'lanadi.
        Module.feature(
            name: "Catalog",
            dependencies: [.target(name: "DesignSystemInterface")],
            testDependencies: [
                .target(name: "CatalogInterface"),
                .target(name: "CatalogTesting"),
            ],
            exampleDependencies: [
                .target(name: "CatalogTesting"),
                .target(name: "DesignSystem"),
            ]
        )
        +
        // Home — Catalog'ning faqat Interface'iga bog'lanadi (muhim: Source'iga EMAS).
        Module.feature(
            name: "Home",
            dependencies: [
                .target(name: "CatalogInterface"),
                .target(name: "DesignSystemInterface"),
            ],
            testDependencies: [
                .target(name: "CatalogInterface"),
                .target(name: "CatalogTesting"),
            ],
            exampleDependencies: [
                .target(name: "Catalog"),
                .target(name: "DesignSystem"),
            ]
        )
        +
        // App — Source modullarni yig'adi, Home'ni esa Interface orqali oladi (DI).
        [
            .target(
                name: "TmaDemo",
                destinations: .iOS,
                product: .app,
                bundleId: "uz.azimjondev.tma.app",
                deploymentTargets: .iOS("17.0"),
                infoPlist: .extendingDefault(with: ["UILaunchScreen": [:]]),
                sources: ["App/Sources/**"],
                dependencies: [
                    .target(name: "Home"),
                    .target(name: "HomeInterface"),
                    .target(name: "Catalog"),
                    .target(name: "CatalogInterface"),
                    .target(name: "DesignSystem"),
                    .target(name: "DesignSystemInterface"),
                ]
            ),
        ]
)

E'tibor bering: Module.feature(...) [Target] qaytaradi, shuning uchun ularni + bilan qo'shamiz va App target'ini oddiy massiv sifatida ulaymiz.

Papka tuzilmasi {#papka}

Factory sources: ["Modules/\(name)/..."] deb kutganidek, papkalarni shunday tashkil qilamiz:

# Har bir feature uchun 5 ta kichik papka
for m in DesignSystem Catalog Home; do
  mkdir -p Modules/$m/{Interface,Sources,Testing,Tests,Example}
done
mkdir -p App/Sources

Natijada loyiha shunday ko'rinadi:

TmaDemo/
├── Tuist.swift
├── Tuist/
│   ├── Package.swift
│   └── ProjectDescriptionHelpers/
│       └── Module.swift          ← factory shu yerda
├── Project.swift
├── App/
│   └── Sources/
│       ├── TmaDemoApp.swift
│       └── AppDependencies.swift  ← composition root
└── Modules/
    ├── DesignSystem/
    │   ├── Interface/   Sources/   Testing/   Tests/
    ├── Catalog/
    │   ├── Interface/   Sources/   Testing/   Tests/   Example/
    └── Home/
        ├── Interface/   Sources/   Testing/   Tests/   Example/

Minimal Tuist.swift (2026 holati — eski Config.swift o'rniga):

// Tuist.swift
import ProjectDescription

let tuist = Tuist(project: .tuist())

Generatsiya qilamiz:

tuist generate --no-open

Natija (real, bizning mashinamizda):

Loading and constructing the graph
Generating workspace TmaDemo.xcworkspace
Generating project TmaDemo
Total time taken: 1.751s
✔ Success
  Project generated.

1.7 sekundda .xcworkspace, .xcodeproj va Derived/ papkasi tayyor. Generatsiya qilingan fayllarni git'ga qo'ymaymiz (.gitignorega *.xcodeproj, *.xcworkspace, Derived/, .build/). tuist generate, tuist graph, tuist edit va boshqa buyruqlar haqida to'liq ma'lumotni 03. Papka tuzilmasi va CLI darsida ko'rishingiz mumkin.


6. Modullar kodi: Interface, Source, Testing, Tests, Example {#modul-kodi}

Endi real kod. Uchta modulimiz bor: DesignSystem (core), Catalog va Home (feature'lar). To'liq zanjirni Catalogda ko'rsatamiz.

CatalogInterface — shartnoma {#catalog-interface}

// Modules/Catalog/Interface/CatalogInterface.swift
import Foundation

// Catalog'ning public modeli va protokoli. Home faqat shu faylga bog'lanadi.
public struct Product: Identifiable, Equatable, Sendable {
    public let id: String
    public let name: String
    public let price: Int   // so'mda butun son

    public init(id: String, name: String, price: Int) {
        self.id = id
        self.name = name
        self.price = price
    }
}

public protocol CatalogServing: Sendable {
    func featured() -> [Product]
}

🧠 Esda tuting: Interface'dagi hamma narsa public bo'lishi SHART — boshqa modullar ko'rishi kerak. Va modellarning initi ham public (Swift'da framework'ning public struct'i avtomatik public memberwise init olmaydi). Buni unutsangiz, boshqa modulda Product(...) yaratib bo'lmaydi degan xato olasiz.

Catalog (Source) — implementatsiya {#catalog-source}

// Modules/Catalog/Sources/CatalogService.swift
import CatalogInterface
import Foundation

// Implementatsiya. Bu faylni o'zgartirsak CatalogInterface o'zgarmaydi.
public struct CatalogService: CatalogServing {
    public init() {}

    public func featured() -> [Product] {
        [
            Product(id: "1", name: "Dasturlash asoslari", price: 120_000),
            Product(id: "2", name: "Swift tili", price: 150_000),
            Product(id: "3", name: "iOS arxitektura", price: 180_000),
        ]
    }
}
// Modules/Catalog/Sources/CatalogView.swift
import CatalogInterface
import DesignSystemInterface
import SwiftUI

public struct CatalogView: View {
    private let service: CatalogServing
    private let theme: ThemeProviding

    public init(service: CatalogServing, theme: ThemeProviding) {
        self.service = service
        self.theme = theme
    }

    public var body: some View {
        List(service.featured()) { product in
            HStack {
                Text(product.name)
                Spacer()
                Text("\(product.price) so'm")
                    .foregroundStyle(theme.accentColor)
            }
        }
    }
}

CatalogView DesignSystemInterfacedan ThemeProvidingni oladi — yana Interface orqali. DesignSystemning haqiqiy DefaultThemeini u ko'rmaydi.

CatalogTesting — qayta ishlatiladigan mock {#catalog-testing}

// Modules/Catalog/Testing/StubCatalogService.swift
import CatalogInterface

// Oldindan tayyor mock — Home testlari ham, Example app ham shuni ishlatadi.
public struct StubCatalogService: CatalogServing {
    private let products: [Product]
    public init(products: [Product] = [
        Product(id: "t1", name: "Test kitob", price: 1000),
        Product(id: "t2", name: "Mock kitob", price: 2000),
    ]) {
        self.products = products
    }
    public func featured() -> [Product] { products }
}

CatalogTests {#catalog-tests}

Bu yerda Swift Testing (yangi @Test / #expect sintaksisi, Xcode 27'da standart) ishlatamiz:

// Modules/Catalog/Tests/CatalogTests.swift
import Testing
@testable import Catalog
import CatalogInterface
import CatalogTesting

struct CatalogTests {
    @Test func serviceReturnsThreeFeaturedProducts() {
        let service = CatalogService()
        #expect(service.featured().count == 3)
    }

    @Test func stubIsConfigurable() {
        let stub = StubCatalogService(products: [Product(id: "x", name: "Bitta", price: 5)])
        #expect(stub.featured().count == 1)
        #expect(stub.featured().first?.name == "Bitta")
    }
}

Home — boshqa modulni faqat Interface orqali ishlatish {#home-source}

Mana TMA qoidasining jonli namunasi. HomeViewModel Catalogning implementatsiyasini emas, CatalogServing protokolini oladi:

// Modules/Home/Sources/HomeViewModel.swift
import CatalogInterface
import Foundation
import Observation

// Home Catalog'ning IMPLEMENTATSIYASINI emas, faqat CatalogServing protokolini biladi.
@Observable
public final class HomeViewModel {
    public private(set) var featuredCount: Int = 0
    private let catalog: CatalogServing

    public init(catalog: CatalogServing) {
        self.catalog = catalog
    }

    public func load() {
        featuredCount = catalog.featured().count
    }
}

HomeView ham xuddi shunday — faqat protokollarni (CatalogServing, ThemeProviding) oladi:

// Modules/Home/Sources/HomeView.swift
import CatalogInterface
import DesignSystemInterface
import SwiftUI

public struct HomeView: View {
    @State private var viewModel: HomeViewModel
    private let theme: ThemeProviding

    public init(catalog: CatalogServing, theme: ThemeProviding) {
        _viewModel = State(initialValue: HomeViewModel(catalog: catalog))
        self.theme = theme
    }

    public var body: some View {
        VStack(spacing: Spacing.medium) {
            theme.title("Bosh sahifa")
            Text("Tanlangan mahsulotlar: \(viewModel.featuredCount)")
        }
        .onAppear { viewModel.load() }
    }
}

Homening o'z yuzasi (HomeInterface) HomeScreenFactory protokolini beradi, va Home (Source) uni implement qiladi:

// Modules/Home/Interface/HomeInterface.swift
import SwiftUI

public protocol HomeScreenFactory: Sendable {
    func makeHome() -> AnyView
}
// Modules/Home/Sources/HomeComponent.swift
import CatalogInterface
import DesignSystemInterface
import HomeInterface
import SwiftUI

// App Home'ni faqat HomeInterface orqali biladi, implementatsiyani shu komponent beradi.
public struct HomeComponent: HomeScreenFactory {
    private let catalog: CatalogServing
    private let theme: ThemeProviding

    public init(catalog: CatalogServing, theme: ThemeProviding) {
        self.catalog = catalog
        self.theme = theme
    }

    public func makeHome() -> AnyView {
        AnyView(HomeView(catalog: catalog, theme: theme))
    }
}

HomeTests — boshqa modulning mock'ini qayta ishlatish {#home-tests}

Bu — Testing target'ining asl foydasi. HomeViewModel CatalogServingga bog'liq, shuning uchun uning testi Catalogning mock'ini (CatalogTestingdagi StubCatalogService) ishlatadi — Catalogning haqiqiy implementatsiyasi yoki server kerak emas:

// Modules/Home/Tests/HomeViewModelTests.swift
import Testing
@testable import Home
import CatalogInterface
import CatalogTesting

struct HomeViewModelTests {
    @Test func loadCountsFeaturedFromCatalog() {
        // Boshqa modulning Testing mock'ini qayta ishlatamiz — haqiqiy Catalog kerak emas.
        let vm = HomeViewModel(catalog: StubCatalogService())
        vm.load()
        #expect(vm.featuredCount == 2)
    }

    @Test func emptyCatalogGivesZero() {
        let vm = HomeViewModel(catalog: StubCatalogService(products: []))
        vm.load()
        #expect(vm.featuredCount == 0)
    }
}

Testlarni ishga tushiramiz (real natija, iPhone 17 / iOS 27.0 simulyatorda):

tuist test Home --device "iPhone 17" --os 27.0
Test session results:
Test Succeeded
✔ Success
  The project tests ran successfully

💡 Pro tip: tuist test Home — bu yerda Home bu scheme nomi, test target emas. Tuist har bir modul uchun avtomatik scheme yaratadi va test target'ni shu scheme'ga biriktiradi. tuist test HomeTests deb yozsangiz, "Couldn't find scheme HomeTests" xatosini olasiz — mavjud scheme'lar tuist test chiqarib beradi.


7. Dependency injection: implementatsiyani bir joyda yig'ish {#di}

Modullar bir-birini faqat Interface orqali biladi. Lekin ilova ishga tushganda kimdir haqiqiy CatalogService() ni yaratishi va Homega berishi kerak. Bu "implementatsiya grafigini qurish" App'da, bitta joyda — composition rootda bo'ladi.

TMA hujjati bu haqda ataylab fikr bildirmaydi ("non-opinionated"), lekin tavsiya beradi:

"We recommend using dependency-injection solutions or patterns that don't add built-time indirections or use platform APIs that were not designed for this purpose."

Ya'ni og'ir DI framework'lar (build vaqtida kod generatsiya qiladigan) yoki platformani noto'g'ri ishlatadigan "hiyla"lar shart emas. Bizda eng oddiy, qo'lda DI yetarli:

// App/Sources/AppDependencies.swift
import Catalog
import CatalogInterface
import DesignSystem
import DesignSystemInterface
import Home
import HomeInterface

// Implementatsiya grafigi bitta joyda yig'iladi — "composition root".
// Modullar bir-birini Interface orqali biladi; konkret tiplar faqat shu yerda uchrashadi.
struct AppDependencies {
    let theme: ThemeProviding = DefaultTheme()
    let catalog: CatalogServing = CatalogService()
    var home: HomeScreenFactory { HomeComponent(catalog: catalog, theme: theme) }
}

E'tibor bering: AppDependencies bu — loyihada yagona joy bo'lib, u ham Catalog (implementatsiya), ham CatalogInterface (shartnoma)ni import qiladi. Konkret tiplar (DefaultTheme, CatalogService, HomeComponent) faqat shu yerda nomlanadi. Boshqa barcha modullar faqat protokollarni biladi.

// App/Sources/TmaDemoApp.swift
import Catalog
import SwiftUI

@main
struct TmaDemoApp: App {
    private let deps = AppDependencies()

    var body: some Scene {
        WindowGroup {
            TabView {
                // Home faqat Interface orqali keladi (HomeScreenFactory.makeHome()).
                deps.home.makeHome()
                    .tabItem { Label("Bosh", systemImage: "house") }

                CatalogView(service: deps.catalog, theme: deps.theme)
                    .tabItem { Label("Katalog", systemImage: "books.vertical") }
            }
        }
    }
}

deps.home.makeHome() — App Homening ichki HomeViewini bilmaydi ham; u faqat HomeScreenFactory protokolini chaqiradi. Agar ertaga Homening butun ichki tuzilishini qayta yozsangiz, App'dagi bu qator o'zgarmaydi — chunki shartnoma o'sha-o'sha.

🧠 Esda tuting: Bu DI shaklida yangi dasturchi uchun katta afzallik bor: "Bu CatalogServing qayerdan kelyapti?" degan savolga javob doim bitta joyda — AppDependenciesda. Implementatsiyani almashtirish (masalan, test uchun StubCatalogServicega) ham shu yerda, bitta qatorda.


8. Statik va dinamik: product type va dynamic configuration {#product-type}

Factory'da har bir modulni .framework (dinamik) qilib belgiladik. Nega? Chunki TMA development uchun dinamik, release uchun statikni tavsiya qiladi. Hujjatdan:

"We recommend using dynamic libraries or frameworks during development ... This is key for fast compilation times and to ensure SwiftUI Previews work reliably. And static libraries or frameworks for the release builds to ensure the app boots fast."

Qisqa qilib:

Dinamik (.framework)Statik (.staticFramework)
Incremental buildtezsekinroq
SwiftUI Previewishonchli ishlaydimuammoli bo'lishi mumkin
Ilova ishga tushishi (launch)sekinroq (dylib yuklanadi)tez (bitta binary)
Ishlatish vaqtidevelopmentrelease

Buni har safar qo'lda o'zgartirmaslik uchun Tuist dynamic configuration beradi: generatsiya vaqtida environment o'zgaruvchisini o'qiysiz. Hujjatdagi naqsh:

// Tuist/ProjectDescriptionHelpers ichida
func productType() -> Product {
    if case let .string(productType) = Environment.productType {
        return productType == "static-library" ? .staticLibrary : .framework
    } else {
        return .framework   // default: dinamik
    }
}
# Release uchun statik qilib generatsiya:
TUIST_PRODUCT_TYPE=static-library tuist generate
# Development uchun oddiy (dinamik):
tuist generate

Environment.productType — TUIST_PRODUCT_TYPE environment o'zgaruvchisini o'qiydi (Tuist TUIST_ prefiksli o'zgaruvchilarni Environmentga uzatadi). Factory'dagi product: .framework o'rniga product: productType() yozsangiz, bitta loyiha ikkala rejimda ham ishlaydi.

🏢 Real-world: Bu nazariya emas. Monzo (Tuist case study, 2026-05-26) aynan shunday qiladi: "tests use dynamic frameworks and release uses static". Ya'ni test va development dinamik, App Store build'i statik. Shu bilan ham tez iteratsiya, ham tez launch oladi.

⚠️ Diqqat — mergeable libraries: Apple statik/dinamik almashish muammosini "mergeable libraries" bilan yengillashtirmoqchi bo'lgan. Lekin TMA hujjati buni tavsiya qilmaydi: u build vaqtida noaniqlik (non-determinism) kiritadi, build'ni takrorlanmas va optimallashtirilishi qiyin qiladi. Statik/dinamikni Environment orqali aniq boshqargan afzal.


9. O'lchov: Interface o'zgarmasa, dependent qayta qurilmaydi {#olchov}

Endi TMA'ning asosiy va'dasini o'lchaymiz. Da'vo shunday edi: modul boshqa modulning Interface'iga bog'lansa, o'sha boshqa modulning implementatsiyasi o'zgarganda — bizning modul qayta kompilyatsiya bo'lmaydi.

Bizning grafik: Home → CatalogInterface (Source'ga emas). Tekshiramiz. Avval loyiha grafigini tuist graph bilan tasdiqlaymiz:

tuist graph --format json --skip-test-targets

Natijadan (bog'liqlik qirralari):

Home     ->  [HomeInterface, CatalogInterface, DesignSystemInterface]
Catalog  ->  [CatalogInterface, DesignSystemInterface]
TmaDemo  ->  [DesignSystemInterface, Catalog, DesignSystem, Home, CatalogInterface, HomeInterface]

Home haqiqatan CatalogInterfacega bog'langan, Catalogga emas. Endi 3 ta ssenariy bo'yicha xcodebuildni o'lchaymiz va qaysi target'lar Swift'ni qayta kompilyatsiya qilganini sanab chiqamiz (xcodebuild log'idagi SwiftDriver Compilation <Target> qatorlari orqali).

1-ssenariy — implementatsiya o'zgardi. Catalog/Sources/CatalogService.swift ichidagi bitta mahsulot nomi va narxini o'zgartiramiz (public API tegmaydi):

xcodebuild -workspace TmaDemo.xcworkspace -scheme TmaDemo \
  -destination 'platform=iOS Simulator,name=iPhone 17,OS=27.0' \
  -derivedDataPath .dd build

Natija:

** BUILD SUCCEEDED **
Qayta kompilyatsiya qilingan target'lar: Catalog

Faqat **Catalog**! Home tegilmadi, TmaDemo (App) ning Swift'i ham tegilmadi. Chunki CatalogInterface.swiftmodule o'zgarmagan — Home faqat shunga qaraydi.

2-ssenariy — Interface o'zgardi. Catalog/Interface/CatalogInterface.swift dagi Productga yangi public maydon qo'shamiz:

public let category: String   // yangi public maydon

Xuddi shu build buyrug'i. Natija:

** BUILD SUCCEEDED **
Qayta kompilyatsiya qilingan target'lar: CatalogInterface, Catalog, Home, TmaDemo

Bu safar to'rtta target — butun zanjir. CatalogInterface o'zgargani uchun, unga bog'liq hamma (Catalog, Home, App) qayta kompilyatsiya bo'ldi.

Ikki ssenariy yonma-yon: implementatsiya o'zgarsa faqat Catalog qayta quriladi (Home tegilmaydi); Interface o'zgarsa butun zanjir qayta quriladi

To'liq o'lchov jadvali (Apple M4 Pro, 12 core, 24 GB; Xcode 27.1; Tuist 4.211.1; iPhone 17 / iOS 27.0 simulyator; -derivedDataPath .dd):

SsenariyWall vaqtQayta kompilyatsiya bo'lgan target'lar
Cold build (DerivedData tozalangan)11.5 shamma 7 ta Swift target
Null build (hech narsa o'zgarmagan)1.8 s— (hech biri)
Catalog implementatsiyasi o'zgardi2.3 s**faqat Catalog**
CatalogInterface o'zgardi3.1 sCatalogInterface, Catalog, Home, TmaDemo

Mexanizm: Xcode nega shunday qaror qiladi? {#mexanizm}

Har bir Swift target build bo'lganda .swiftmodule fayli yaratiladi — bu uning public interfaceining "surati". Target'ning dependent'i (unga bog'liq modul) faqat shu .swiftmoduleni ko'radi. Xcode incremental build'da: agar dependency'ning .swiftmodulei o'zgarmagan bo'lsa, dependent'ni qayta kompilyatsiya qilmaydi.

  • Catalogning implementatsiyasini o'zgartirganda, CatalogInterface.swiftmodule o'zgarmaydi (u alohida target). Home faqat CatalogInterfacega qaragani uchun — tegilmaydi.
  • Catalogning Interfaceini o'zgartirganda, CatalogInterface.swiftmodule o'zgaradi — unga bog'liq hamma qayta kompilyatsiya bo'ladi.

Agar Home to'g'ridan-to'g'ri Catalog (Source) ga bog'langanida edi, Catalogning har bir implementatsiya o'zgarishi Catalog.swiftmoduleni o'zgartirardi (public simvollar o'sha modulda), va Home har safar qayta kompilyatsiya bo'lardi. Interface'ni ajratish aynan shuni oldini oladi.

📊 Real raqamlar: Bizning loyiha kichik (7 target), shuning uchun wall vaqtlar yaqin — vaqt asosan xcodebuildning rejalashtirish va linklashiga ketadi, kompilyatsiyaga emas. Yutuq modul soni oshgani sayin kattalashadi. Asana'da (2023-02-21 blog) µFeature ajratishdan keyin Inbox modulidagi bir qatorlik o'zgarish 30–50 s → ~4 s ga tushgan. Ya'ni bu yerdagi "faqat 1 target qayta quriladi" tamoyili, 300 ming satrli loyihada sekundlarni daqiqalarga aylantiradi.

⚠️ Diqqat — o'lchash metodikasi: Bitta o'lchov ishonchli emas. intrada open-source loyihasi (2026-09-30) build cache'ni tekshirganda to'g'ri metodikani ko'rsatgan: bitta build buyrug'ini tanlang, har safar bitta qatorlik o'zgarish kiriting, har rejimda 10 marta ishga tushiring, birinchisini (warm-up) tashlab yuboring, medianni oling, va mashina yuki (load average) past bo'lganda boshlang. Biz bu yerda target-larning qaysi biri qayta kompilyatsiya bo'lishini (bu deterministik) ko'rsatdik; aniq sekundlar mashinaga va yukka qarab o'zgaradi.


10. Chegarani majburlash: tuist inspect dependencies {#inspect}

TMA qoidasini qo'lda eslab yurish qiyin. Birov Homeda import Catalog (implementatsiya!) deb yozib yuborsa-chi? Loyiha baribir compile bo'lishi mumkin, chunki Catalog grafik ichida transitiv mavjud. Bu "implicit dependency" — e'lon qilinmagan, lekin ishlatilayotgan bog'liqlik. Ular TMA chegarasini sekin-asta buzadi.

🧠 Esda tuting: Eski Tuist.swift dagi enforceExplicitDependencies opsiyasi 4.211.1 da deprecated. Uning o'rniga alohida buyruq bor: tuist inspect dependencies.

Men bu loyihani yozganimda, birinchi urinishda aynan shu buyruq meni ushladi. tuist inspect dependencies --only implicit ishga tushirdim va mana bu xatoni oldim (real natija):

✖ Error
  The following implicit dependencies were found:
   - CatalogTests implicitly depends on: CatalogInterface
   - DesignSystemTests implicitly depends on: DesignSystemInterface
   - HomeTests implicitly depends on: CatalogInterface
   - TmaDemo implicitly depends on: CatalogInterface, DesignSystemInterface

Nima bo'lgan edi? Masalan HomeTests fayli import CatalogInterface qilardi, lekin target'da CatalogInterface bog'liqligi aniq e'lon qilinmagan edi — u CatalogTesting orqali transitiv kelardi. TmaDemo (App) esa AppDependenciesda CatalogInterface va DesignSystemInterfaceni import qilardi, lekin faqat Catalog, DesignSystem (Source) ni e'lon qilgan edi.

Yechim — har bir target import qilgan narsani aniq e'lon qilish. Project.swiftda testDependencies va App'ning dependenciesiga yetishmayotgan Interface'larni qo'shdim (yuqoridagi to'liq Project.swiftda ularni ko'rasiz). Keyin qayta tekshirdim:

tuist inspect dependencies
We did not find any dependency issues in your project (checked: implicit, redundant).

tuist inspect dependencies ikki xil muammoni ushlaydi:

  • --only implicit — e'lon qilinmagan, lekin ishlatilayotgan bog'liqlik (yashirin). Bu majburan tuzatilishi kerak: explicit modules va CAS cache faqat implicit bog'liqliksiz ishlaydi.
  • --only redundant — e'lon qilingan, lekin ishlatilmayotgan bog'liqlik (ortiqcha). Bu tozalik uchun: masalan factory har bir Tests'ga Testingni avtomatik qo'shsa, lekin ba'zi test uni import qilmasa, shu ortiqchalikni ko'rsatadi.

🏢 Real-world: Toss (~1000 modul) binary cache ishlashi uchun avval grafikni "cache qilib bo'ladigan" holatga keltirgan: ishlatilmagan import'larni avtomatik olib tashlagan, shartli kompilyatsiya (#if) flag'larini umumiy modullardan chiqargan, va har bir PR'da cache qilib bo'ladigan modullar ulushi tushsa, ogohlantiradigan tekshiruv qo'shgan. Tuist statik tarzda implicit import'larni aniqlashni tavsiya qiladi — chunki explicit modules va CAS faqat implicit bog'liqliksiz ishlaydi. (Manba: Tuist "Toss" case study + "scale" blog, 2025-09-22.)

Shuning uchun tuist inspect dependenciesni CI'da (har bir PR'da) ishga tushirish — TMA chegarasini uzoq muddat saqlashning amaliy usuli.


11. Example app bilan tez iteratsiya {#example}

TMA'ning "try" (sinab ko'rish) qismi — Example app. Uni butun ilovani build qilmasdan ishga tushirasiz. Bizning CatalogExample:

// Modules/Catalog/Example/CatalogExampleApp.swift
import Catalog
import CatalogTesting
import DesignSystem
import SwiftUI

// Butun ilovasiz, faqat Catalog'ni mock ma'lumot bilan ishga tushiruvchi kichik app.
@main
struct CatalogExampleApp: App {
    var body: some Scene {
        WindowGroup {
            CatalogView(service: StubCatalogService(), theme: DefaultTheme())
        }
    }
}

E'tibor bering: StubCatalogService — CatalogTestingdagi mock. Ya'ni Example server kerak qilmaydi, tayyor ma'lumot bilan ishlaydi.

Tuist har bir modulning target'larini bitta scheme'ga guruhlaydi. Catalog scheme'ining "run" target'i — aynan CatalogExample.app. Uni build qilib ko'ramiz (real o'lchov, DerivedData tozalangan):

xcodebuild -workspace TmaDemo.xcworkspace -scheme Catalog \
  -destination 'platform=iOS Simulator,name=iPhone 17,OS=27.0' \
  -derivedDataPath .dd-ex build
** BUILD SUCCEEDED **  (wall: 6.8 s)
Kompilyatsiya qilingan target'lar:
  Catalog, CatalogInterface, CatalogTesting, DesignSystem, DesignSystemInterface, CatalogExample

Diqqat qiling: Home, HomeInterface, TmaDemo umuman build bo'lmadi. Catalog feature'ini sinab ko'rish uchun Home yoki butun ilova kerak emas. 6.8 s — to'liq ilovaning cold build'i (11.5 s) dan sezilarli kam, va loyiha o'sgan sari bu farq kattalashadi.

🚀 Motivatsiya: Mana TMA'ning kundalik kuchi: siz "Katalog" ekranida ishlayapsiz. Example app'ni ochasiz, mock ma'lumot bilan 6.8 sekundda simulyatorda ko'rasiz, o'zgartirasiz, yana ko'rasiz — "Bosh sahifa", "Profil", "Savat" va qolgan 50 ta modul sizni sekinlatmaydi. Butun ilovani faqat ish tugab, integratsiya paytida yig'asiz.


12. Katta miqyosda: Monzo, Toss, monday.com {#miqyos}

Bizning loyiha 3 modul. TMA'ning haqiqiy kuchi yuzlab modulda ko'rinadi. Bu yerdagi raqamlarning hammasi vendor (Tuist) tomonidan e'lon qilingan yoki mijoz-muallif case study'lar — ularni shunday o'qing, mustaqil benchmark emas.

🏢 Monzo (kompaniya muallifligidagi, Tuist e'lon qilgan case study, 2026-05-26): bank ilovasi ~50 dan ~200 modulgacha o'sgan — 45+ iOS muhandis, 1.9M satr Swift, 20k unit + 8k snapshot test. CI bir paytlar har PR uchun 70 daqiqadan oshgan. Tuist module cache'ni mainda har 30 daqiqada "warm" qilib turish bilan: P50 PR vaqti 52 → 15 daqiqa (−71%), P90 57 → 35. Hujjatga ko'ra: docs-only PR ~3 daqiqa, feature-modul o'zgarishi ~10 daqiqa, umumiy (shared) modul o'zgarishi (~15k test) ~23 daqiqa. Cache hit ~80%, va hech qachon production build uchun ishlatilmaydi; test dinamik framework, release statik. (Manba: Tuist "Monzo" case study.)

🏢 Toss (~1000 modul, 2026-10-01): profiling ko'rsatdiki, Xcode vaqtining katta qismi dependency grafigini hisoblashga ketardi — bu narx o'zgarish hajmiga emas, modul soniga qarab o'sadi. ~3 oyda jamoa "focus" workflow qurdi: dasturchi bitta scheme e'lon qiladi, qolgani oldindan qurilgan XCFramework sifatida (self-hosted Tuist + Kura cache) keladi. 1 daqiqadan tez tugaydigan lokal build'lar ulushi uch barobardan ko'proq oshgan. ~1000 binary'da yangi to'siqlar: framework search path'lar, hash barqarorligi, cache warm narxi. (Manba: Tuist "Toss: Building less to ship faster".)

🏢 monday.com (2025-12-15): ~20 iOS muhandis, ~200 modul; merge CI workflow >20 daqiqadan (2022) ~9 daqiqaga tushgan, "ko'p yaxshilanishlardan", Tuist module cache "muhim rol" o'ynagan. Delivery Hero (~115 modul, Logistics jamoasi rider ilovasi): clean build 2.5 → 0.5 daqiqa, CI build+test 2.5 barobar tez. (Manba: Tuist case study'lar, vendor-published.)

Modul soni bo'yicha "chegara" haqida: hech bir manba Tuist yoki TMA uchun aniq jamoa-hajmi yoki modul-soni chegarasini bermaydi. E'lon qilingan "langar"lar: Delivery Hero ~115, monday.com / Monzo ~200 (20–45+ muhandis), Toss ~1000. Umumiy maslahat (Tuist'ning o'zidan): og'riq o'lchanmaguncha native toolchain'ga yaqin turing.


13. Kamchiliklar va qachon TMA kerak emas {#kamchilik}

TMA bepul emas. Halol kamchiliklar (hammasi sourced):

  • Boilerplate va o'rganish egri chizig'i. Asana (2023-02-21): ~1400 satrli Tuist manifest, "faqat ozchilik" muhandis Tuist ichki ishlashida qulay edi ("3+ Tuist eksperti amalda yetarli"). 5 target × har feature = ko'p fayl; factory yordam beradi, lekin konsepsiya baribir o'rganishni talab qiladi.
  • Checkout sekinlashadi. Asana: git checkout 5–15 sekund sekinlashdi (checkout'da loyiha qayta generatsiya qilinadi).
  • "Modularizatsiya" o'zi yetarli emas. Asana: bitta katta AsanaCore modulini ajratish hech qanday build vaqti tejamadi — yutuq feature'larni mustaqil modullarga bo'lishdan keldi. Noto'g'ri bo'lish (bir nechta katta modul) foyda bermaydi.
  • **Interface ajratish ko'p public talab qiladi.** Boshqa modul ko'radigan har bir tip, funksiya, init public bo'lishi kerak. Asana minglab simvolni skript bilan public qilgan.
  • Compiler-directive muqobili trade-off bilan. 5 target o'rniga Testingni #if DEBUG bilan Feature ichiga qo'ysangiz grafik soddalashadi, lekin ilovaga kerak bo'lmagan test kodini ham kompilyatsiya qilasiz (TMA hujjatidagi ogohlantirish).

Qachon TMA kerak emas (yoki yengilroq shakli yetarli)? Agar ilova kichik bo'lsa (bitta ekran, bir nechta feature, bitta dasturchi), 5 target × har feature ortiqcha murakkablik bo'lishi mumkin. Bunday holda Interface'siz oddiy modullar (shunchaki Feature + FeatureTests) ham yetarli — chegarani keyinroq, og'riq paydo bo'lganda qo'shasiz. TMA strukturasi "hammasi yoki hech narsa" emas: avval modullarga bo'ling, Interface ajratishni build vaqti yoki coupling muammoga aylanganda kiriting. Agar sizda allaqachon Xcode (yoki XcodeGen/SPM) loyihasi bor bo'lsa va uni shu strukturaga ko'chirmoqchi bo'lsangiz, 05. Migratsiya darsidagi bosqichma-bosqich yondashuvdan foydalaning.

⚠️ Diqqat: Yuqoridagi barcha kompaniya raqamlari vendor yoki mijoz muallifligidagi case study'lardan. Bazel, Tuist va oddiy Xcode'ni bir xil kod bazasida solishtiradigan mustaqil, neytral benchmark 2025–2026 da topilmadi. Har bir raqam bitta tool'ni o'sha kompaniyaning avvalgi holati bilan solishtiradi, boshqa tool bilan emas.


14. Amaliy mashqlar {#mashqlar}

  1. Yangi feature qo'shing. Profile nomli 4-modul yarating (Module.feature(name: "Profile", ...)). U DesignSystemInterfacega bog'lansin. ProfileInterfaceda ProfileServing protokoli va User modeli bo'lsin. tuist generate qiling va build qiling.
  2. Interface chegarasini sinang. Home/Sourcesda import Catalog (implementatsiya!) deb yozib ko'ring va HomeViewModelda CatalogService()ni to'g'ridan-to'g'ri yarating. tuist inspect dependencies --only implicit ni ishga tushiring — u nima deydi? Keyin to'g'ri yo'lga (Interface + DI) qaytaring.
  3. O'lchovni takrorlang. Profilega Home faqat Interface orqali bog'lansin. Profile/Sourcesdagi bir faylni o'zgartiring va xcodebuild bilan qaysi target qayta kompilyatsiya bo'lishini tekshiring. Keyin ProfileInterfaceni o'zgartiring va farqni ko'ring. Natijani jadvalga yozing.
  4. Statik/dinamik almashtirish. ⭐ Factory'ga productType() funksiyasini qo'shing (8-bo'lim). tuist generate va TUIST_PRODUCT_TYPE=static-library tuist generate orasidagi farqni tuist graph yoki build sozlamalarida tekshiring.
  5. Example app'ni ishga tushiring. HomeExampleni simulyatorda oching (Xcode'da Home scheme → Run, yoki tuist generate Home bilan fokuslang). U CatalogService + DefaultTheme bilan ishlayotganini ko'ring. Keyin uni StubCatalogService + MockThemega o'zgartiring — server kerak emasligini his qiling.
  6. CI tekshiruvi yozing. ⭐ GitHub Actions (yoki boshqa CI) qadamida tuist inspect dependencies ni qo'shing, shunda har bir PR implicit/redundant bog'liqlik kiritsa, build yiqiladi. Toss aynan shunday "cacheable ulush" tekshiruvini qo'ygan.

15. O'zingizni tekshiring {#checklist}

  • [ ] µFeature / TMA'da bitta modul nechta target'dan iborat va har biri nima qiladi — ayta olaman.
  • [ ] "Source'ga emas, Interface'ga bog'lan" qoidasini va u clean build'ni nega tezlatishini (.swiftmodule mexanizmi) tushunaman.
  • [ ] ProjectDescriptionHelpersda Module.feature(...) factory yozib, undan Project.swiftda foydalana olaman.
  • [ ] FeatureTesting target nega alohida va uni boshqa modul testi qanday qayta ishlatishini bilaman.
  • [ ] Dependency injection'ni composition root (App)da yig'a olaman; nega faqat App implementatsiya modullariga bog'lanishini tushunaman.
  • [ ] Development uchun dinamik, release uchun statik product type — va Environment orqali almashtirishni bilaman.
  • [ ] tuist inspect dependencies --only implicit nima ushlashini va nega CI'da kerakligini tushunaman.
  • [ ] Interface o'zgarishi va implementatsiya o'zgarishi qaysi target'larni qayta kompilyatsiya qilishini bashorat qila olaman.

16. Xulosa {#xulosa}

  • TMA (eski nomi µFeatures) — ilovani mustaqil feature modullaridan, aniq Interface'lar orqali quradi. Maqsad: har bir feature'ni tez build, test, try qilish.
  • Bitta modul = 5 target: Interface (shartnoma), Source (implementatsiya), Testing (qayta ishlatiladigan mock), Tests, Example (mustaqil mini-app).
  • Asosiy qoida: modul boshqa modulning Interface'iga bog'lanadi, Source'iga emas. Buni biz o'lchadik: Catalog implementatsiyasini o'zgartirganda faqat Catalog qayta kompilyatsiya bo'ldi (2.3 s), Home tegilmadi; Interface'ni o'zgartirganda butun zanjir qayta qurildi (4 target, 3.1 s).
  • ProjectDescriptionHelpersdagi factory 5 target'ni avtomatlashtiradi — Project.swift qisqa va konvensiya bir xil qoladi.
  • Faqat App (composition root) implementatsiyalarni yig'adi va inject qiladi; qolgan modullar protokollarni biladi.
  • Development uchun dinamik, release uchun statik framework (Environment orqali almashtiriladi). Mergeable libraries tavsiya etilmaydi.
  • tuist inspect dependencies TMA chegarasini majburlaydi — implicit (yashirin) va redundant (ortiqcha) bog'liqliklarni ushlaydi. Men bu loyihada aynan shu bilan implicit bog'liqliklarni topdim va tuzatdim.
  • Miqyos: Monzo (~200 modul, P50 PR 52→15 daq), Toss (~1000 modul), monday.com (~200 modul) — hammasi aynan shu ajratishga tayanadi (vendor case study'lar).

Keyingi dars: 07. Cache va optimizatsiya — binary cache, selective testing, Xcode cache va build insights — bu darsda qurgan modullarni endi binary cache qilib, qayta build'ni butunlay o'tkazib yuborishni va faqat o'zgargan modullar testini ishga tushirishni o'rganamiz.


17. Manbalar {#manbalar}

  1. The Modular Architecture (TMA) — Tuist docs — 5 target, core principle, dependencies, product types.
  2. Code sharing — Tuist docs — ProjectDescriptionHelpers va featureFramework factory namunasi.
  3. Synthesized files — Tuist docs — bundle accessor'lar (Bundle.module), resource synthesizer'lar.
  4. Dependencies — Tuist docs — product type, PackageSettings, static/dynamic.
  5. Reducing iOS Build Times by using Interface Modules — SwiftRock — Interface ajratish g'oyasining asli (TMA hujjati shunga tayanadi).
  6. Scaling a mature iOS codebase with Tuist — Asana Engineering (2023-02-21) — ~10 muhandis, ~300k LOC, 45 s → 15 s median, Inbox 30–50 s → 4 s, kamchiliklar.
  7. Monzo case study — Tuist — ~50→200 modul, P50 PR 52→15 daq, module cache warming.
  8. Toss: Building less to ship faster — Tuist case study va Tuist "scale" blog — ~1000 modul, graph computation narxi, focus workflow, implicit import tozalash.
  9. monday.com va Delivery Hero case study'lari — ~200 / ~115 modul, CI yaxshilanishlari.
  10. Trendyol: self-hosted Tuist — Tuist blog (2024-12-16) — build −65%, UI test tayyorlash 15 daq → 30 s.
  11. Build cache measurement methodology — intrada issue #1886 — build vaqtini halol o'lchash metodikasi (median of N, load average).

Markdown manba

iOS Lessons