Published on

Coordinator pattern — navigatsiya boshqaruvi

Authors

Coordinator Pattern — navigatsiya logikasini View lardan ajratadi. Kichik ilovada View ichida NavigationLink yetarli. Lekin 10+ ekranli ilovada navigatsiya oqimini boshqarish murakkablashadi — Coordinator buni markazlashtiradi.

Coordinator tuzilmasi

// ═══════════════════════════════════════════════════════════════
//  🗺 ROUTE — barcha mumkin bo'lgan ekranlar
//
//  Route = "manzil". Ilovadagi har ekran bu enum ning bitta case i.
//  Hashable — NavigationPath da saqlash uchun
//
//  Nima uchun enum? Chunki:
//  1. Barcha ekranlar BIR JOYDA ko'rinadi
//  2. Yangi ekran qo'shsangiz — case qo'shasiz
//  3. Compiler o'tkazib yuborilgan case ni aytadi (switch exhaustive)
//  4. Associated value — ekranga ma'lumot uzatish
// ═══════════════════════════════════════════════════════════════
enum AppRoute: Hashable {
    case boshSahifa                         // Asosiy ekran
    case maqolaTafsiloti(maqolaId: Int)     // Maqola tafsiloti — ID kerak
    case profil                             // Foydalanuvchi profili
    case sozlamalar                         // Sozlamalar ekrani
    case maqolaMuharriri(maqolaId: Int?)    // nil = yangi maqola yaratish
    // Int? — Optional: bor = tahrirlash, nil = yangi
}


// ═══════════════════════════════════════════════════════════════
//  🎯 COORDINATOR — navigatsiya markaziy boshqaruvchisi
//
//  Coordinator — "yo'l ko'rsatuvchi". U:
//  ✅ Qaysi ekranga o'tishni boshqaradi (push, pop, sheet)
//  ✅ NavigationPath ni egalaydi va boshqaradi
//  ✅ Deep link larni parse qiladi
//  ✅ View → Route → Coordinator → View ochish
//
//  View lar bir-birini BILMAYDI:
//  ❌ BoshSahifa → ProfilView() emas
//  ✅ BoshSahifa → coordinator.push(.profil)
//
//  Bu Single Responsibility Principle:
//  • View = UI ko'rsatish
//  • ViewModel = logika
//  • Coordinator = NAVIGATSIYA
//
//  @MainActor — UI thread da ishlash
//  ObservableObject — path o'zgarganda View yangilanadi
// ═══════════════════════════════════════════════════════════════
@MainActor
class AppCoordinator: ObservableObject {
    // NavigationPath — navigatsiya stacki (yo'l)
    // path = [.boshSahifa, .profil, .sozlamalar]
    // → Bosh sahifa → Profil → Sozlamalar (3 ekran)
    @Published var path = NavigationPath()

    // Sheet va fullscreen uchun alohida holat
    @Published var sheet: AppRoute?
    @Published var fullScreenCover: AppRoute?

    // ── PUSH — yangi ekranni stack ga qo'shish ──
    // NavigationLink kabi, lekin dasturiy boshqaruv bilan
    func push(_ route: AppRoute) {
        path.append(route)
        // path: [..., .profil] — profil ekrani ochiladi
    }

    // ── POP — oxirgi ekranni yopish ──
    func pop() {
        guard !path.isEmpty else { return }
        // guard — bo'sh stack da crash oldini olish
        path.removeLast()
    }

    // ── POP TO ROOT — hammani yopib bosh sahifaga qaytish ──
    func popToRoot() {
        path.removeLast(path.count)
        // Barcha ekranlarni stack dan olib tashlash
        // Faqat bosh sahifa qoladi
    }

    // ── SHEET — modal oyna ochish ──
    func present(_ route: AppRoute) {
        sheet = route
        // sheet != nil → .sheet ko'rsatiladi
    }

    // ── FULL SCREEN — to'liq ekranli modal ──
    func presentFullScreen(_ route: AppRoute) {
        fullScreenCover = route
    }

    // ── DISMISS — modal oynani yopish ──
    func dismiss() {
        sheet = nil
        fullScreenCover = nil
    }

    // ══════════════════════════════════════
    //  DEEP LINKING — tashqi URL dan ekranga o'tish
    //
    //  Misol URL: meningIlovam://maqola?id=42
    //  → Maqola tafsiloti ekrani ochiladi (id=42)
    //
    //  Bu push notification, QR kod, veb sahifadan
    //  ilovaning aniq ekraniga tushish imkoni beradi
    // ══════════════════════════════════════
    func handleDeepLink(url: URL) {
        // URLComponents — URL ni qismlarga ajratish
        guard let components = URLComponents(
            url: url,
            resolvingAgainstBaseURL: false
        ) else { return }

        // URL path bo'yicha route aniqlash
        switch components.path {
        case "/maqola":
            // URL: /maqola?id=42
            if let idString = components.queryItems?
                .first(where: { $0.name == "id" })?.value,
               let id = Int(idString) {
                popToRoot()  // Avval bosh sahifaga
                push(.maqolaTafsiloti(maqolaId: id))  // Keyin maqolaga
            }
        case "/profil":
            popToRoot()
            push(.profil)
        default:
            break  // Noma'lum URL — hech narsa qilmaymiz
        }
    }

