Azimjon Abdurasulov

05. Dynamic Navigation va Routing Tizimi

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

05. Server-Driven Navigation va Dynamic Routing Tizimi

"Faqat UI ko'rinishini serverdan boshqarish — bu yarim yo'l. Haqiqiy qudrat foydalanuvchi qaysi ekranga, qanday o'tish usuli bilan (push, modal, sheet) o'tishi va back-stack qanday boshqarilishini server hal qilganda namoyon bo'ladi."


1. Nega Hardcoded NavigationLink SDUI da Ishlamaydi?

Klassik SwiftUI ilovasida tugma bosilganda qayerga o'tish kodga mixlab qo'yiladi:

// ❌ HARDCODED NAVIGATION: Relizsiz o'zgartirib bo'lmaydi
NavigationLink(destination: CheckoutViewController()) {
    Text("Sotib olish")
}

Agar kompaniya to'lov oqimini o'zgartirmoqchi bo'lsa (masalan, to'g'ridan-to'g'ri to'lovga emas, avval "Aksiya shartlari" degan yangi ekranga yo'naltirish), an'anaviy ilovada buni relizsiz qilib bo'lmaydi.

Server-Driven Navigation Qanday Ishlaydi?

Tugmaga faqat abstrakt Action biriktiriladi. Server tugma bosilganda nima sodir bo'lishini deklarativ JSON orqali belgilaydi:

{
  "event": "onTap",
  "type": "navigate",
  "route": "app://checkout/summary?orderId=8841",
  "presentation": "sheet",
  "sheetDetents": ["medium", "large"],
  "clearStackOnComplete": false
}

2. Dynamic Routing Arxitekturasi (Coordinator Pattern)

Navigatsiyani markazlashgan holda boshqarish uchun Universal Router & Coordinator arxitekturasi qo'llaniladi:

flowchart TD
    UserTap["Foydalanuvchi Tugmani Bosdi"] --> ActionDispatcher["ActionDispatcher.dispatch(action)"]
    ActionDispatcher --> RouteClassifier{"Route Turini Aniqlash"}
    
    RouteClassifier -->|"app://sdui/screen?path=..."| SDUILoader["Dinamik SDUI Ekranni Yuklash"]
    RouteClassifier -->|"app://native/camera"| NativeBridge["Native Funksiyani Ishga Tushirish (FaceID, Kamera)"]
    RouteClassifier -->|"https://azimjondev.uz/help"| WebSafari["Safari / WebKit Brauzerda Ochish"]
    RouteClassifier -->|"app://navigation/pop_to_root"| StackCleaner["Navigation Stack'ni Tozalash"]

    SDUILoader --> PresentationEngine{"Taqdimot Turi (Presentation)"}
    PresentationEngine -->|"push"| NavStack["NavigationStack (Push)"]
    PresentationEngine -->|"sheet"| SheetModal["BottomSheet / Sheet"]
    PresentationEngine -->|"fullScreen"| FullModal["FullScreenCover"]

    style ActionDispatcher fill:#131720,stroke:#bef264,stroke-width:2px
    style RouteClassifier fill:#181d26,stroke:#7da2ce,stroke-width:1px
    style PresentationEngine fill:#181d26,stroke:#60a5fa,stroke-width:1px

3. Multi-Step Dynamic Wizard va Back-Stack Boshqaruvi

Keling, real FinTech o'tkazma ssenariysini ko'rib chiqamiz:

stateDiagram-v2
    direction LR
    [*] --> Step1: 1. Karta Raqami Kiritish
    Step1 --> Step2: 2. Summani Kiritish
    Step2 --> Step3: 3. SMS OTP Tasdiqlash
    Step3 --> Success: 4. Muvaffaqiyat Ekrani (Success)
    
    Success --> [*]: "Bosh Sahifaga Qaytish"\n(action: pop_to_root)

Foydalanuvchi muvaffaqiyat ekranida "Bosh sahifaga qaytish" tugmasini bosganida, server quyidagi action'ni beradi:

{
  "type": "navigation_command",
  "command": "pop_to_root"
}

Clientdagi router.popToRoot() komandasi bir zumda oradagi barcha 3 ta ekranni xotiradan tozalab, foydalanuvchini asosiy sahifaga qaytaradi. Foydalanuvchi "Orqaga" bosib yana SMS kod kiritish oynasiga qaytib qolmaydi!


4. Swift 6 da To'liq Ishlovchi Universal Router Kodi

import SwiftUI

// MARK: - 1. Taqdimot Turlari
public enum PresentationStyle: String, Decodable, Sendable {
    case push
    case sheet
    case fullScreenCover
}

// MARK: - 2. Navigatsiya Marshruti
public struct SDUIAppRoute: Hashable, Identifiable, Sendable {
    public var id: String { url.absoluteString + "_\(presentation.rawValue)" }
    public let url: URL
    public let presentation: PresentationStyle
}

