Azimjon Abdurasulov

08. Code Sharing va App Store

🚀 Tuist — Mukammal Qo‘llanma & Amaliyot

🔧 Code Sharing, Templates va App Store Publish

"Kodni takrorlamang — ulashing! DRY (Don't Repeat Yourself) prinsipi Tuist manifest'lari uchun ham amal qiladi."


1. ProjectDescriptionHelpers — Manifest'lar Orasida Kod Ulashish

ProjectDescriptionHelpers — bu Tuist'ning eng kuchli feature'laridan biri. U barcha manifest fayllarda ishlatilishi mumkin bo'lgan shared Swift code papkasi.

Joylashuv

MyApp/
├── Tuist/
│   └── ProjectDescriptionHelpers/    ← SHU PAPKA
│       ├── Project+Templates.swift
│       ├── Target+Templates.swift
│       └── Constants.swift
├── Project.swift                      ← import ProjectDescriptionHelpers
└── Tuist.swift

Misol 1: Target Template

// Tuist/ProjectDescriptionHelpers/Target+Templates.swift
import ProjectDescription

extension Target {
    /// iOS Framework target yaratish uchun helper
    public static func makeFramework(
        name: String,
        sources: String? = nil,
        dependencies: [TargetDependency] = [],
        resources: ResourceFileElements? = nil
    ) -> Target {
        return .target(
            name: name,
            destinations: .iOS,
            product: .framework,
            bundleId: "com.example.\(name)",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .default,
            sources: ["\(sources ?? name)/Sources/**"],
            resources: resources,
            dependencies: dependencies
        )
    }

    /// iOS App target yaratish uchun helper
    public static func makeApp(
        name: String,
        dependencies: [TargetDependency] = []
    ) -> Target {
        return .target(
            name: name,
            destinations: .iOS,
            product: .app,
            bundleId: "com.example.\(name)",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .extendingDefault(with: [
                "UILaunchScreen": [
                    "UIColorName": "",
                    "UIImageName": "",
                ],
            ]),
            sources: ["\(name)/Sources/**"],
            resources: ["\(name)/Resources/**"],
            dependencies: dependencies
        )
    }
}

Misol 2: Konstantalar

// Tuist/ProjectDescriptionHelpers/Constants.swift
import ProjectDescription

public enum AppConstants {
    public static let organizationName = "com.example"
    public static let deploymentTarget: DeploymentTargets = .iOS("16.0")
    public static let destinations: Destinations = .iOS

    // Build settings
    public static let baseSettings: SettingsDictionary = [
        "SWIFT_VERSION": "5.9",
        "CODE_SIGN_STYLE": "Automatic",
        "DEVELOPMENT_TEAM": "ABCDE12345",
    ]
}

Manifest'da Ishlatish

// Project.swift
import ProjectDescription
import ProjectDescriptionHelpers  // ← Import!

let project = Project(
    name: "MyApp",
    targets: [
        .makeFramework(
            name: "CoreModule",
            dependencies: [
                .external(name: "Alamofire"),
            ]
        ),
        .makeFramework(
            name: "HomeFeature",
            dependencies: [
                .target(name: "CoreModule"),
            ]
        ),
        .makeApp(
            name: "MyApp",
            dependencies: [
                .target(name: "HomeFeature"),
            ]
        ),
    ]
)

50 qatorlik har bir target 5 qatorga tushdi! Va barcha target'lar bir xil standart da yaratiladi.


2. Templates va tuist scaffold

Template'lar yangi module yaratishni avtomatlashtiradi.

Template Yaratish

mkdir -p Tuist/Templates/Feature
// Tuist/Templates/Feature/Feature.swift
import ProjectDescription

let nameAttribute: Template.Attribute = .required("name")

let template = Template(
    description: "Yangi feature module yaratish",
    attributes: [
        nameAttribute,
    ],
    items: [
        // Sources papka va fayl
        .file(
            path: "Modules/\(nameAttribute)/Sources/\(nameAttribute)View.swift",
            templatePath: "FeatureView.stencil"
        ),
        .file(
            path: "Modules/\(nameAttribute)/Sources/\(nameAttribute)ViewModel.swift",
            templatePath: "FeatureViewModel.stencil"
        ),
        // Tests
        .file(
            path: "Modules/\(nameAttribute)/Tests/\(nameAttribute)ViewModelTests.swift",
            templatePath: "FeatureTests.stencil"
        ),
        // Interface
        .file(
            path: "Modules/\(nameAttribute)/Interface/\(nameAttribute)ServiceProtocol.swift",
            templatePath: "FeatureInterface.stencil"
        ),
    ]
)

