Azimjon Abdurasulov

03. JSON Schema va Component Tree

⚡ Server-Driven UI (SDUI) — Mukammal Enterprise & System Design

03. JSON Schema va Component Tree: Server Bilan Qat'iy Shartnoma

"Agar schema'da noaniqlik yoki bo'shliq bo'lsa, mobil ilovada albatta crash yuz beradi. SDUI tizimlarida Schema — bu backend va mobile injiniring jamoalari o'rtasidagi konstitutsiyadir."


1. Component Tree Anatomiyasi: Serverdan Keluvchi Iyerarxiya

Server-Driven UI tizimlarida har bir ekran daraxtsimon komponentlar strukturasi (Component Tree) ko'rinishida taqdim etiladi.

Quyidagi diagrammada SDUI ekranining iyerarxik tuzilishi tasvirlangan:

graph TD
    Root["Screen Root (Dashboard)"] --> Header["Section: Header (Sticky)"]
    Root --> Body["Section: Body (Lazy Scroll)"]
    Root --> Bottom["Section: Bottom Bar (Pinned)"]

    Header --> UserWidget["Component: UserProfileHeader\nid: 'u_101', type: 'profile_header'"]
    
    Body --> Carousel["Component: PromoCarousel\nid: 'c_201', type: 'carousel_banner'"]
    Body --> QuickActions["Component: QuickActionsGrid\nid: 'q_301', type: 'actions_grid'"]
    Body --> Feed["Component: DynamicTransactionFeed\nid: 'f_401', type: 'history_list'"]

    Bottom --> CTA["Component: PrimaryActionButton\nid: 'btn_501', type: 'action_button'"]

    style Root fill:#131720,stroke:#bef264,stroke-width:2px
    style Header fill:#181d26,stroke:#7da2ce,stroke-width:1px
    style Body fill:#181d26,stroke:#7da2ce,stroke-width:1px
    style Bottom fill:#181d26,stroke:#7da2ce,stroke-width:1px

2. To'liq JSON Schema Spetsifikatsiyasi

Professional SDUI arxitekturasida server ixtiyoriy JSON yubora olmaydi. U qat'iy tekshirilgan JSON Schema shartnomasiga bo'ysunadi:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "screenId": "dashboard_home",
  "version": "2.1.0",
  "minSupportedClientVersion": "1.4.0",
  "refreshPolicy": {
    "ttlSeconds": 180,
    "strategy": "cache_first",
    "backgroundRefresh": true
  },
  "layout": {
    "type": "scroll_view",
    "showsIndicators": false,
    "bounce": true
  },
  "components": [
    {
      "id": "header_profile_01",
      "type": "user_profile_header",
      "layout": {
        "padding": { "top": 16, "leading": 20, "bottom": 12, "trailing": 20 },
        "zIndex": 10
      },
      "props": {
        "avatarUrl": "https://cdn.azimjondev.uz/avatars/user_102.jpg",
        "userName": "Azimjon Dasturchi",
        "badgeText": "PRO",
        "badgeColor": "token.accentSuccess",
        "balanceFormatted": "14,500,000 UZS"
      },
      "accessibility": {
        "label": "Azimjon Dasturchi, balansingiz 14 million 500 ming so'm",
        "trait": "header"
      },
      "actions": [
        {
          "event": "onTap",
          "type": "navigate",
          "route": "app://settings/profile",
          "presentation": "push"
        }
      ]
    },
    {
      "id": "promo_banner_carousel",
      "type": "carousel_banner",
      "layout": {
        "margin": { "top": 8, "bottom": 16 }
      },
      "props": {
        "autoScrollInterval": 4.5,
        "aspectRatio": 2.1,
        "items": [
          {
            "id": "banner_ramadan",
            "imageUrl": "https://cdn.azimjondev.uz/banners/spring.png",
            "route": "app://shop/category/tech"
          }
        ]
      }
    }
  ]
}

3. Polymorphic Decoding Fojiasi va Crashlarning Oldini Olish

Swift tilida SDUI modellari bilan ishlashda eng katta halokatli xato — bu polimorfik massivlarni oddiy Decodable enum bilan parslashdir:

