Published on

SwiftUI-da AppStorage va Transitionlar yordamida foydalanuvchi onboarding'ini boshqarish

Authors

Hammaga salom! Men — Nik, bu Swiftful Thinking. Agar siz hozirgina bizga qo'shilayotgan bo'lsangiz, ushbu videoni pauza qilib, avval butun SwiftUI bootcamp kursining qolgan qismini ko'rib chiqishni tavsiya qilaman, va shundan keyin qaytib kelishingiz mumkin — chunki bu video bootcamp kursining so'nggi videolaridan biri, va buning sababi shu: biz bu yerda oldingi videolarda o'rgangan ko'plab xususiyatlardan foydalanamiz, va men ularning har birining qanday ishlashini qaytadan batafsil tushuntirib o'tirmayman, chunki har bir xususiyat haqida alohida video qilgan edim.

Demak, biz transition va animatsiyalardan, picker va sliderlardan foydalanamiz, va foydalanuvchi go'yo ro'yxatdan o'tayotgandek, soxta (mock) onboarding interfeysi yaratamiz. Shundan so'ng, foydalanuvchi ro'yxatdan o'tgandan keyin, uning barcha ma'lumotini saqlash uchun AppStoragedan foydalanamiz, va nihoyat, foydalanuvchini tizimdan chiqarish (sign out) imkoniyatini qo'shib, jarayonni qaytadan boshlash imkonini yaratamiz.

Bu, ehtimol, butun SwiftUI bootcamp kursidagi eng uzun video — chunki bu yerda biz haqiqatan ham yangi narsa o'rganmaymiz, bu shunchaki ushbu kursda o'rgangan ko'plab bilimlarni amalda qo'llashning mukammal, qo'lda bajariladigan misoli, xolos. Bu ajoyib onboarding tajribasi yaratadi, va umid qilamanki, sizlarga ham yoqadi. Keling, ko'rib chiqaylik.


Loyiha tuzilishi: IntroView yaratish

Yana Xcode'damiz, va bu safar biroz boshqacha narsa qilamiz — ushbu videoda bir nechta turli ekran yaratamiz, menimcha uchta ekran. Shuning uchun shunchaki yangi fayl yaratish o'rniga, avval yangi guruh (group) yaratamiz — guruh, asosan, bir nechta faylni saqlaydigan papka, xolos. Buni Onboarding Views deb ataymiz.

Shu guruhga o'ng tugmani bosib, yangi fayl yaratamiz — bu ekran uchun birinchi SwiftUI View'ni yaratamiz, va buni IntroView deb ataymiz (ya'ni "kirish view'i" — bu ilovamizda yuklanadigan birinchi view, xolos). "Create" tugmasini bosamiz, canvas'da "Resume" tugmasini bosib, hammasi ulanganiga ishonch hosil qilamiz.

Avval ekranga ZStack qo'shib, fon qatlami sifatida radial gradient qo'shamiz — buni biroz chiroyli qilamiz: gradient markazi, boshlang'ich radiusi va tugash radiusi parametrlarini alohida qatorlarga joylashtiramiz, ranglarni esa qizildan ikkita binafsha rangga (biri ochroq, biri to'qroq) o'zgartiramiz. Gradient markazini ekranning markazidan, yuqori chap burchakka (.topLeading) ko'chiramiz, tugash radiusini esa ekranning to'liq balandligiga (UIScreen.main.bounds.height) tenglashtiramiz, boshlang'ich radiusni esa 5 qilib qoldiramiz. Va, albatta, bu rangni "xavfsiz hudud" (safe area) chegaralaridan tashqariga ham chiqarishni xohlaymiz:

var background: some View {
    RadialGradient(
        gradient: Gradient(colors: [
            Color(red: 0.75, green: 0.55, blue: 0.95),
            Color(red: 0.45, green: 0.2, blue: 0.65)
        ]),
        center: .topLeading,
        startRadius: 5,
        endRadius: UIScreen.main.bounds.height
    )
    .ignoresSafeArea()
}

Endi bizda ekranda juda chiroyli fon bor, va shu fonning ustida biz ikki turli view orasida almashtirib turamiz. Buning uchun, avval foydalanuvchi tizimga kirgan yoki kirmaganligini bildiradigan mantiq kerak: agar foydalanuvchi tizimga kirgan bo'lsa, uning profil view'ini ko'rsatamiz, aks holda esa onboarding view'ini ko'rsatamiz.

Foydalanuvchi tizimga kirgan-kirmaganligini aniqlash uchun, AppStorageda saqlanadigan o'zgaruvchidan foydalanamiz (aynan shuning uchun bundan oldingi videoni AppStorage haqida qilgan edim, shuning uchun siz bu qanday ishlashini allaqachon bilishingiz kerak). Kalit (key) sifatida "signed_in" stringini beramiz (bunday kalitlarni yaratganda, ularni har doim kichik harflarda va bo'shliqsiz yozish aqlli yechim — shuning uchun bo'shliq o'rniga pasta chiziqcha ishlatamiz). O'zgaruvchimizni currentUserSignedIn deb ataymiz, turi — Bool, va boshlang'ich qiymat sifatida false beramiz.

AppStorage'ga boshlang'ich qiymat berganimizda, bu juda aqlli ishlaydi: agar "signed_in" kaliti uchun allaqachon saqlangan qiymat bo'lsa, shu qiymat ishlatiladi; agar hech qanday qiymat bo'lmasa, false ishlatiladi. Hozircha bizning ilovamizda bu qiymat hali o'rnatilmagan, shuning uchun false bo'ladi, ammo keyinroq buni truega o'rnatganimizda, ilova ochilganda u har doim truedan boshlanadi.