Stencil Template Fayllari

// Tuist/Templates/Feature/FeatureView.stencil
import SwiftUI
import {{ name }}Interface

public struct {{ name }}View: View {
    @StateObject private var viewModel: {{ name }}ViewModel

    public init(viewModel: {{ name }}ViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel)
    }

    public var body: some View {
        Text("{{ name }} View")
    }
}
// Tuist/Templates/Feature/FeatureViewModel.stencil
import Foundation
import {{ name }}Interface

public class {{ name }}ViewModel: ObservableObject {
    private let service: {{ name }}ServiceProtocol

    public init(service: {{ name }}ServiceProtocol) {
        self.service = service
    }
}

Scaffold Buyrug'i

# Yangi feature yaratish
tuist scaffold Feature --name Settings

# Natija:
# Modules/Settings/Sources/SettingsView.swift
# Modules/Settings/Sources/SettingsViewModel.swift
# Modules/Settings/Tests/SettingsViewModelTests.swift
# Modules/Settings/Interface/SettingsServiceProtocol.swift

Foyda: Yangi modul qo'shish 30 soniya oladi. Va barcha modullar bir xil strutura da yaratiladi!


3. Plugins — Tashqi Tuist Plugin'lar

Plugin'lar — bu bir nechta loyiha orasida Tuist konfiguratsiyasini ulashish uchun ishlatiladi.

Plugin Yaratish

TuistPluginMyCompany/
├── Plugin.swift
├── ProjectDescriptionHelpers/
│   └── CompanyTargetTemplates.swift
└── Templates/
    └── ... (ixtiyoriy)
// Plugin.swift
import ProjectDescription

let plugin = Plugin(name: "MyCompanyPlugin")

Plugin'ni Ishlatish

// Tuist.swift
import ProjectDescription

let tuist = Tuist(
    plugins: [
        .git(
            url: "https://github.com/mycompany/tuist-plugin",
            tag: "1.0.0"
        ),
        // Yoki local path
        .local(path: "../TuistPluginMyCompany"),
    ]
)
tuist install  # Plugin'ni ham yuklaydi

4. Synthesized Files

Tuist ma'lum fayllarni avtomatik generate qila oladi.

Bundle Accessors

Resource'larga kirish uchun avtomatik code yaratiladi:

// Tuist avtomatik yaratadi:
// Bundle+Module.swift
extension Bundle {
    static let module: Bundle = .init(for: BundleToken.self)
}

// Sizning kodingizda:
let image = UIImage(named: "logo", in: .module, compatibleWith: nil)

Asset Accessors

// Tuist avtomatik yaratadi (SwiftGen kabi):
// Assets+Generated.swift
enum Asset {
    static let logo = ImageAsset(name: "logo")
    static let primaryColor = ColorAsset(name: "primaryColor")
}

// Kodda:
let image = Asset.logo.image
let color = Asset.primaryColor.color

Sozlash

// Project.swift
let project = Project(
    name: "MyApp",
    resourceSynthesizers: [
        .assets(),        // Assets.xcassets uchun
        .strings(),       // Localizable.strings uchun
        .fonts(),         // Fontlar uchun
        .plists(),        // Plist fayllar uchun
    ],
    targets: [...]
)

5. Dynamic Configuration

Environment variable'lar bilan manifest'ni o'zgartirish:

// Project.swift yoki Helper'da
import ProjectDescription

// Environment dan o'qish
func resolveProductType() -> Product {
    if case let .string(value) = Environment.productType {
        switch value {
        case "static": return .staticFramework
        case "dynamic": return .framework
        default: return .framework
        }
    }
    return .framework
}

// Ishlatish
.target(
    name: "CoreModule",
    product: resolveProductType(),  // ← Dynamic!
    // ...
)
# Development (dynamic)
tuist generate

# Release (static)
TUIST_PRODUCT_TYPE=static tuist generate

# Staging
TUIST_API_URL=https://staging.example.com tuist generate

6. App Store Archive va Publish (Batafsil)

6.1 Tuist Loyihasini Archive Qilish

