- Published on
SwiftUI-da NavigationStack'dan qanday foydalanish kerak
- Authors
- Name
- ShoxruxC
- @iOSdasturchi
Hammaga salom, xush kelibsiz! Men — Nik, bu Swiftful Thinking, va biz yana SwiftUI bootcamp pleylistiga qaytdik. Bu — men kanalimda taxminan ikki yil oldin chiqargan original pleylist, va shundan beri SwiftUI'da juda ko'plab yangilanishlar bo'ldi. Shuning uchun men ushbu pleylistni qaytadan ko'rib chiqib, hali qamrab olmagan ba'zi yangi xususiyatlar haqida bir nechta video qo'shishni o'yladim. iOS 16 taxminan bir yil oldin chiqgan edi, ammo u yerda men sizlarga albatta ulashmoqchi bo'lgan juda qiziqarli xususiyatlar bor — buni keyingi bir necha videoda qilaman. Shuningdek, ushbu pleylistni birinchi marta yaratganimdan beri eskirgan (deprecated) bo'lib qolgan bir nechta narsa ham bor — men buning yangi versiyasi nima ekanligi haqida ko'plab izoh oldim, shuning uchun bu eskirgan narsalarning yangi yo'lini ko'rsatuvchi bir nechta video ham qo'shaman, va nega ular eskirgan bo'lib qolganini ham tushuntiraman — chunki bu Apple'ning shunchaki API'sini o'zgartirgani uchun emas, balki ular SwiftUI'ni yanada yaxshilab borayotgani uchun: eskirgan narsa, aslida, ishlamay qolgani uchun emas, balki buni qilishning yangi va yaxshiroq usuli mavjud bo'lgani uchun eskirgan bo'lib qoladi.
Ushbu video esa, bilganingizdek, NavigationStack haqida. iOS 16'dan kelgan, ehtimol, eng katta yaxshilanish — aynan shu NavigationStack. iOS 16'dan oldin bizda NavigationView bor edi, va u hamon kodimizda ishlayveradi, ammo agar biz hozir SwiftUI ilovalar qurayotgan bo'lsak va iOS 16 yoki undan yuqorisini qo'llab-quvvatlay olsak, biz NavigationView o'rniga NavigationStack'dan foydalanishimiz kerak. Ushbu videoda avval, albatta, uni qanday ishlatishni tushuntiraman, ammo shuningdek, Stack'ning View'ga nisbatan afzalliklari haqida ham gaplashaman. Hazillashmayman: bu haqiqatan ham juda kuchli — NavigationStack bizga ilgari bo'lmagan juda ko'plab yangi xususiyatlarga yo'l ochib beradi. Shuni hozircha qamrab olmaganim uchun uzr so'rayman — bu davrda Firebase haqida ko'plab boshqa video qilishni xohlardim, ammo agar buni hali o'rganmagan bo'lsangiz, umid qilamanki, bu video birovga yordam beradi. Men hozirgi paytda barcha ilovalarimda NavigationStack'dan foydalanaman — bu ajoyib. Keling, ko'rib chiqaylik.
NavigationView'ni eslab o'tish
Avval NavigationStackBootcamp nomli yangi SwiftUI View fayl yaratamiz. Mavjud matnni o'chirib, NavigationView (ushbu pleylistning oldingi videosida o'rgangan eski navigation view) qo'shamiz — va ko'ramiz: bu, kelajakdagi iOS versiyasida eskirib qolishi haqida ogohlantirish chiqaradi. Demak, Apple bizga shuni aytmoqda: hozircha NavigationView'dan foydalanishingiz mumkin, ammo bu eskirib boradi — kelajakda buni umuman ishlatib bo'lmaydigan vaqt keladi (garchi bu, ehtimol, juda uzoq vaqtdan keyin bo'lsa ham, chunki Apple App Store'dagi hozirgi ilovalarni "buzib" qo'yishni xohlamaydi, shuning uchun ular buni hech bo'lmasa yaqin kelajakda ishlaydigan holatda saqlab qoladi). Shunga qaramay, biz iloji boricha tezroq NavigationStack'ga o'tishimiz kerak.
(Yana NavigationSplitView ham bor, ammo men buni qamrab olmayman — men buni shaxsan hech qachon ishlatmaganman; bu ko'proq iPad va kattaroq qurilmalarda keng tarqalgan, deb o'ylayman. Ammo NavigationStack juda ko'p narsa taklif etadi, va men deyarli barcha ilovalarimda aynan shundan foydalanaman.)
Avval NavigationView qanday ishlashini ko'rib chiqaylik, so'ngra NavigationStack'ga o'tamiz. NavigationView bizni navigation hierarchy (navigatsiya tartibiga) kiritadi, va shu tartibda bo'lganimizdan keyin, matn qo'shishimiz mumkin — masalan, "Hello world". Shu hierarchy ichida bo'lganimiz uchun, .navigationTitle("Nav Bootcamp") ham qo'shishimiz mumkin — eslatib o'taman: NavigationView ichida bo'lmasak, bu sarlavhani va yuqoridagi nav-bar'ni olishimiz mumkin emas:
NavigationView {
Text("Hello world")
.navigationTitle("Nav Bootcamp")
}
NavigationView ichida, keyingi ekranga o'tish uchun NavigationLinkdan foydalanamiz. Tezda buni qo'shaylik: NavigationLink, destination va labelga ega variantdan foydalanamiz. Destination uchun, masalan, Text("Screen 2")ni qo'yamiz, label uchun esa Text("Click me") deb yozamiz:
NavigationLink(destination: Text("Screen 2")) {
Text("Click me")
}
Bosganimda, keyingi ekranga o'tadi, va orqaga ham qaytishimiz mumkin — bu bizning standart xulq-atvorimiz.
NavigationView'ning cheklovlari
Davom etishdan oldin, hozirgi sozlamadagi bir nechta cheklov haqida gaplashmoqchiman. Avvalo, bu NavigationLink bizga faqat bir ekranga o'tishga ruxsat beradi — men keyingi ekranga o'ta olaman, ammo to'g'ridan-to'g'ri uchinchi yoki to'rtinchi ekranga o'ta olmayman, bir vaqtning o'zida bir nechta ekranga o'ta olmayman. NavigationStack esa bizga buni qilish imkonini beradi.
Ammo, ehtimol, undan ham muhimroq narsa — NavigationStack bizga ikkinchi darajali ekranlarni lazy (kerak bo'lganda) yuklash imkonini beradi. "Lazy loading" nima ekanligini hammangiz bilmasligingiz mumkin, shuning uchun tezkor bir misol qilib ko'rsataman.
Avval ikkinchi ekran uchun struct yarataman — MySecondScreen deb ataymiz, bu View bo'ladi, va unga Int turidagi value qiymatini parametr sifatida beramiz. Hozircha maxsus init ham yaratamiz — bu init ichida, qaysi ekran ishga tushirilayotganini bilish uchun, shunchaki "Init for screen \(value)" deb konsolga chop etamiz, va shu qiymatni ekranga ham chiqaramiz, shunda biz to'g'ri ekranga borayotganimizni bilamiz:
struct MySecondScreen: View {
let value: Int
init(value: Int) {
self.value = value
print("Init for screen \(value)")
}
var body: some View {
Text("Screen \(value)")
}
}
Endi tezkor bir ForEach qo'shamiz: 0dan 10dan kichik qiymatgacha bo'lgan oraliqda, har bir element uchun NavigationLink qo'yamiz — bu yerda biz 10 elementdan iborat sodda bir tsikl yaratamiz, har biri o'z NavigationLinkiga ega, va har biri o'z x qiymatiga mos ekranga olib boradi. Buni ScrollView va VStack ichiga (bo'shlig'i 40) joylaymiz:
NavigationView {
ScrollView {
VStack(spacing: 40) {
ForEach(0..<10) { x in
NavigationLink(destination: MySecondScreen(value: x)) {
Text("Click me \(x)")
}
}
}
}
.navigationTitle("Nav Bootcamp")
}
Endi ekranimda "Click me 0", "Click me 1" va hokazo tugmalar bor — buni bossam, mos ekranga o'taman, bu standart xulq-atvor.
Endi shu view'ni ilovamning birinchi ekrani qilib belgilaymiz, va simulyatorda ishga tushirib ko'ramiz. Hech qanday tugmani bosishdan oldin, konsolimga qarasam, barcha bu ekranlarning initi allaqachon ishga tushirilganini ko'raman — men ulardan hech birini bosmagan bo'lsam ham, masalan ettinchi, sakkizinchi, to'qqizinchi ekranning init'i allaqachon ishlagan (har biri ikki marta — bu, view qanday render qilinishi bilan bog'liq, xavotirlanarli emas). Ammo meni xavotirga soladigan narsa shu: men hozircha shunchaki birinchi ekrandaman, ammo to'qqiztasi ham allaqachon "sahna ortida" render qilingan.
Agar mening crypto ilovam haqidagi kursni kuzatib borgan bo'lsangiz, yoki iOS 16'dan oldin biror ilova yaratgan bo'lsangiz, ehtimol shu muammoga duch kelgan bo'lishingiz mumkin — chunki bu navigation link'lar lazy emas. Agar bu ikkinchi ekran og'ir bo'lsa — masalan, unda ko'plab yuklab olish (download) funksiyalari bo'lsa — biz hozircha "sahna ortida" qo'shimcha to'qqiz ekranni yuklab olayapmiz. Kattaroq ilovada bu butun ilovani sezilarli darajada sekinlashtirib qo'yishi mumkin — biz esa faqat kerak bo'lgan ekranlarni, kerak bo'lganda yuklashni xohlaymiz.
Aynan shu sababdan, NavigationStack'ga o'tishning eng katta foydalaridan biri — bularni lazy tarzda amalga oshirish imkoniyati, shunda biz ekranni faqat unga o'tganimizda yuklaymiz.
NavigationStack'ga o'tish
NavigationStack'dan foydalanish, aslida, juda sodda — shunchaki NavigationView o'rniga NavigationStackdan foydalanishimiz kerak, xolos. Bizning mavjud NavigationLink'larimiz esa hozircha o'z holicha ishlayveradi:
NavigationStack {
ScrollView {
VStack(spacing: 40) {
ForEach(0..<10) { x in
NavigationLink(destination: MySecondScreen(value: x)) {
Text("Click me \(x)")
}
}
}
}
.navigationTitle("Nav Bootcamp")
}
NavigationStack faqat iOS 16'dan boshlab mavjud, shuning uchun butun loyihamni iOS 16 uchungina kompilyatsiya qilinadigan qilib o'zgartiraman (bu shunchaki o'quv loyihasi bo'lgani uchun shunday qilaman). Simulyatorda yana ishga tushirsak, hamma narsa hamon ishlayotganini ko'ramiz.
Yana ishga tushirsak, hamon o'sha init muammosi bor — chunki bu NavigationLink'lar hamon lazy emas. Ammo bilib qo'yish foydali: ilovamizni yangilashni boshlaganimizda, hozirgi sozlamani darhol qaytadan qurmasdan ham ishlatib turishimiz mumkin. Demak, agar bu kod sizning ilovangizda allaqachon mavjud bo'lsa, uni o'z holicha qoldirib, shunchaki NavigationStack'ga o'tishingiz, va asta-sekin NavigationLink'larni navigationDestinationlarga ko'chirib borishingiz mumkin.
navigationDestination: lazy yuklashning kaliti
Avval navigationTitleni biroz pastga ko'chiraman, chunki u bilan hozir ishlamoqchi emasman. Xo'sh, navigationDestination nima o'zi? Bu shunchaki yana bir modifikator, xolos — .navigationDestination deb chaqiramiz, va for destinationga ega variantdan foydalanamiz (bu, menimcha, boshqasiga qaraganda ancha ko'p ishlatiladi). Bu bizga ma'lum bir ma'lumot turi uchun destination view'ga o'tish imkonini beradi, va bu ma'lumot turi juda muhim — Option tugmasini bosib qarasak: bu Hashable protokoliga mos keladigan istalgan tur bo'lishi mumkin. Ko'pchilik turlar standart bo'yicha allaqachon Hashable'ga mos keladi (agar o'z maxsus modelingizni yaratsangiz, uni Hashable'ga moslashtirishingiz kerak bo'ladi), ammo hozircha biz shunchaki butun sonlardan (Int) foydalanamiz: Int.self deb yozamiz — bu yerda self butun son turining o'ziga ishora qiladi.
Destination — bu, asosan, biz qayerga borishni xohlaymiz, degani, va, albatta, biz MySecondScreenga borishni xohlaymiz. Bu Hashable qiymat — bu turning tanlangan qiymati, xolos: agar to'rtinchi raqamga o'tsak, aynan shu qiymat shu yerga keladi, shuning uchun buni value deb ataymiz, so'ngra MySecondScreen(value: value)ga o'tamiz.
Endi destination va label'ga ega NavigationLink'dan foydalanish o'rniga, boshqa initializerdan foydalanamiz — NavigationLink, valuega ega bo'lgan variant (bu qiymat ham Hashable, va bu qiymat yuqoridagi bilan bir xil turda bo'lishi kerak). Demak, "Click me"ni bosganimizda, qiymat — bu x bo'ladi, ya'ni biz uzatayotgan qiymat. Label uchun esa avvalgi matnimizni qoldiramiz:
NavigationStack {
ScrollView {
VStack(spacing: 40) {
ForEach(0..<10) { x in
NavigationLink(value: x) {
Text("Click me \(x)")
}
}
}
}
.navigationDestination(for: Int.self) { value in
MySecondScreen(value: value)
}
}
.navigationTitle("Nav Bootcamp")
Kompilyatsiya qilib ko'raylik — kodimizga shunchaki kichik o'zgartirish kiritdik, va UI'da bu xuddi avvalgisi bilan bir xil ko'rinadi: uchinchisini bossam, uchinchi ekranga o'taman, yettinchisini bossam — yettinchisiga. Ammo bu safar simulyatorda ishga tushirib, konsolni ochsam — hech narsa chop etilmagan! Tashqi ko'rinishi bir xil bo'lsa ham, "sahna ortida" katta farq bor: biz endi to'qqiz ekranning barchasini oldindan yuklamadik. To'rtinchisini bossam, faqat o'sha paytda to'rtinchisi yuklanadi; yettinchisini bossam — faqat o'sha paytda yettinchisi.
Va shu birgina o'zgarish, NavigationStack'ni NavigationView'dan ustun qilib qo'yadi — menimcha, bu SwiftUI va iOS 16'ga kiritilgan, kam baholanган, ammo eng katta yutuq. Navigatsiyaning shu tarzda lazy bo'lishi, production darajasidagi, kengaytiriladigan ilovalar uchun juda muhim. Agar sizning ilovangizda bu hali shunday qilinmagan bo'lsa, buni birinchi navbatda qayta qurishni qattiq tavsiya qilaman. Ammo, albatta, NavigationStack bilan qilishimiz mumkin bo'lgan narsalar bundan ham ko'proq.
Bir nechta ma'lumot turi uchun bir nechta destination
Keling, buni biroz davom ettiraylik. Yuqorida let fruits, turi — [String], qiymatlari esa "apple", "orange", "banana" bo'lgan massiv qo'shamiz. Endi shu mevalar ustida ham tsikl yuritaylik — buni yuqoriroqqa, alohida ForEach sifatida qo'shamiz.
Mevalar identifiable emas, ammo ular hashable, shuning uchun id parametrini qo'shishimiz kerak: hash qiymati — bu \.self, ya'ni satrning o'zi. Har bir element uchun esa, yana bir NavigationLink qo'shamiz — yana valuega ega variantdan foydalanamiz, qiymat sifatida shu mevani beramiz, label uchun esa shu meva nomini matn qilib qo'yamiz:
let fruits: [String] = ["apple", "orange", "banana"]
ForEach(fruits, id: \.self) { fruit in
NavigationLink(value: fruit) {
Text(fruit)
}
}
Canvas'ni yangilasak, ekranda "apple", "orange", "banana"ni ko'ramiz, ammo birortasini bossak, hech qayoqqa o'tmaydi. Buni aytishdan oldin, kodga biroz e'tibor bering va o'zingiz tushunib ko'ring: nega "Click me 0", "1", "9" mukammal ishlayapti, ammo "orange" va "banana" ishlamayapti?
Sababi shu: navigationDestination qo'shganimizda, biz unga "butun sonlarni (Int) izla, va Int kelganida shu ekranga o't" deb aytgan edik. Ammo bu mevalar — stringlar, ular endi butun son emas, shuning uchun biz string'lar uchun yana bir navigationDestination qo'shishimiz kerak. Qaysi destination'ga borishimiz kerak? Buning uchun yana bir ikkinchi ekran yaratishimiz shart emas — shunchaki "Another screen" deb yozilgan matn qo'yamiz, va shu mevaning qiymatini ham qo'shamiz (albatta, bu safar bu — string):
.navigationDestination(for: String.self) { fruit in
Text("Another screen: \(fruit)")
}
Bu juda qiziq va dasturchi sifatida biroz kutilmagan: biz, aslida, bu navigationDestinationlarni ustma-ust qatlamlay olamiz — bir nechta turli ma'lumot turi uchun bir nechta destination'ga ega bo'lishimiz mumkin. Bu, kengaytiriladigan ilovalar uchun juda foydali — chunki sizda, masalan, foydalanuvchi profili modeli, post modeli, sozlamalar ekrani bo'lishi mumkin, va endi biz routing'imizni haqiqatan ham dinamik qilishni boshlashimiz mumkin. Men shu string destination'ni qo'shdim, va endi olma, apelsin, banan ekranlariga, shuningdek butun son ekranlariga ham o'tishim mumkin. Bu, NavigationView imkoniyatlariga nisbatan juda katta qadam.
Path bog'lash (binding)
Videoni yakunlashdan oldin, qilishim kerak bo'lgan so'nggi narsa — NavigationStack'da bog'lashimiz mumkin bo'lgan path haqida gaplashish. NavigationStack qurganimizda, unda path bog'lash (binding) ham bor, va bu path — NavigationPathga bog'langan Binding turida. "NavigationPath" nomi biroz chalg'ituvchi, chunki, aslida, bu Hashable bo'lgan deyarli har qanday narsa bo'lishi mumkin — masalan, bizning bir destination'imizda path Int, boshqasida esa String. Bu birinchi ishlatganda biroz chalkash bo'lib tuyulishi mumkin, ammo bu, aslida, shunchaki bir binding, xolos, va undan keyin closure keladi.
Shu kodni o'chirib, shu path'dan foydalanamiz — nimaga bog'laymiz? Yuqorida @State private var stackPath, turi — hozircha string'lar bilan ishlaylik (chunki bizda allaqachon meva massivimiz bor), shuning uchun [String] massiviga bog'laymiz, va boshlang'ich qiymat sifatida bo'sh massiv beramiz. Buni dollar belgisi bilan bog'laymiz: $stackPath:
@State private var stackPath: [String] = []
NavigationStack(path: $stackPath) {
...
}
Endi, ko'rinishidan, biz unchalik ko'p kod qo'shmadik, ammo bu, aslida, ilovamizning haqiqiy ishlashini sezilarli darajada o'zgartirdi. Bu NavigationStack — ya'ni butun stack — endi string qiymatlari uchun destination'larga bog'lanadi: bu yerda faqat string'lar bor, butun son yo'q. Demak, bizda butun son uchun destination ham, string uchun destination ham bo'lsa-da, butun son destination'i endi buzilib qoladi, chunki path boshqa barcha destination'larni "ustidan bosib qo'yadi". Agar "orange"ni bossam, bu ishlaydi, chunki bular string'lar; ammo "4" raqamini bossam, bu endi ishlamaydi. Buni isbotlash uchun: agar shu path'ni olib tashlab, "4"ni bossam — ishlaydi; path'ni qaytarib qo'yib, "4"ni bossam — yana buziladi. Demak, bu xato emas — bu shunchaki shunday ishlaydi, deb bilib qo'yish kerak.
Xo'sh, agar bu bizni shu yo'nalishda cheklab qo'ysa, nega umuman stackPathdan foydalanishimiz kerak? Buning sababi shu: path'ni bog'lab qo'yganimizda, biz endi shu path'ga dasturiy ravishda (programmatically) elementlar qo'sha olamiz. Bir string'ni bosganimizda, bu, aslida, shu string'ni shu path'ga qo'shish, xolos — masalan, "banana"ni bossam, stackPath — "banana" qiymatiga ega bo'lgan, bittadan elementdan iborat massiv bo'ladi.
Ammo men yuqorida o'zimning maxsus tugmamni yarataman — "Super segue" deb ataymiz, va harakat sifatida stackPath.append("coconut")ni chaqiraman:
Button("Super segue") {
stackPath.append("coconut")
}
Shu tugmani bosganimda, "coconut"ga bog'lanadi. Ammo nega men buni "Super Segway" deb ataganman? Chunki biz, aslida, shu path'ga butun bir massivni qo'sha olamiz — append(contentsOf:)dan foydalanib, masalan "coconut", "watermelon", "mango"ni birga qo'shamiz:
Button("Super segue") {
stackPath.append(contentsOf: ["coconut", "watermelon", "mango"])
}
Bu juda ajoyib — iOS 15'da bunday qilish imkonimiz yo'q edi, ammo NavigationStack bizga endi bir vaqtning o'zida bir nechta ekranga o'tish imkonini beradi. Bu kod shu uchta qiymatning barchasini path'ga qo'shadi, ya'ni go'yo bir vaqtning o'zida uchta ekranni "bosib" yuboradi. "Super segue"ni bossam, men endi "Mango" ekranidaman — bu uchinchi ekran, va orqaga bossam, dastlabki ekranga emas, avval "Watermelon"ga, so'ngra "Coconut"ga, va shundan keyingina dastlabki ekranga qaytaman.
Agar siz hozirgina o'rganayotgan, oddiy ilovalar yaratayotgan bo'lsangiz, bu, ehtimol, sizga tez-tez kerak bo'lmaydigan bir real holat. Ammo kattaroq, kengaytiriladigan ilovalar, yirik kompaniyalar uchun bu sozlama zarur bo'lib qoladi — bu sizga bir vaqtning o'zida bir nechta ekranni path'ga "bosib" yuborish imkonini beradi, va bu, ba'zi ilovalar uchun haqiqatan ham hal qiluvchi ahamiyatga ega bo'lgan holatlar mavjud.
Xulosa
Mana shu — NavigationStack haqida sizlarga aytmoqchi bo'lgan barcha narsa. Bu stackPath'ni qayta belgilab, bir nechta ma'lumot turini qabul qilishi uchun moslashtirishning ham bir yo'llari bor, ammo bu ancha murakkabroq va "hackier" (biroz noaniq usulda qilingan), shuning uchun men buni ushbu pleylistda batafsil ko'rib chiqmayman. Hozircha, menimcha, bu boshlang'ich va o'rta darajadagi ilovalaringizning deyarli barcha holatlarini qamrab oladi. Shaxsan men, super segue qilishim kerak bo'lmaganda, odatda path'dan umuman foydalanmayman — va, rostini aytsam, ko'pchilik ilovalarga bu kerak emas, shuning uchun men buni ko'pincha ishlatmay qo'yganman. Ammo agar shu binding sizda bo'lsa, super segue qilishingiz mumkin bo'ladi. Yana bir qiziq narsa: bu sizga view'ni allaqachon oldindan o'rnatilgan path bilan boshlash imkonini ham beradi — masalan, agar xohlasangiz, onAppearda yoki shunchaki yuqorida, boshlang'ich qiymat sifatida bir, ikki, uch kabi yozib qo'yishingiz mumkin, va ekran yuklangan paytda, u allaqachon uchinchi ekranda boshlanadi. Bu bilan juda moslashtirilgan narsalar qilish mumkin.
Ammo bu mendan shu, xolos. Yakuniy xulosa shu: agar NavigationView'dan foydalanayotgan bo'lsangiz, hamon ishlataverishingiz mumkin, ammo NavigationStack'ga o'tish yo'liga tushib olishni tavsiya qilaman — bu, oddiygina, yaxshiroq, yangiroq, samaraliroq, va, rostini aytsam, ishlatish jihatidan ham osonroq. Chunki, masalan, agar mening crypto ilovam haqidagi kursni kuzatib borgan bo'lsangiz, biz o'zimizning maxsus "lazy" navigatsiyamizni yaratish uchun juda ko'p ish qilgan edik (men hozircha o'sha kursni o'zgartirmoqchi emasman, menimcha bu faqat bir ekranda qilingan edi) — ammo NavigationStack bizga aynan shu turdagi "hackiness"ning hojatini qoldirmasdan, lazy navigation destination'larni to'g'ridan-to'g'ri "qutidan chiqarib" beradi.
Tomosha qilganingiz uchun rahmat. Har doimgidek, men — Nik, bu Swiftful Thinking, keyingi videoda ko'rishamiz!