Endi shu Boolean'dan foydalanib: agar foydalanuvchi tizimga kirgan bo'lsa, biz hali yaratmagan profil view'ini ko'rsatamiz (hozircha shunchaki "Profile View" matnini qo'yamiz), aks holda esa "Onboarding View" matnini ko'rsatamiz:

struct IntroView: View {

    @AppStorage("signed_in") var currentUserSignedIn: Bool = false

    var body: some View {
        ZStack {
            background

            if currentUserSignedIn {
                Text("Profile View")
            } else {
                Text("Onboarding View")
            }
        }
    }
}

Mana shu — bizning kirish view'imiz uchun, asosan, shu, xolos: bizda fonimiz bor, va shu fonning ustiga biz profil view'ini yoki onboarding view'ini joylaymiz.


OnboardingView: holatlar (states) tizimi

Keling, endi onboarding view'idan boshlaylik. Navigator'da bizning Onboarding Views guruhimizga o'tib, o'ng tugmani bosib, yangi fayl yaratamiz — bu SwiftUI View bo'ladi, va buni OnboardingView deb ataymiz. "Create" tugmasini bosamiz, canvas'ni ochamiz — bu view'da ancha ko'p mantiq bo'ladi, shuning uchun avval nima qilishimiz haqida umumiy tasavvur beraman.

Avval @State var onboardingState, turi Int, qiymati 0 bo'lgan o'zgaruvchi yaratamiz — bu holat shunchaki onboarding jarayonining qaysi bosqichida turganimizni bildiradi: boshida turibmizmi, foydalanuvchi ismini kiritayotgan birinchi ekrandami, ikkinchi ekrandami va hokazo.

Boshqa dasturchi ushbu faylni ochganida bu holatlar nimani bildirishini bilishi uchun, shu yerga izoh qo'shamiz (teskari chiziqcha va yulduzcha bilan ko'p qatorli izoh ochib, Enter bosamiz): onboarding holati 0 bo'lganda — bu xush kelibsiz (welcome) ekrani; 1 bo'lganda — foydalanuvchi ismini kirita oladigan ekran; 2 bo'lganda — yoshini kirita oladigan ekran; 3 bo'lganda esa — jinsini kirita oladigan ekran. Bu izohga sarlavha sifatida "Onboarding states:" deb yozib qo'yamiz — endi klaviaturada Option+Command+chap strelka bosib buni yig'ib (fold qilib) qo'yishimiz, kerak bo'lganda esa Option+Command+o'ng strelka bosib qaytadan yoyishimiz mumkin.

Birinchi ekranimiz — xush kelibsiz (welcome) ekrani. Ammo buni boshlashdan oldin, esingizda bo'lsa, bir necha daqiqa oldin IntroView'ga binafsha fon qo'shgan edik, shuning uchun bu ekran allaqachon binafsha fonga ega bo'ladi, oq emas. Shuning uchun preview'da soxta (fake) fon qo'shib, to'qroq fon ustida ishlay olishimiz uchun, shu OnboardingView preview'iga vaqtincha .background(Color.purple) qo'shib qo'yamiz.

Endi view'imizda ZStack qo'shamiz — bunda ikki asosiy qatlam bo'ladi: tarkib (content) qatlami va tugmalar qatlami. Tugmalar qatlami osonroq, shuning uchun shundan boshlaymiz: VStack qo'shib, ichiga Spacer qo'yamiz (bu hamma narsani pastga itarib qo'yadi), so'ngra tugmani qo'shamiz — aslida men haqiqiy Button o'rniga onTapGesturedan foydalanaman, shuning uchun shunchaki matn qo'shib, "Sign in" deb yozamiz. Bunga headline shrifti, binafsha rangda matn, balandligi 55, maksimal kengligi cheksiz (.infinity) bo'lgan frame, oq fon va burchak radiusi 10 beramiz. Butun VStackga esa, biroz yuqoriga itarish uchun padding(30) qo'shamiz, va, albatta, bosganimizda biror narsa qilishi uchun .onTapGesture qo'shamiz (hozircha bo'sh qoldiramiz).

Endi bodyda kod ko'payib ketganligi sababli, shu matnni alohida o'zgaruvchiga chiqaramiz: private var bottomButton, turi some View, va shu matnni qirqib olib shu yerga joylaymiz — bodyda esa shunchaki bottomButtonga murojaat qilamiz, shunda body toza va qisqa qoladi.

Bundan tashqari, bunday komponentlar ko'payib ketishini bilganim uchun, men buni extension (kengaytma) ichiga olib o'taman — bu, asosan, mavjud view'imiz uchun kodni boshqa joyga, masalan faylning pastiga ko'chirish imkonini beradi. extension OnboardingView yaratamiz, MARK: Components deb izoh qo'shamiz, va bottomButtonni shu extension ichiga ko'chiramiz. Endi asosiy view'imiz juda toza va tartibli ko'rinadi, va agar kimdir bottomButton nima ekanligini bilishni xohlasa, shunchaki o'ng tugmani bosib, "Jump to Definition" orqali to'g'ridan-to'g'ri shu kodga o'tishi mumkin.


Content layer: switch statement bilan ekranlarni almashtirish