// MARK: - 3. Markaziy Router
@MainActor
@Observable
public final class SDUIRouter {
    public static let shared = SDUIRouter()
    
    public var navigationPath = NavigationPath()
    public var sheetRoute: SDUIAppRoute?
    public var fullScreenRoute: SDUIAppRoute?
    
    private init() {}
    
    /// Serverdan kelgan marshrutni bajarish
    public func navigate(to rawRoute: String, presentation: PresentationStyle = .push) {
        guard let url = URL(string: rawRoute) else { return }
        
        // Maxsus navigatsiya buyruqlari
        if url.scheme == "app" && url.host == "navigation" {
            handleNavigationCommand(url: url)
            return
        }
        
        let appRoute = SDUIAppRoute(url: url, presentation: presentation)
        
        switch presentation {
        case .push:
            navigationPath.append(appRoute)
        case .sheet:
            sheetRoute = appRoute
        case .fullScreenCover:
            fullScreenRoute = appRoute
        }
    }
    
    private func handleNavigationCommand(url: URL) {
        switch url.path {
        case "/pop_to_root":
            popToRoot()
        case "/pop":
            pop()
        case "/dismiss":
            sheetRoute = nil
            fullScreenRoute = nil
        default:
            break
        }
    }
    
    public func pop() {
        if !navigationPath.isEmpty {
            navigationPath.removeLast()
        }
    }
    
    public func popToRoot() {
        navigationPath = NavigationPath()
        sheetRoute = nil
        fullScreenRoute = nil
    }
}

5. Asosiy Ilova Konteyneri (SDUIRootContainerView)

Router'ni butun ilovaga ulovchi asosiy SwiftUI ko'rinishi:

import SwiftUI

public struct SDUIRootContainerView: View {
    @State private var router = SDUIRouter.shared
    public let rootEndpoint: String
    
    public init(rootEndpoint: String) {
        self.rootEndpoint = rootEndpoint
    }
    
    public var body: some View {
        NavigationStack(path: $router.navigationPath) {
            RemoteSDUIScreen(endpoint: rootEndpoint)
                .navigationDestination(for: SDUIAppRoute.self) { route in
                    RemoteSDUIScreen(endpoint: route.url.path)
                }
        }
        .sheet(item: $router.sheetRoute) { route in
            RemoteSDUIScreen(endpoint: route.url.path)
        }
        .fullScreenCover(item: $router.fullScreenRoute) { route in
            RemoteSDUIScreen(endpoint: route.url.path)
        }
    }
}

public struct RemoteSDUIScreen: View {
    public let endpoint: String
    @State private var components: [ComponentNode] = []
    
    public var body: some View {
        SDUIScreenView(components: components)
            .task {
                // Serverdan endpoint bo'yicha JSON yuklash
            }
    }
}

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

<details> <summary><b>1-Savol: Nega SDUI navigatsiyasida har doim Universal Deeplink formati (masalan, 'app://profile/edit') ishlatilishi shart?</b> (Yechimni ko'rish uchun bosing)</summary>

Tahlil va Yechim: Chunki bitta yagona format 3 ta turli manba uchun bir xil xizmat qiladi:

  1. Ichki SDUI tugmalari: Barcha komponentlar ushbu havola orqali harakat qiladi.
  2. Push-Notification: Foydalanuvchiga yuborilgan push xabari to'g'ridan-to'g'ri kerakli ekranni ochadi.
  3. Universal Links: Veb-saytdagi havolani bosganda ilova bir xil marshrut bilan kerakli ekranga olib o'tadi.

</details>

<details> <summary><b>2-Savol: Xavfsizlik bo'yicha: agar server JSON orqali 'app://safari?url=javascript:...' yuborsa, router qanday himoyalanishi lozim?</b> (Yechimni ko'rish uchun bosing)</summary>

Tahlil va Yechim: Router ichida URL Scheme Whitelist (Ruxsat etilgan sxemalar ro'yxati) bo'lishi shart. Faqat tasdiqlangan sxemalar (app://, https://) va xavfsiz ichki domenlar ochiladi. Har qanday javascript:, file:// yoki ruxsat etilmagan sxemalar darhol bloklanadi va xavfsizlik loglariga qayd etiladi. </details>


7. Amaliy Topshiriq (Senior Challenge)

SDUIRouter ga sheet orqali ochilgan modal oyna yopilganda o'zidan oldingi ekranni yangilash (Refresh Trigger) parametrini qo'shing:

{
  "type": "navigate",
  "route": "app://card/add",
  "presentation": "sheet",
  "onDismiss": { "action": "reload_screen" }
}

Muvaffaqiyatli karta qo'shilgach, sheet yopilib, asosiy sahifadagi kartalar ro'yxati qayta so'rovsiz yoki yangilangan holatda chizilishini ta'minlang!

Markdown manba

iOS Lessons