03. MVVM + Coordinator — Navigatsiya Boshqaruvi
Arxitektura
iOS Arxitektura 3: MVVM + Coordinator — Navigatsiya Boshqaruvi va Ekranlar Orasidagi O'tish
Holat: Ushbu dars 2026-yil oktabr holatiga ko'ra eng dolzarb iOS arxitektura amaliyotlari (xususan, UIKit va MVVM-C naqshi) asosida tayyorlangan. Swift 6 va iOS 17+ imkoniyatlaridan to'liq foydalanilgan.
Mundarija
- Navigatsiya muammosi
- Coordinator nima?
- Coordinator Protocol
- AppCoordinator
- Child Coordinator'lar
- ViewModel -> Coordinator aloqasi
- Amaliy loyiha: To'liq MVVM + Coordinator app
- TabBar bilan Coordinator
- Deep Linking bilan Coordinator
- Coordinator kamchiliklari
- Xulosa
- Manbalar
1. Navigatsiya muammosi {#navigatsiya-muammosi}
Klassik MVC yoki oddiy MVVM arxitekturasida bitta katta muammo bor: navigatsiya boshqaruvi (Routing) asosan ViewController ichida amalga oshiriladi.
Faraz qilaylik, bizda HomeViewController bor va u foydalanuvchini DetailViewController ga o'tkazishi kerak.
// Antipattern: Yomon amaliyot
class HomeViewController: UIViewController {
func didSelectUser(_ user: User) {
let detailVC = DetailViewController()
detailVC.user = user // Tight coupling!
self.navigationController?.pushViewController(detailVC, animated: true)
}
}
Bu yondashuvning muammolari:
- Tight Coupling (Qattiq bog'liqlik):
HomeViewControllerendiDetailViewControllerhaqida to'liq biladi (uni qanday yaratish, unga nima kerakligi). Ular bir-biriga qattiq bog'lanib qolgan. - Qayta foydalanish (Reusability): Agar
DetailViewControllerni boshqa joydan chaqirmoqchi bo'lsak, yana shu kodni takrorlashimiz kerak. - ViewModel UIKit'ni bilmasligi kerak: Agar biz navigatsiyani
ViewModelichiga olib kirsak, u UIKit (UINavigationController) ga bog'lanib qoladi. ViewModel UI elementlaridan xoli bo'lishi shart!
Yechim: Navigatsiyani alohida ob'ektga — Coordinator'ga topshirish.
2. Coordinator nima? {#coordinator-nima}
Coordinator (Muvofiqlashtiruvchi) — bu butun ilova yoki uning bir qismidagi navigatsiyani va oqimni (flow) boshqaradigan alohida ob'ekt.
Coordinator yondashuvida ViewController'lar bir-birini MUTLAQO BILMAYDI. Ularning yagona vazifasi — ma'lumotni ko'rsatish (UI) va foydalanuvchi harakatlarini ViewModel ga yetkazish. ViewModel esa kerak bo'lganda Coordinator orqali keyingi ekranga o'tishni so'raydi.
Coordinator arxitekturasi daraxti:
Coordinator Ierarxiyasi
Senior eslatmasi: Har doim ham har bir ekran uchun alohida Coordinator ochish shart emas. Kichikroq oqimlar uchun bitta Coordinator bir nechta ekranni boshqarishi mumkin (masalan,
HomeCoordinatorham ro'yxatni, ham detail ekranni ko'rsatishi mumkin).
3. Coordinator Protocol {#coordinator-protocol}
App'dagi barcha Coordinator'lar bir xil shablonga ega bo'lishi uchun, biz Coordinator protokolini yaratamiz.
import UIKit
@MainActor
protocol Coordinator: AnyObject {
/// O'zidan kichik coordinator'larni ushlab turish uchun array (Memory management uchun muhim!)
var childCoordinators: [Coordinator] { get set }
/// Ushbu coordinator boshqaradigan asosiy navigation controller
var navigationController: UINavigationController { get }
/// Coordinator o'z ishi va navigatsiyani boshlashi uchun metod
func start()
/// Coordinator o'z ishini tugatganda uni tozalash
func finish()
}
extension Coordinator {
func finish() {
childCoordinators.removeAll()
}
func addChild(_ coordinator: Coordinator) {
childCoordinators.append(coordinator)
}
func removeChild(_ coordinator: Coordinator) {
childCoordinators.removeAll { $0 === coordinator }
}
}
Muhim:
childCoordinatorsmassiviCoordinatorob'ektlarini xotirada ushlab turish (retain) uchun juda muhim. Aks holda ular ARC tomonidan o'chirib yuboriladi va navigatsiya buziladi.
4. AppCoordinator {#appcoordinator}
AppCoordinator — ilovaning boshlanish nuqtasi. U UIWindow ni oladi va foydalanuvchi holatiga qarab (masalan, avtorizatsiyadan o'tganmi yoki yo'q) tegishli child coordinator'ga yo'naltiradi.
import UIKit
@MainActor
final class AppCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
private let window: UIWindow
init(window: UIWindow) {
self.window = window
self.navigationController = UINavigationController()
}
func start() {
window.rootViewController = navigationController
window.makeKeyAndVisible()
let isLoggedIn = UserDefaults.standard.bool(forKey: "isLoggedIn")
if isLoggedIn {
showHomeFlow()
} else {
showAuthFlow()
}
}
private func showAuthFlow() {
// AuthCoordinator ni yaratamiz va ishga tushiramiz
// To'liq implementatsiyasi keyingi qismlarda ko'rsatilgan
}
private func showHomeFlow() {
// HomeCoordinator ni yaratamiz va ishga tushiramiz
}
}
SceneDelegate da qanday chaqiriladi:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
appCoordinator = AppCoordinator(window: window)
appCoordinator?.start()
}
}
5. Child Coordinator'lar {#child-coordinators}
Endi biz o'ziga xos flow'larni (oqimlarni) yaratamiz.
AuthCoordinator: Faqatgina Login va Register ekranlarini boshqaradi.
import UIKit
@MainActor
protocol AuthCoordinatorDelegate: AnyObject {
func didFinishAuth(_ coordinator: AuthCoordinator)
}
@MainActor
final class AuthCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
weak var delegate: AuthCoordinatorDelegate?
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
showLogin()
}
private func showLogin() {
let viewModel = LoginViewModel()
// ViewModel -> Coordinator aloqasi closure orqali
viewModel.onLoginSuccess = { [weak self] in
self?.delegate?.didFinishAuth(self!)
}
viewModel.onRegisterRequested = { [weak self] in
self?.showRegister()
}
let vc = LoginViewController(viewModel: viewModel)
navigationController.setViewControllers([vc], animated: false)
}
private func showRegister() {
// Register ekranini ko'rsatish
print("Register ekrani ochildi")
}
}
6. ViewModel -> Coordinator aloqasi {#vm-coordinator-aloqasi}
ViewModel va Coordinator bir-biri bilan qanday gaplashadi? ViewModel navigatsiya qilish kerakligini biladi, lekin qanday qilishni emas. Buning uchun asosan 2 ta usul ishlatiladi:
- Delegate Protocol: ViewModel delegate protokoli e'lon qiladi, Coordinator unga conform bo'ladi.
- Closures (Callback): ViewModel o'zida block/closure e'lon qiladi, Coordinator uni to'ldiradi. (Biroz qulayroq va qisqaroq).
MVVM + Coordinator O'zaro Aloqa Oqimi
7. Amaliy loyiha: To'liq MVVM + Coordinator app {#amaliy-loyiha}
Quyida to'liq ishlaydigan va kompilyatsiya bo'ladigan Home flow kodi keltirilgan. Model, ViewModel, ViewController va Coordinator to'liq bog'langan.
import UIKit
// MARK: - 1. Model
struct User: Identifiable {
let id: UUID = UUID()
let name: String
let email: String
}
// MARK: - 2. ViewModels
@MainActor
final class UserListViewModel {
// Navigatsiya uchun callback'lar (Coordinator to'ldiradi)
var onUserSelected: ((User) -> Void)?
var onLogout: (() -> Void)?
// UI update uchun closure
var onDataUpdated: (() -> Void)?
private(set) var users: [User] = []
func fetchUsers() {
// Fake tarmoq so'rovi
self.users = [
User(name: "Alisher Navoiy", email: "alisher@navoiy.uz"),
User(name: "Zahiriddin Bobur", email: "bobur@andijon.uz")
]
onDataUpdated?()
}
func selectUser(at index: Int) {
let user = users[index]
onUserSelected?(user) // Coordinator'ga xabar beramiz
}
func logout() {
onLogout?() // Coordinator'ga xabar beramiz
}
}
@MainActor
final class UserDetailViewModel {
let user: User
init(user: User) {
self.user = user
}
}
// MARK: - 3. ViewControllers
final class UserListViewController: UIViewController {
private let viewModel: UserListViewModel
private let tableView = UITableView()
init(viewModel: UserListViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
viewModel.fetchUsers()
}
private func setupUI() {
title = "Foydalanuvchilar"
view.backgroundColor = .systemBackground
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Chiqish", style: .plain, target: self, action: #selector(logoutTapped))
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func bindViewModel() {
viewModel.onDataUpdated = { [weak self] in
self?.tableView.reloadData()
}
}
@objc private func logoutTapped() {
viewModel.logout()
}
}
extension UserListViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = viewModel.users[indexPath.row].name
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
viewModel.selectUser(at: indexPath.row)
}
}
final class UserDetailViewController: UIViewController {
private let viewModel: UserDetailViewModel
init(viewModel: UserDetailViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
title = viewModel.user.name
let label = UILabel()
label.text = viewModel.user.email
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
// MARK: - 4. HomeCoordinator
@MainActor
protocol HomeCoordinatorDelegate: AnyObject {
func didLogout(_ coordinator: HomeCoordinator)
}
@MainActor
final class HomeCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
weak var delegate: HomeCoordinatorDelegate?
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let viewModel = UserListViewModel()
// 1. Detail ekranga o'tish marshruti
viewModel.onUserSelected = { [weak self] user in
self?.showUserDetail(user)
}
// 2. Logout marshruti
viewModel.onLogout = { [weak self] in
guard let self = self else { return }
self.delegate?.didLogout(self)
}
let vc = UserListViewController(viewModel: viewModel)
navigationController.setViewControllers([vc], animated: true)
}
private func showUserDetail(_ user: User) {
let viewModel = UserDetailViewModel(user: user)
let vc = UserDetailViewController(viewModel: viewModel)
navigationController.pushViewController(vc, animated: true)
}
}
Bu amaliyotda ko'rganingizdek, UserListViewController qanday qilib Detail ekranga o'tish haqida hech narsa bilmaydi. U faqat ViewModel'ga selectUser qilinganini xabar qiladi. ViewModel esa onUserSelected closure'ni chaqiradi. Coordinator haqiqiy pushViewController amalni bajaradi.
8. TabBar bilan Coordinator {#tabbar-coordinator}
Agar ilovangizda TabBar bo'lsa, TabBarCoordinator yaratish juda keng tarqalgan amaliyotdir. TabBar o'z ichida bir nechta NavigationController'larni saqlaydi va har biri alohida Child Coordinator tomonidan boshqariladi.
@MainActor
final class TabBarCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var navigationController: UINavigationController
private var tabBarController: UITabBarController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
self.tabBarController = UITabBarController()
}
func start() {
// Tab 1: Home
let homeNav = UINavigationController()
homeNav.tabBarItem = UITabBarItem(title: "Asosiy", image: UIImage(systemName: "house"), tag: 0)
let homeCoordinator = HomeCoordinator(navigationController: homeNav)
addChild(homeCoordinator)
homeCoordinator.start()
// Tab 2: Profile
let profileNav = UINavigationController()
profileNav.tabBarItem = UITabBarItem(title: "Profil", image: UIImage(systemName: "person"), tag: 1)
let profileCoordinator = ProfileCoordinator(navigationController: profileNav)
addChild(profileCoordinator)
profileCoordinator.start()
tabBarController.viewControllers = [homeNav, profileNav]
// TabBar'ni asosiy qilib ko'rsatamiz
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.pushViewController(tabBarController, animated: false)
}
}
9. Deep Linking bilan Coordinator {#deep-linking}
AppCoordinator Deep Linking (Universal Links yoki URL Schemes) larni boshqarish uchun eng zo'r joy.
Faraz qilaylik, bizga myapp://user/123 degan link keldi.
extension AppCoordinator {
func handleDeepLink(url: URL) {
guard let host = url.host else { return }
if host == "user" {
// ID ni ajratib olamiz
let path = url.path.replacingOccurrences(of: "/", with: "")
if let userId = Int(path) {
// HomeFlow'ga o'tishni va aynan o'sha profilni ochishni so'raymiz
showUserFromDeepLink(id: userId)
}
}
}
}
Muhim: Deep link orqali kelganda, Coordinator'lar zanjiri bo'ylab tegishli coordinator'ni topish va unga marshrutlash vazifasi ancha murakkablashadi. Buning uchun ko'pincha
Routerkabi qo'shimcha yordamchi sinflar kiritilishi mumkin.
10. Coordinator kamchiliklari {#kamchiliklar}
| MVVM + Coordinator afzalliklari | Kamchiliklari |
|---|---|
| Separation of Concerns: View faqat UI, ViewModel faqat logika, Coordinator faqat navigatsiya. | Boilerplate Kod: Juda ko'p yozish kerak (protocol, delegate/closure). Kichik loyihalar uchun over-engineering. |
| Reusability: Bitta Ekranni turli xil oqimlarda (masalan onboarding'da va settings'da) ishlatish oson. | Memory Leaks: childCoordinators to'g'ri tozalanmasa (arraydan o'chirib tashlanmasa), jiddiy memory leak kelib chiqadi. |
| Deep Link yordami: Barcha marshrutlash bir joydan boshqarilgani uchun juda qulay. | SwiftUI integratsiyasi: SwiftUI asosan state-driven navigatsiyani targ'ib qiladi (NavigationStack, navigationDestination), Coordinator pattern esa imperative yondashuv. SwiftUI 4+ da buni Router pattern yordamida zamonaviylashtirish mumkin. |
Xulosa {#xulosa}
Coordinator Pattern — bu katta va o'rta o'lchamdagi iOS loyihalari uchun navigatsiya muammolariga standart echim. U ViewModel'ni UIKit'dan ajratadi va kodni mustaqil ravishda test qilinadigan (testable) modullarga bo'lib yuboradi.
Garchi bir oz boshlang'ich sozlash (boilerplate) talab qilsa-da, arxitekturani masshtablash (scaling) jarayonida Coordinator'ning foydasi o'zini oqlaydi.
Manbalar {#manbalar}
- Soroush Khanlou - "The Coordinator" (Original g'oya muallifi)
- Paul Hudson - How to use the coordinator pattern in iOS apps
- Apple Developer Documentation -
UINavigationController,UIWindow