Tuist loyihasi oddiy Xcode loyiha sifatida archive qilinadi. Hech qanday farq yo'q!

Xcode UI orqali (oddiy usul)
# 1. Generate qiling
tuist install
tuist generate

# 2. Xcode ochiladi
# 3. Product → Archive
# 4. Organizer da: Distribute App → App Store Connect → Upload
Terminal orqali (CI/CD uchun)
#!/bin/bash
# build_and_upload.sh

# 1. Tuist generate
tuist install
tuist generate --no-open

# 2. Archive
xcodebuild archive \
    -workspace MyApp.xcworkspace \
    -scheme MyApp \
    -configuration Release \
    -destination "generic/platform=iOS" \
    -archivePath ./build/MyApp.xcarchive \
    CODE_SIGN_STYLE=Manual \
    PROVISIONING_PROFILE_SPECIFIER="MyApp Distribution" \
    CODE_SIGN_IDENTITY="Apple Distribution"

# 3. Export IPA
xcodebuild -exportArchive \
    -archivePath ./build/MyApp.xcarchive \
    -exportPath ./build/ipa \
    -exportOptionsPlist ./ExportOptions.plist

# 4. Upload to App Store Connect
xcrun altool --upload-app \
    -f ./build/ipa/MyApp.ipa \
    -t ios \
    --apiKey "YOUR_API_KEY_ID" \
    --apiIssuer "YOUR_ISSUER_ID"

6.2 ExportOptions.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store</string>
    <key>teamID</key>
    <string>ABCDE12345</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>com.example.MyApp</key>
        <string>MyApp Distribution</string>
    </dict>
    <key>uploadBitcode</key>
    <false/>
    <key>uploadSymbols</key>
    <true/>
</dict>
</plist>

6.3 Signing Settings — Project.swift da

.target(
    name: "MyApp",
    // ...
    settings: .settings(
        base: [
            // Automatic signing (developer account)
            "CODE_SIGN_STYLE": "Automatic",
            "DEVELOPMENT_TEAM": "ABCDE12345",
        ],
        configurations: [
            .debug(
                name: "Debug",
                settings: [
                    "CODE_SIGN_IDENTITY": "Apple Development",
                ]
            ),
            .release(
                name: "Release",
                settings: [
                    "CODE_SIGN_IDENTITY": "Apple Distribution",
                    // Manual signing uchun:
                    // "CODE_SIGN_STYLE": "Manual",
                    // "PROVISIONING_PROFILE_SPECIFIER": "MyApp Distribution",
                ]
            ),
        ]
    )
)

6.4 Fastlane Bilan Integratsiya

# Fastfile
default_platform(:ios)

platform :ios do
  desc "Build using Tuist and upload to App Store"
  lane :release do
    # 1. Tuist generate
    sh("cd .. && tuist install")
    sh("cd .. && tuist generate --no-open")

    # 2. Build
    build_app(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      configuration: "Release",
      export_method: "app-store",
    )

    # 3. Upload
    upload_to_app_store(
      skip_metadata: true,
      skip_screenshots: true,
    )
  end
end
# Fastlane ishga tushirish
fastlane release

6.5 Xcode Cloud Bilan

// ci_scripts/ci_post_clone.sh (Xcode Cloud)
#!/bin/bash
set -e

# Tuist o'rnatish
brew tap tuist/tuist
brew install --formula tuist

# Dependencies va Generate
tuist install
tuist generate --no-open

Muhim: Xcode Cloud ci_scripts/ci_post_clone.sh scriptini avtomatik ishga tushiradi clone'dan keyin.


Xulosa

FeatureTushuntirish
ProjectDescriptionHelpersManifest kodini DRY qilish
TemplatesYangi modul yaratishni avtomatlashtirish
PluginsLoyihalar orasida config ulashish
Synthesized FilesAsset/String accessors avtomatik
Dynamic ConfigEnvironment variable bilan manifest o'zgartirish
App Store ArchiveXcode UI yoki xcodebuild terminal bilan
Fastlane/Xcode CloudCI/CD bilan to'liq integratsiya

💡 Keyingi darslik: Real Loyiha #1: ToDo App — Birinchi real loyihani Tuist bilan quramiz!

"Yaxshi vositalar yaxshi ustalar yaratadi. Tuist — sizning eng yaxshi vositangiz!" 🛠️

Markdown manba

iOS Lessons