// ❌ HALOKATLI XATO (ANTI-PATTERN):
enum ComponentPayload: Decodable {
    case profileHeader(ProfileHeaderProps)
    case carousel(CarouselProps)
    // Ertaga backend yangi 'cryptoBanner' qo'shsa...
}

Nega bu butun ilovani qulatadi?

flowchart TD
    subgraph Bad["1. Oddiy Swift JSONDecoder (Falokat)"]
        RawJSON["JSON: [Profile, Carousel, CryptoWidget]"] --> Parser1["JSONDecoder.decode([Component].self)"]
        Parser1 -->|"CryptoWidget tanilmadi!"| Fail["DecodingError.dataCorrupted"]
        Fail --> WhiteScreen["💥 BUTUN MASSIV QULAYDI: Foydalanuvchiga OQ EKRAN!"]
    end

    subgraph Good["2. Safe Failable Array Decoding (Professional)"]
        RawJSON2["JSON: [Profile, Carousel, CryptoWidget]"] --> Parser2["SafeDecodableArray"]
        Parser2 --> Success1["✅ ProfileHeader -> Render"]
        Parser2 --> Success2["✅ Carousel -> Render"]
        Parser2 --> Fallback["⚠️ CryptoWidget -> Noma'lum! Tashlab ketiladi"]
        Success1 --> SmoothUI["🚀 EKRAN 99% MUKAMMAL ISHLAYDI!"]
        Success2 --> SmoothUI
    end

    style Bad fill:#251014,stroke:#f87171,stroke-width:1px
    style Good fill:#0d1f14,stroke:#4ade80,stroke-width:1px

Swift 6 da Ishlovchi "Safe Failable Array Decoding" Kodi:

import Foundation

// MARK: - 1. Safe Property Wrapper
@propertyWrapper
public struct SafeDecodableArray<T: Decodable>: Decodable, Sendable where T: Sendable {
    public var wrappedValue: [T]
    
    public init(wrappedValue: [T]) {
        self.wrappedValue = wrappedValue
    }
    
    public init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        var elements: [T] = []
        
        while !container.isAtEnd {
            do {
                let element = try container.decode(T.self)
                elements.append(element)
            } catch {
                #if DEBUG
                print("[SDUI Defensive Decoding] Noma'lum yoki buzilgan komponent tashlab ketildi: \(error)")
                #endif
                // Noma'lum elementni xavfsiz o'tkazib yuborish
                _ = try? container.decode(DummyCodable.self)
            }
        }
        self.wrappedValue = elements
    }
}

private struct DummyCodable: Decodable {}

// MARK: - 2. Asosiy Ekran Modeli
public struct SDUIScreenPayload: Decodable, Sendable {
    public let screenId: String
    public let version: String
    public let minSupportedClientVersion: String?
    
    // Xavfsiz failable massiv
    @SafeDecodableArray
    public var components: [ComponentNode]
    
    enum CodingKeys: String, CodingKey {
        case screenId
        case version
        case minSupportedClientVersion
        case components
    }
}

// MARK: - 3. Komponent Modeli
public struct ComponentNode: Decodable, Identifiable, Sendable {
    public let id: String
    public let type: String
    public let layout: ComponentLayout?
    public let props: [String: AnyCodable]
    public let actions: [ActionNode]?
    public let accessibility: AccessibilityNode?
}

4. Design Tokens: Nega Qat'iy Hex Kodlar Katta Xato?

Serverdan "color": "#FF0000" (qattiq hex kod) yuborish SDUI da eng xavfli anti-patternlardan biridir.

Qattiq Hex Kodlarning 3 Ta Zarari:

  1. Dark Mode Fojiasi: Tungi rejimda server har bir element uchun o'zi rang hisoblab chiqishi mumkin emas (tarmoq trafigi va arxitektura buziladi).
  2. Apple Dynamic Type: Shrift "fontSize": 14 bo'lsa, iOS sozlamalarida shriftni kattalashtirgan foydalanuvchida yozuvlar bir-birining ustiga minib ketadi.
  3. Rebranding: Kompaniya brend rangini och yashildan to'q yashilga o'zgartirsa, serverdagi barcha tarixiy JSON fayllarni tahrirlab chiqish kerak bo'ladi.