Endi tarkib (content) qatlamini qo'shishimiz kerak — bu qatlam onboarding holatiga qarab o'zgarib turadi. Avval ZStack qo'shamiz (shunda holat o'zgarganda ham bu ZStack ekranda qolib turadi).

Bu yerda mantiqni if onboardingState == 0 { ... } else if onboardingState == 1 { ... } kabi yozish ham mumkin edi, va bu ham ishlaydi — ammo agar sizning ilovangizda barcha shartli mantiq bir xil o'zgaruvchiga asoslangan bo'lsa (bizning holatda bularning barchasi onboardingStatega bog'liq), switch bayonotidan foydalanish ancha samaraliroq. Shuning uchun bu ko'plab iflar o'rniga, switch onboardingState deb yozamiz, va case 0: uchun hozircha shunchaki burchak radiusi 25, foreground rangi qizil bo'lgan RoundedRectangle qo'yamiz.

Switch bayonoti to'liq qamrovli (exhaustive) bo'lishi kerak — bu shuni bildiradi: agar biz caselarni qo'shsak, barcha mumkin bo'lgan holatlarni qamrab olishimiz kerak. Bizda, albatta, 0, 1, 2, 3 bor, ammo Int cheksiz davom etishi mumkin, shuning uchun bizga default holat ham kerak — bu yerda ham yana bir RoundedRectangle qo'yamiz, bu safar yashil rangda:

var content: some View {
    ZStack {
        switch onboardingState {
        case 0:
            RoundedRectangle(cornerRadius: 25)
                .foregroundColor(.red)
        default:
            RoundedRectangle(cornerRadius: 25)
                .foregroundColor(.green)
        }
    }
}

Albatta, bu to'rtburchaklarni keyinroq haqiqiy ekranlarga almashtiramiz — hozircha shunchaki strukturani sozlab olmoqdamiz.


Xush kelibsiz ekrani (case 0)

Birinchi holat — xush kelibsiz ekrani, va bu yerda ham ancha ko'p kod bo'ladi, shuning uchun bottomButton kabi, buni ham alohida o'zgaruvchiga chiqaramiz. Extension'imizga private var welcomeSection, turi some View, qiymati esa bo'shliq (spacing: 40) bilan VStack bo'ladi. Avval ichiga shunchaki Text("hi") qo'yamiz, va buni preview'imizdagi case 0ning o'rniga qo'yamiz — onboarding holatimiz 0 bo'lgani uchun, buni darhol ko'ramiz.

Endi buni to'ldirishni boshlaymiz: avval Image qo'shamiz — agar bu haqiqiy ilova bo'lsa, bu yerga logotipimizni qo'yardik, ammo bizda logotip yo'q, shuning uchun tizim belgisidan foydalanamiz: "heart.text.square.fill". Buni resizable va scaledToFit qilamiz, o'lchami 200x200 bo'lgan frame beramiz (tekislash kerak emas), va binafsha fonda yaxshiroq ko'rinishi uchun oq rang beramiz.

Rasmning ostiga sarlavha qo'shamiz: "Find your match" — shrifti .largeTitle, og'irligi .semibold (bu odatda chiroyliroq ko'rinadi), rangi oq. Bunga biroz custom underline (maxsus tagiga chizish) qo'shmoqchiman — buning uchun .overlay qo'shamiz (bu yerda men yana izohlangan tarkib o'rniga, ko'p qatorli Capsule qo'shaman, uslubi continuous, balandligi 3 bo'lgan frame bilan). Esda tutaylik: overlay'larda tekislash (alignment) ham bor, shuning uchun pastki tekislashni (.bottom) beramiz — endi bu chiziq matnimizning pastida joylashadi. Uni biroz pastroq surish uchun .offset(x: 0, y: 5) qo'shamiz, va, albatta, oq rangga bo'yaymiz.

Sarlavhaning ostiga yana bir matn qo'shamiz, biroz batafsil tasvir bilan — masalan, "This is the number one app for finding your match online. In this tutorial we are practicing using app storage and other SwiftUI techniques." — bu matnga og'irlik sifatida .medium, rang sifatida esa oq beramiz.

Butun VStackga padding(30) qo'shamiz. Endi bularni ekranning yuqori uchdan bir qismiga joylashtirishni xohlayman — buni qilish uchun, esda tutaylik: pastga Spacer qo'shsak, hammasi yuqoriga suriladi, agar yuqoriga ham Spacer qo'shsak, bu yana markazga qaytadi (chunki ikkala spacer ham teng kattalikda bo'ladi). Ammo qulay bir hiyla bor: agar pastga ikkita spacer qo'ysak, bu uch spacerning barchasi teng kattalikda bo'ladi, demak pastda yuqoridagiga qaraganda ikki marta ko'proq bo'shliq bo'ladi — bu esa tarkibimizni ekranning yuqori uchdan bir qismiga olib boradi. Va nihoyat, butun VStackga multilineTextAlignment(.center) qo'shib, hammasini markazga tekislaymiz.


Ism kiritish ekrani (case 1)

Onboarding holati 0 bo'lganda bizda xush kelibsiz bo'limi bor, ammo 1 bo'lganda foydalanuvchidan ismini so'rashimiz kerak. Yana extension'imizga qaytib, welcomeSectionning ostiga yana bir private var addNameSection, turi some View qo'shamiz. welcomeSectiondagi tuzilishni juda ma'qul ko'rganim uchun, buni nusxalab, shu yerga joylayman, faqat rasm va underline overlay'ni o'chirib tashlayman, sarlavhani esa "What's your name"ga, qo'shimcha matnni esa olib tashlayman.

Buni case 1ga qo'yamiz (vaqtincha onboarding holatini 1ga o'zgartirib, ko'rib chiqamiz). Sarlavhaning ostiga, foydalanuvchi ismini yozishi uchun TextField qo'shamiz — placeholder sifatida "Your name..." beramiz, va bunga binding string kerak bo'ladi.

Shu yerda bir qiziq narsani ko'rsatib o'tmoqchiman: agar shu o'zgaruvchini to'g'ridan-to'g'ri extension ichida yaratishga harakat qilsak (masalan, @State var textFieldText: String = ""ni shu yerda), Xcode bizga xatolik beradi — chunki extension'lar saqlangan xususiyatlarni (stored properties) o'z ichiga olishi mumkin emas. Bu shuni bildiradi: bu kabi o'zgaruvchilarni extension ichida e'lon qilib bo'lmaydi. Shuning uchun buni qirqib olib, asosiy OnboardingViewning yuqorisiga olib chiqamiz — garchi bu o'zgaruvchi yuqorida e'lon qilingan bo'lsa ham, biz uni hamon pastdagi extension ichida ishlatishimiz mumkin, chunki bu extension aynan shu view uchun yozilgan.

TextField'ni formatlaymiz: shrift — .headline, fon — oq, fondan oldin balandligi 55 bo'lgan frame, gorizontal padding, va cornerRadius(10). VStackdagi bo'shliqni (spacing) 20ga o'zgartiramiz.

Davom etishdan oldin, shu o'zgaruvchining nomini ham o'zgartirib, soddalashtiraylik: textFieldText o'rniga, shunchaki name deb ataymiz. Buning uchun Xcode'ning ajoyib xususiyatidan foydalanamiz: o'zgaruvchining ustiga o'ng tugmani bosib, **"Refactor" → "Rename"**ni tanlaymiz — bu o'zgaruvchi ishlatilgan barcha joylarni avtomatik ravishda topib, hammasida bir vaqtning o'zida nomini o'zgartiradi:

@State var name: String = ""
TextField("Your name...", text: $name)
    .font(.headline)
    .padding(.horizontal)
    .frame(height: 55)
    .background(Color.white)
    .cornerRadius(10)

Yosh kiritish ekrani (case 2)

Xuddi shu tarzda, addNameSectionning ostiga private var addAgeSection qo'shamiz, addNameSectiondagi tuzilishni nusxalab olamiz, sarlavhani "What's your age"ga o'zgartiramiz, va TextField o'rniga endi Slider qo'shamiz.

Bu slider'da step ham bo'lishi kerak, chunki yoshni faqat butun sonlarda kiritishini xohlaymiz — foydalanuvchi 18.5 yosh kirita olmasligi kerak, faqat 18 yoki 19. Shuning uchun value va stepga ega bo'lgan variantdan foydalanamiz.

Bunga bog'lash uchun, yana yuqoriga chiqib, ism o'zgaruvchisining ostiga @State var age, turi Double, boshlang'ich qiymati 50 bo'lgan o'zgaruvchi qo'shamiz, va shu yerda case 2: addAgeSectionni ham qo'shib qo'yamiz.

Slider'ning qiymati $age bo'ladi, oraliq esa — eng kichik yoshni 18, eng kattasini esa 100 qilib belgilaymiz, qadam (step) esa har safar bir butun songa teng bo'ladi. onEditingChanged bizga kerak emas, shuning uchun buni olib tashlaymiz. Slider rangini ko'k o'rniga oq qilamiz (.accentColor(.white)).

Hozirgi yosh qiymatini ko'rsatish uchun, slider'ning ostiga yana bir matn qo'shamiz va unga agega murojaat qilamiz — ammo bu juda chiroysiz, masalan 50.000 ko'rinishida chiqadi, shuning uchun buni formatlaymiz. Swift'da bunday holatlar uchun qulay yo'l bor: Stringni format bilan yaratish — formatimiz "%.0f" bo'ladi, ya'ni nol kasr xona, va ichiga ageni qo'yamiz:

@State var age: Double = 50

Slider(value: $age, in: 18...100, step: 1)
    .accentColor(.white)

Text(String(format: "%.0f", age))
    .font(.largeTitle)
    .fontWeight(.semibold)
    .foregroundColor(.white)

Endi slider'ni siljitganimizda, son to'liq, butun ko'rinishda mukammal yangilanadi — bu aynan biz xohlagan natija.


Jins tanlash ekrani (case 3)

Yana bir bor — addAgeSectionning ostiga private var addGenderSection qo'shamiz, shu VStack tuzilmasini nusxalab olamiz, sarlavhani "What's your gender"ga o'zgartiramiz. Bu yerda, albatta, slider kerak emas, shuning uchun uni o'chirib, o'rniga Picker qo'shamiz — selection, label va contentga ega variantdan foydalanamiz.

Tanlov (selection) stringga bog'lanishi kerak, shuning uchun yana yuqoriga chiqib, @State var gender, turi String, boshlang'ich qiymati bo'sh string bo'lgan o'zgaruvchi qo'shamiz (bu shuni bildiradi: foydalanuvchi hali jinsni tanlamagan). Shu yerda case 3: addGenderSectionni ham qo'shamiz.

selectionni $genderga bog'laymiz, label o'rnida esa shunchaki "Select a gender" matnini qoldiramiz. Tarkib (content) uchun hozircha ikkita variant qo'yamiz, so'ngra picker uslubini o'zgartiramiz — .pickerStyle(MenuPickerStyle()) deb yozamiz, bu picker'ni menyu ko'rinishiga o'tkazadi. Aslida ekranda biz ko'rib turgan narsa — bu, asosan, picker'ning labeli, shuning uchun barcha formatlashni shu label'ga qo'shamiz: shrift — .headline, foreground rangi — binafsha, balandligi 55, maksimal kengligi cheksiz bo'lgan frame, oq fon, va cornerRadius(10).

Endi tanlovlarni to'ldiraylik: birinchisi — "Male", ikkinchisi — "Female", uchinchisi — "Non-binary". Picker'lar haqida alohida video qilgan edim, va siz bilishingiz kerak: bu yerda haqiqatan ham tanlanadigan narsa — bu tag. genderning tanlovi String turida bo'lgani uchun, bu taglarni ham string qilib belgilashimiz kerak — shuning uchun har bir variantga mos tagni qo'shamiz: male, female, non-binary.

Endi tanlanган jinsga qarab, label matnini ham dinamik qilamiz: agar gender.count qiymati 1dan katta bo'lsa (ya'ni satr bo'sh emas — eslatib o'tay, bo'sh string'ning counti nolga teng, "male"ning esa to'rtga teng), shu genderni ko'rsatamiz, aks holda esa "Select a gender" matnini ko'rsatamiz:

@State var gender: String = ""

Picker(selection: $gender, label: Text(gender.count > 1 ? gender : "Select a gender")
    .font(.headline)
    .foregroundColor(.purple)
    .frame(height: 55)
    .frame(maxWidth: .infinity)
    .background(Color.white)
    .cornerRadius(10)
) {
    Text("Male").tag("male")
    Text("Female").tag("female")
    Text("Non-binary").tag("non-binary")
}
.pickerStyle(MenuPickerStyle())

Preview'da sinab ko'rsak: jinsni Female deb tanlasak, label mukammal yangilanadi.


Bottom tugmasini dinamik qilish

Endi bizda barcha to'rt ekran tayyor, va endi shu ekranlar orasida harakatlanish va nihoyat tizimga kirish (sign in) mantig'ini qo'shishimiz kerak.

Avval shuni payqaymiz: pastdagi tugmamiz har doim "Sign in" deb yozilgan — ammo, aslida, foydalanuvchi allaqachon ro'yxatdan o'tayotgani uchun, bu "Sign up" deyilishi kerak, va keyingi ekranlarda esa "Next" (keyingi) deyilishi, eng so'nggi ekranda esa "Finish" (yakunlash) deyilishi kerak.

Shuning uchun shu matnni dinamik qilamiz: agar onboardingState == 0 bo'lsa — "Sign up" (katta harflarga aylantirib), agar onboardingState == 3 bo'lsa (bu bizning so'nggi holatimiz, jins tanlash) — "Finish", aks holda esa — "Next":

Text(
    onboardingState == 0 ? "Sign up".uppercased() :
    (onboardingState == 3 ? "Finish" : "Next")
)

Matn o'zgarganda biroz g'alati animatsiya borligini payqadim, shuning uchun shu matnga .animation(nil) qo'shib, bu animatsiyani bekor qilamiz.


handleNextButtonPressed funksiyasi

Endi extension OnboardingViewimizning pastiga yana bir extension qo'shamiz — MARK: Functions deb izoh bilan. Bu yerda func handleNextButtonPressed() yaratamiz — bu funksiya pastdagi tugmani bosganimizda har safar chaqiriladi.

Avval, eng sodda holatda: tugmani bosganimizda, onboardingStateni animatsiya bilan birga 1ga oshiramiz:

func handleNextButtonPressed() {
    withAnimation(.spring()) {
        onboardingState += 1
    }
}

Ammo agar onboardingState == 3 bo'lsa (ya'ni biz so'nggi — jins tanlash ekranidamiz), tugmani bosganimizda keyingi raqamga o'tish o'rniga, tizimga kirish (sign in) mantig'ini ishga tushirishimiz kerak. Shuning uchun: agar onboardingState == 3 bo'lsa, tizimga kirish kodi ishlaydi (buni keyinroq yozamiz); aks holda esa, animatsiya bilan birga keyingi holatga o'tamiz.


Kiritilgan ma'lumotlarni tekshirish (validatsiya) va alert

Ammo davom etishdan oldin, muhim bir narsani payqadim: hozircha foydalanuvchi hech narsa yozmasdan ham "Next" tugmasini bosib, keyingi ekranga o'tib ketishi mumkin. Buning oldini olishimiz kerak — foydalanuvchi haqiqatan ham matn kiritmaguncha, keyingi ekranga o'tishga ruxsat bermasligimiz kerak.

Buni tekshirishning eng qulay joyi — aynan tugmani bosganimizda, keyingi ekranga o'tishdan oldin. Shu uchun handleNextButtonPressed funksiyasi ichida, joriy onboardingState ustida yana bir switch qo'shamiz (faqat shu holatni tekshirishimiz kerak bo'lgani uchun): case 1 bo'lganda (ya'ni ism kiritish ekranida), guard name.count >= 3 else { ...; return } deb tekshiramiz — agar ism kamida 3 belgidan iborat bo'lmasa, funksiyadan chiqib ketamiz. default holatida esa shunchaki break qo'yamiz (bu, switch ichida boshqa case'larga to'g'ri kelmasak, shunchaki switch'dan chiqib, qolgan kodni davom ettirishni bildiradi).

Ammo guard ishlamay qolganda, foydalanuvchiga shu haqida bildirish uchun, ekranga tezkor bir alert chiqarmoqchiman — bu haqida ham alohida video qilgan edim, shuning uchun siz buni tanish bo'lishi kerak. Yuqorida @State var alertTitle, turi String, bo'sh string, va @State var showAlert, turi Bool, qiymati false bo'lgan o'zgaruvchilar qo'shamiz. Asosiy ZStackga esa .alert(isPresented: $showAlert) qo'shamiz, tarkibida esa sarlavhasi alertTitlega teng bo'lgan Alert qaytaramiz.

Har safar alert ko'rsatishdan oldin, sarlavhani o'rnatishimizni unutmasligimiz uchun, buni alohida funksiyaga chiqaramiz: func showAlert(title: String) — bu funksiya avval alertTitleni o'rnatadi, so'ngra showAlert.toggle()ni chaqiradi. Endi alert ko'rsatish kerak bo'lganda, biz to'g'ridan-to'g'ri ikki qatorni yozish o'rniga, shunchaki shu funksiyani chaqiramiz:

@State var alertTitle: String = ""
@State var showAlert: Bool = false

func showAlert(title: String) {
    alertTitle = title
    showAlert.toggle()
}

case 1 uchun esa, ism juda qisqa bo'lsa, showAlert(title: "Your name must be at least three characters long 😅")ni chaqirib, so'ngra return qilamiz:

func handleNextButtonPressed() {
    switch onboardingState {
    case 1:
        guard name.count >= 3 else {
            showAlert(title: "Your name must be at least three characters long 😅")
            return
        }
    default:
        break
    }

    if onboardingState == 3 {
        signIn()
    } else {
        withAnimation(.spring()) {
            onboardingState += 1
        }
    }
}

Yosh ekrani uchun bunday tekshirish shart emas, chunki slider o'zi 18 dan 100 gacha bo'lgan oraliqda cheklangan, demak bu doim haqiqiy bo'ladi. Ammo jins ekrani uchun yana bir tekshirish kerak — esda tutaylik, genderni bo'sh string bilan boshlagan edik, shuning uchun shunchaki uning bo'sh emasligini tekshirsak bo'ladi: case 3: guard gender.count > 1 else { showAlert(title: "Please select a gender before moving forward 🙄"); return }.


AppStorage orqali ma'lumotlarni saqlash (signIn funksiyasi)

Endi bizga signIn() funksiyasini yozish qoldi — va bu, aslida, juda sodda: biz foydalanuvchi hozir kiritgan barcha qiymatlarni AppStorage'ga saqlaymiz.

Yuqorida, kiritilgan ma'lumotlar va alert o'zgaruvchilaridan keyin, yana bir bo'lim qo'shamiz — AppStorage o'zgaruvchilari uchun. Har bir ma'lumot uchun bittadan kerak: @AppStorage("name") var currentUserName: String?, @AppStorage("age") var currentUserAge: Int?, @AppStorage("gender") var currentUserGender: String? — bularni optional qilamiz, chunki ilovani birinchi marta ochganimizda, bu qiymatlar hali hech narsaga o'rnatilmagan bo'ladi, demak ular nil bo'ladi.

Yana bittasi kerak: foydalanuvchi tizimga kirgan-kirmaganligi. Bu uchun aynan IntroViewda ishlatgan AppStorage kalitini ("signed_in") ishlatishimiz juda muhim, shuning uchun shu kodni IntroView'dan qirqib olib, shu yerga ham joylaymiz:

@AppStorage("name") var currentUserName: String?
@AppStorage("age") var currentUserAge: Int?
@AppStorage("gender") var currentUserGender: String?
@AppStorage("signed_in") var currentUserSignedIn: Bool = false

func signIn() {
    currentUserName = name
    currentUserAge = Int(age)
    currentUserGender = gender

    withAnimation(.spring()) {
        currentUserSignedIn = true
    }
}

age o'zgaruvchimiz Double turida, ammo currentUserAge esa Int? turida bo'lgani uchun, bu yerda kichik xatolik chiqadi — buni Int(age) deb yozib, oddiygina turini o'zgartirib qo'yamiz (bilamizki, Double har doim butun son bo'ladi, chunki slider'imizda step: 1 qo'ygandik, shuning uchun bu muammo emas).

currentUserSignedInni o'rnatganimizda, buni animatsiya bilan birga qilishni xohlayman, chunki bu signed-in (tizimga kirgan) view'ga o'tishni boshqaradi — shuning uchun withAnimation(.spring()) ichiga olamiz.


IntroView'ni ulash va ilovani ishga tushirish

Endi IntroViewga qaytib, else bo'limidagi "Onboarding View" matnini, hozirgina yaratganimiz OnboardingView()ga almashtiramiz. So'ngra App.swift faylimizga o'tib, ilovamiz ishga tushganda ko'rsatadigan birinchi ekranni IntroView qilib belgilaymiz.

Simulyatorda "Run" tugmasini bosib sinab ko'raylik — binafsha fonimiz va uning ustida xush kelibsiz ekranimiz bor: "Find your match", va ostida tasvir matni. "Sign up"ni bossak — keyingi ekranga o'tadi, ammo hozircha bu shunchaki bir oz so'nib o'tish (fade) effekti bilan o'tadi. Bu yomon emas, ammo buni yaxshilashimiz mumkin — keling, transitionlar bilan biroz amaliyot qilaylik.


Ekranlar orasidagi o'tish animatsiyasi (Transition)

Barcha bo'limlar (welcome, addName, addAge, addGender) uchun bir xil transition'dan foydalanamiz, shuning uchun buni alohida o'zgaruvchiga chiqaramiz: let transition, turi AnyTransition. Bizga kiritish (insertion) va olib tashlash (removal) uchun har xil harakat kerak, shuning uchun .asymmetricdan foydalanamiz: kiritishda, ekranning o'ng tomonidan kirib kelishini xohlaymiz (.move(edge: .trailing)), olib tashlashda esa ekranning chap tomonidan chiqib ketishini xohlaymiz (.move(edge: .leading)):

let transition: AnyTransition = .asymmetric(
    insertion: .move(edge: .trailing),
    removal: .move(edge: .leading)
)

Endi shu transition'ni barcha to'rt bo'limimizga (welcomeSection, addNameSection, addAgeSection, addGenderSection) .transition(transition) orqali qo'shamiz. Preview'ni qaytadan ishga tushirib sinab ko'rsak: "Sign up"ni bossak, endi ekran chiroyli tarzda chapga suriladi, keyingisi esa o'ngdan suzib kiradi — bu juda professional va chiroyli ko'rinadi, va buning uchun atigi bir necha qator kod yetarli bo'ldi.


ProfileView yaratish

Endi ushbu videoda qilishimiz kerak bo'lgan so'nggi narsa — profil viewini yaratish, bu juda sodda ekran bo'ladi. Navigator'da Onboarding Views guruhiga o'tib, o'ng tugmani bosib, yangi SwiftUI View fayl yaratamiz va buni ProfileView deb ataymiz.

Bu view'ga kerak bo'lgan birinchi narsa — foydalanuvchi onboarding'dan o'tgandan keyin AppStorage'ga saqlangan barcha qiymatlar. Shuning uchun OnboardingViewdan AppStorage o'zgaruvchilarini (currentUserName, currentUserAge, currentUserGender) nusxalab, shu yerning yuqorisiga ham joylaymiz — bular AppStorage'da bo'lgani uchun, ilovamizning istalgan view'ida ulardan foydalanishimiz mumkin.

Endi VStack qo'shib, ichiga bir Image qo'shamiz — bu foydalanuvchi uchun profil rasmi bo'lardi, ammo bizda haqiqiy rasm yo'q, shuning uchun tizim belgisidan foydalanamiz: "person.circle.fill", resizable, scaledToFit, o'lchami 150x150 bo'lgan frame bilan.

Rasmning ostiga matn qo'shamiz — bu yerda foydalanuvchi ismini ko'rsatamiz. Esda tutaylik, bu optional, shuning uchun uni xavfsiz "ochishimiz" kerak — if let ishlatishimiz ham mumkin edi, ammo bu qiymat har doim mavjud bo'lishini bilganimiz uchun, shunchaki nil coalescing operatoridan (??) foydalanamiz, va agar qiymat bo'lmasa, standart qiymat sifatida "Your name"ni beramiz (bu, aslida, hech qachon yuz bermasligi kerak, chunki ilovamizda bu ism allaqachon o'rnatilgan bo'ladi).

Yana bir matn qo'shamiz, yoshi uchun: "This user is \(currentUserAge ?? 0) years old" (yana, agar nimadir sababga ko'ra nil bo'lsa, standart qiymat sifatida 0 beramiz). Va so'nggi matn — jinsi uchun: "Their gender is \(currentUserGender ?? "Unknown")":

VStack {
    Image(systemName: "person.circle.fill")
        .resizable()
        .scaledToFit()
        .frame(width: 150, height: 150)

    Text(currentUserName ?? "Your name")

    Text("This user is \(currentUserAge ?? 0) years old")

    Text("Their gender is \(currentUserGender ?? "Unknown")")
}

Endi formatlashni qo'shamiz: butun VStackga shrift sifatida .font(.title) beramiz, fondan oldin ozgina padding qo'shamiz, fon sifatida Color.white beramiz, fondan oldin cornerRadius(10), fondan keyin esa shadow(radius: 10) qo'shamiz. Yana biroz qo'shimcha bo'shliq uchun .padding(.vertical, 40) qo'shamiz, va matnni binafsha rangga aylantiramiz: .foregroundColor(.purple).

Albatta, bu hali unchalik chiroyli ko'rinmaydi — men buni haqiqiy ilovada ishlatmasdim, ammo bizning maqsadimiz shunchaki turli AppStorage o'zgaruvchilari va modifikatorlardan amaliyot qilish, hamda shu AppStorage o'zgaruvchilari yordamida ekranlar orasida harakatlanishni ko'rsatish, xolos.


Chiqish (Sign Out) tugmasi

Qilishim kerak bo'lgan so'nggi narsa — ZStackning pastiga yana bir tugma qo'shish. Text("Sign out") qo'shamiz, fon rangi sifatida qora (Color.black), foreground rangi sifatida oq matn, shrift sifatida .headline, balandligi 55 bo'lgan frame, maksimal kengligi cheksiz bo'lgan frame, va cornerRadius(10) beramiz. VStackga ham bo'shliq (spacing) sifatida, masalan, 20 qo'shamiz.

Shu tugmani bosganimizda, albatta, ilovadan chiqishni (sign out) xohlaymiz, shuning uchun bodydan tashqarida func signOut() funksiyasi yaratamiz. Tizimdan chiqish uchun qilishimiz kerak bo'lgan yagona narsa — barcha hozirgi foydalanuvchi ma'lumotini o'chirish: ism, yosh, jins, va tizimga kirganlik holati — xuddi tizimga kirganimizda qilganimiz kabi, ammo bu safar teskarisi. Shuning uchun currentUserName = nil, currentUserAge = nil, currentUserGender = nil, va currentUserSignedIn = false (eslatib o'tay, bu o'zgaruvchi optional emas, shunchaki true yoki false).

Va nihoyat, buni ham animatsiya bilan qilishni xohlayman, chunki bu boshqa ekranga o'tishni keltirib chiqaradi — shuning uchun withAnimation(.spring()) ichiga olamiz:

func signOut() {
    withAnimation(.spring()) {
        currentUserName = nil
        currentUserAge = nil
        currentUserGender = nil
        currentUserSignedIn = false
    }
}

"Sign out" matniga esa .onTapGesture { signOut() } qo'shamiz.


Profil va Onboarding orasidagi o'tish

So'nggi tafsilot sifatida, profil va onboarding view'lari orasidagi shu so'nggi almashinuvga ham animatsiya, ya'ni transition qo'shaylik. ProfileViewga .transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top))) qo'shib, profil view ekranga pastdan kirib kelishini, va yuqoriga chiqib ketishini ta'minlaymiz.

OnboardingView uchun esa, aynan teskarisini qilamiz — transition'ni nusxalab, kiritishni .topga, olib tashlashni esa .bottomga o'zgartiramiz:

// ProfileView uchun:
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))

// OnboardingView uchun:
.transition(.asymmetric(insertion: .move(edge: .top), removal: .move(edge: .bottom)))

Sinab ko'rish va yakuniy fikrlar

Keling, butun jarayonni so'nggi marta sinab ko'raylik: "Find your match" ekranidan boshlaymiz, "Sign up"ni bosamiz — ikkinchi ekranga chiroyli animatsiya bilan o'tamiz. "What's your name" — ismimni, masalan, "Nick" deb yozib, "Next"ni bosamiz. Yoshim uchun, aytaylik, 71ni tanlab, "Next"ni bosamiz. Jinsim uchun, agar tanlamasdan "Finish"ni bossam, "iltimos, davom etishdan oldin jinsni tanlang" degan alert chiqadi — jinsni tanlab, "Finish"ni bossam, profil view'iga chiroyli pastdan yuqoriga o'tish bilan kiramiz. Endi bu foydalanuvchi tizimga kirgan, va bizda uning ismi, yoshi, jinsi bor, hamda tizimdan chiqish imkoni ham bor.

Agar tizimdan chiqmasdan, shunchaki ilovani yopib, qaytadan ochsam, barcha shu qiymatlar hamon AppStorage'da saqlanган bo'ladi — demak, bu ma'lumot sessiyalar orasida saqlanib qoladi. Shuning uchun foydalanuvchini tizimga kirgan holda saqlab qolishimiz mumkin, va ular ilovani qayta ochganda, ular allaqachon profil view'ida bo'ladi, onboarding view'iga qaytmaydi. Agar "Sign out"ni bossam, bu barcha qiymatlarni AppStorage'dan olib tashlaydi, va biz yana onboarding view'iga qaytamiz — shunda "Sign up"ni bossak, butun jarayonni yana boshidan o'tamiz. Agar shu holatda ilovani yopib qaytadan ochsam, u boshlang'ich holatdan boshlanadi, chunki biz barcha qiymatlarni AppStorage'dan olib tashlagan edik, va biz go'yo tizimga umuman kirmaganday bo'lamiz.

Albatta, agar bu sizning haqiqiy ilovangiz bo'lsa, foydalanuvchi ma'lumotini biror ma'lumotlar bazasida saqlardingiz, shunda ular tizimga kirib-chiqishlari mumkin bo'lardi, va har safar qaytadan ro'yxatdan o'tishlari shart bo'lmasdi — ammo bu shunchaki o'quv darslik bo'lgani uchun, biz bu yerda transition va AppStorage'dan foydalanish bo'yicha juda ko'p amaliyot qildik.

Ko'rib turganingizdek, AppStorage juda kuchli vosita — chunki biz shu o'zgaruvchilardan ilovamizning barcha ekranlarida foydalanishimiz mumkin: biz ularni IntroViewda ishlatdik, xuddi shu o'zgaruvchilarni ProfileViewda, shuningdek OnboardingViewda ham ishlatdik, va aynan shu o'zgaruvchilar asosida ilovamizning butun holatini boshqardik.

Umid qilamanki, bu yaxshi amaliyot bo'ldi — bilaman, bu uzunroq videolardan biri edi, ammo endi siz haqiqatan ham mohir SwiftUI dasturchisiga aylanib bormoqdasiz. Tomosha qilganingiz uchun yana bir bor rahmat, umid qilamanki, sizlarga yoqdi — agar biror narsa chalkash bo'lsa, izohlarda yozing, chunki bilaman, bu yerda juda ko'p kod yozdik. Har doimgidek, men — Nik, bu Swiftful Thinking, keyingi videoda ko'rishamiz!

Buy mea coffee