    // ══════════════════════════════════════
    //  ROUTE → VIEW — qaysi ekran ko'rsatiladi?
    //
    //  @ViewBuilder — switch ichida View qaytarish
    //  Bu funksiya Route ni View ga "tarjima qiladi"
    //  Barcha ekranlar SHU YERDA boshqariladi
    // ══════════════════════════════════════
    @ViewBuilder
    func view(for route: AppRoute) -> some View {
        switch route {
        case .boshSahifa:
            BoshSahifaKorinishi()
        case .maqolaTafsiloti(let id):
            MaqolaTafsilotiKorinishi(maqolaId: id)
        case .profil:
            ProfilKorinishi()
        case .sozlamalar:
            SozlamalarKorinishi()
        case .maqolaMuharriri(let id):
            MaqolaMuharrirKorinishi(maqolaId: id)
            // id = nil → yangi maqola
            // id = 42 → maqola #42 ni tahrirlash
        }
    }
}


// ═══════════════════════════════════════════════════════════════
//  🏠 APP ROOT — Coordinator ni NavigationStack ga ulash
//
//  Bu ilovaning "ildiz" View i.
//  NavigationStack(path:) — Coordinator ning path ni ishlatadi
//  .navigationDestination — Route → View aylantirish
//  .sheet — modal oynalar
//  .environmentObject — barcha child View larga Coordinator uzatish
//  .onOpenURL — deep link qabul qilish
// ═══════════════════════════════════════════════════════════════
struct ContentView: View {
    @StateObject private var coordinator = AppCoordinator()

    var body: some View {
        // NavigationStack — Coordinator ning path ni boshqaradi
        NavigationStack(path: $coordinator.path) {
            // Bosh sahifa — stack ning "pastki" qismi
            coordinator.view(for: .boshSahifa)
                // .navigationDestination — push qilingan route → View
                .navigationDestination(for: AppRoute.self) { route in
                    coordinator.view(for: route)
                    // Route ni View ga "tarjima qiladi"
                }
        }
        // Sheet — modal oyna (pastdan chiqadi)
        .sheet(item: $coordinator.sheet) { route in
            coordinator.view(for: route)
        }
        // Barcha child View larga Coordinator ni uzatish
        // Istalgan View da @EnvironmentObject var coordinator
        .environmentObject(coordinator)
        // Deep link — tashqi URL qabul qilish
        .onOpenURL { url in
            coordinator.handleDeepLink(url: url)
        }
    }
}


// ═══════════════════════════════════════════════════════════════
//  👁 VIEW — Coordinator ORQALI navigatsiya
//
//  View boshqa View larni bilmaydi!
//  View faqat coordinator.push(.route) deydi
//  Coordinator qaysi View ochishni HAL QILADI
//
//  Bu nimaga yaxshi?
//  1. Navigatsiya tartibini O'ZGARTIRISH oson
//  2. A/B testing — boshqa oqim sinash
//  3. Deep linking — istalgan ekranga tushish
//  4. View lar mustaqil — alohida test qilish mumkin
// ═══════════════════════════════════════════════════════════════
struct BoshSahifaKorinishi: View {
    // @EnvironmentObject — ContentView dan uzatilgan Coordinator
    @EnvironmentObject var coordinator: AppCoordinator

    var body: some View {
        List {
            // Push navigatsiya — stack ga qo'shish
            Button("Maqolaga o'tish") {
                coordinator.push(.maqolaTafsiloti(maqolaId: 1))
                // Stack: [.boshSahifa, .maqolaTafsiloti(1)]
            }

            Button("Profilni ochish") {
                coordinator.push(.profil)
                // Stack: [.boshSahifa, .profil]
            }

            // Sheet navigatsiya — modal oyna
            Button("Sozlamalar (sheet)") {
                coordinator.present(.sozlamalar)
                // Sheet ochiladi — back button yo'q, swipe down bilan yopiladi
            }

            // Full screen — to'liq ekranli modal
            Button("Yangi maqola (fullscreen)") {
                coordinator.presentFullScreen(.maqolaMuharriri(maqolaId: nil))
                // nil = yangi maqola yaratish
            }
        }
        .navigationTitle("Bosh sahifa")
    }
}

Coordinator diagrammasi

                    ┌──────────────┐
AppCoordinator│
                    │              │
                    │ path: [...]                    │ sheet: ?                    └──────┬───────┘
              ┌────────────┼────────────┐
              ▼            ▼            ▼
      ┌──────────┐  ┌──────────┐  ┌──────────┐
BoshSahifa│Tafsilot │  │ Profil        (push)  (push) (sheet)      └──────────┘  └──────────┘  └──────────┘

Navigatsiya oqimi:
View → coordinator.push(.route)Coordinator → path.appendNavigationStack

AppRoute ni Hashable va Identifiable qilish

// Sheet uchun Identifiable kerak
extension AppRoute: Identifiable {
    var id: String {
        switch self {
        case .boshSahifa: return "boshSahifa"
        case .maqolaTafsiloti(let id): return "maqola-\(id)"
        case .profil: return "profil"
        case .sozlamalar: return "sozlamalar"
        case .maqolaMuharriri(let id): return "muharrir-\(id?.description ?? "yangi")"
        }
    }
}

🎯 Topshiriq: Coordinator qo'shish

Ilovangizga AppCoordinator qo'shing. Kamida 4 ta ekran Route ni aniqlang. Bosh sahifada tugmalar orqali push, sheet va fullscreen navigatsiyani sinab ko'ring. Deep link handler qo'shing — URL dan ekranga o'tishni amalga oshiring.

Buy mea coffee