To'g'ri Yechim: Semantic Design Tokens

Server faqat semantik nom (token) yuboradi:

{
  "textColor": "token.textPrimary",
  "backgroundColor": "token.surfaceElevated",
  "typography": "token.headlineMedium",
  "cornerRadius": "token.radiusLarge"
}

iOS klientida bu semantik tokenlar tizimning native ranglariga map qilinadi:

import SwiftUI

public enum DesignTokens {
    public static func color(for token: String) -> Color {
        switch token {
        case "token.textPrimary":
            return Color.primary
        case "token.textSecondary":
            return Color.secondary
        case "token.surfaceElevated":
            return Color(uiColor: .secondarySystemBackground)
        case "token.accentSuccess":
            return Color.green
        case "token.accentDanger":
            return Color.red
        default:
            return Color.primary // Fallback
        }
    }
    
    public static func font(for token: String) -> Font {
        switch token {
        case "token.headlineLarge":
            return Font.system(.title2, design: .rounded, weight: .bold)
        case "token.headlineMedium":
            return Font.system(.headline, design: .default, weight: .semibold)
        case "token.bodyRegular":
            return Font.body
        case "token.caption":
            return Font.caption
        default:
            return Font.body
        }
    }
}

5. Breaking vs Non-Breaking Schema Changes

Senior dasturchi doimo quyidagi qoidaga rioya qilishi shart:

O'zgarish TuriXususiyatiHolatiNatija
Yangi maydon (Field) qo'shishOptional (field: String?)✅ Non-BreakingEski ilovalar buni shunchaki e'tiborsiz qoldiradi
Yangi komponent turi qo'shishSafeDecodableArray bilan✅ Non-BreakingYangi ilovalar chizadi, eski ilovalarda fallback ishlaydi
Maydon turini o'zgartirishprice: 15000 (Int) -> price: "15,000" (String)❌ CRITICAL BREAKING!Eski ilovalarda parslash xatosi va crash
Majburiy maydonni o'chirishid yoki type ni olib tashlash❌ CRITICAL BREAKING!Butun komponent parslanishdan to'xtaydi

6. Interaktiv Savol-Javob (Self-Check Quiz)

<details> <summary><b>1-Savol: Nega JSON Schema'da har bir komponentda majburiy unikal 'id' bo'lishi shart?</b> (Yechimni ko'rish uchun bosing)</summary>

Tahlil va Yechim: SwiftUI'da ForEach yoki UIKit'da UICollectionViewDiffableDataSource ekranni chizayotganda elementlarni identifikatsiyalashi kerak (Identifiable). Agar id bo'lmasa, serverdan yangi data kelganda butun List qayta chiziladi (re-created), bu esa ekranda miltillash (flicker) va 120 FPS dan 30 FPS ga tushib qolishni keltirib chiqaradi. </details>

<details> <summary><b>2-Savol: Backend maydon turini o'zgartirishi kerak (masalan, Int dan String ga). Buni eski ilovalarni sindirmasdan qanday qilish mumkin?</b> (Yechimni ko'rish uchun bosing)</summary>

Tahlil va Yechim: Eski maydonni o'zgartirmasdan, yangi nom bilan yangi maydon qo'shiladi:

{
  "price": 15000,             // Eski ilovalar uchun saqlanadi
  "priceFormatted": "15,000 UZS" // Yangi versiyalar uchun qo'shiladi
}

Eski ilovalar o'zining price qiymatini o'qishda davom etadi, yangi ilovalar esa priceFormatted ni oladi. 1 yildan keyin eski maydon o'chirilishi mumkin. </details>


7. Amaliy Topshiriq (Senior Challenge)

O'z kompyuteringizda SafeDecodableArray testini yozing:

  1. 3 ta to'g'ri ComponentNode va 2 ta xato strukturaga ega bo'lgan JSON test string yarating.
  2. JSONDecoder().decode(SDUIScreenPayload.self, from: data) orqali dekod qiling.
  3. Assert qiling: natijada array crash bo'lmasdan aynan 3 ta elementni qabul qilganini Unit Test orqali tasdiqlang!

Markdown manba

iOS Lessons