Published on

SwiftUI ilovasida foydalanuvchi tajribasi (UX) va animatsiyalar

Authors

Ushbu pleylistda biz SwiftUI'da vazifalar ro'yxati (to-do list) ilovasini qurmoqdamiz, va ushbu videoda biz biroz foydalanuvchi tajribasiga (user experience) e'tibor qaratamiz — chunki hozircha ro'yxatimiz bo'sh bo'lganda, ekran juda-juda bo'sh ko'rinadi, bu esa yaxshi foydalanuvchi tajribasi emas. Shuning uchun, buning o'rniga, ekranga foydalanuvchilarga ro'yxatda hech qanday element yo'qligini bildiradigan kichik bir xabar qo'shamiz, ularga yangi element qo'shish imkonini taqdim etamiz, va buni haqiqatan ham chiroyli ko'rsatish uchun biroz animatsiya ham qo'shamiz.


Muammo: bo'sh ro'yxat foydalanuvchini chalg'itadi

Oldingi videoda biz ilovamizdagi elementlarni saqlash mantig'ini yig'ib bo'ldik — endi barcha elementlarimiz UserDefaults'da saqlanmoqda. Ammo payqagan bo'lishingiz mumkin: oldingi videoda ilovamizni birinchi marta, UserDefaults bilan ishga tushirganimizda, ro'yxatimizda hech qanday element bo'lmagan edi, va u shunday ko'rinardi. Agar shu elementlarni o'chirsam, ro'yxatimiz butunlay bo'sh bo'lib qoladi — hech qanday element ko'rsatmaslik, albatta, to'g'ri, ammo agar bu haqiqiy ilova bo'lganida, va foydalanuvchimiz shu ekranni ko'rsa, ular bu ilova yuklanishni tugatmagan deb o'ylashlari mumkin, chunki ekran juda bo'sh ko'rinadi — bu ilova noto'g'ri ishlayaptimi yoki haqiqatan ham ro'yxatimiz bo'shmi, buni aniq bilolmaymiz.

Shuning uchun men shu yerga kichik bir bildirishnoma, kichik bir matn qo'yib, vazifalar ro'yxati bo'sh ekanligini ko'rsatishni xohlayman — bu, foydalanuvchiga aynan shu ekran, aynan shu ro'yxat ekanligini, ammo hozircha hech qanday element yo'qligini bildiradi. Shuning uchun vazifalar ro'yxati bo'sh bo'lganida shu yerga qo'yish uchun juda sodda bir view yaratamiz.


ListView ichida shartli ko'rsatish

Avval ListViewga ZStack qo'shamiz, qavslarni ochib, mavjud Listni qirqib olib, shu yerga joylaymiz. Shu ZStack ichiga shartli mantiq qo'shamiz: if listViewModel.items.isEmpty { ... } else { ... } deb yozamiz.

Agar bo'sh bo'lsa va hech qanday element bo'lmasa, qavslarni ochib, shu yerga matn qo'shamiz: Text("No items"). Aks holda, ya'ni bo'sh bo'lmasa va elementlar bo'lsa, qirqib olgan ro'yxatimizni shu yerga joylaymiz:

ZStack {
    if listViewModel.items.isEmpty {
        Text("No items")
    } else {
        List {
            ForEach(listViewModel.items) { item in
                ListRowView(item: item)
            }
            .onDelete(perform: listViewModel.deleteItem)
            .onMove(perform: listViewModel.moveItem)
        }
        .listStyle(PlainListStyle())
    }
}

Demak, ro'yxat faqat elementlar mavjud bo'lganda ko'rsatiladi, va hozircha elementlar bo'lmasa, biz shunchaki shu matnni ko'rsatamiz — ammo shu matn yoki ro'yxat ko'rsatilishidan qat'i nazar, navigatsiya sarlavhasi va bar elementlari har doim ekranda qolib turadi.


NoItemsView yaratish

Endi bu yangi fayl yaratamiz: Navigator'da Views papkasiga o'ng tugmani bosib, yangi SwiftUI View fayl yaratamiz, va buni NoItemsView deb ataymiz. "Create" tugmasini bosamiz, canvas'da "Resume" tugmasini bosamiz.

Avval ekranga ScrollView qo'shamiz, qavslarni ochib, ichiga tezkor bir matn qo'sib ko'rsak, bu matn ekranning eng yuqorisiga chiqib ketganini ko'ramiz. ScrollView'ning maksimal balandligi cheksiz bo'lgani uchun, bu mazmunni ekranning yuqorisiga olib boradi — bu juda qulay, chunki agar shunchaki matn qo'yganimizda, u ekranning markazida bo'lib qolardi, men esa bu mazmunni yuqoriga itarib qo'yishni xohlayman.

ScrollView'ga, ehtiyot shart uchun, kengligi va balandligi imkon qadar katta bo'lishini ham qo'shamiz: .frame(maxWidth: .infinity, maxHeight: .infinity).

Yana shuni ham ta'kidlab o'taman: bu NoItemsViewni ko'rsatganimizda, biz ListView ichida bo'lganimiz uchun, allaqachon NavigationView ichidamiz — shuning uchun preview'ga ham buni qo'shib, qanday ko'rinishini tekshirib ko'raylik: NavigationView qo'shib, shu yerga joylaymiz, va .navigationTitle("Title") qo'shib, sinab ko'raylik.

ScrollView {
    Text("hi")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

Sarlavha va tavsif matnini qo'shish

Endi ScrollView ichiga VStack qo'shamiz, qavslarni ochib, avval matn qo'shamiz — bu sarlavha bo'ladi: "There are no items" (matnga xohlagan narsangizni yozishingiz mumkin). Bunga .font(.title) va .fontWeight(.semibold) beramiz — "semibold" har doim chiroyli ko'rinadi.

Shu matnning ostiga tavsif qo'shamiz, yana bir matn: "Are you a productive person? I think you should click the add button and add a bunch of items to your to-do list".

Butun VStackga .multilineTextAlignment(.center) qo'shamiz, shunda matn markazga tekislanadi. VStackga bo'shliq (spacing) sifatida 10 qo'shamiz, va shu multilineTextAlignmentdan keyin esa, hammasini biroz ichkariga itarish uchun, .padding(40) qo'shamiz:

VStack(spacing: 10) {
    Text("There are no items")
        .font(.title)
        .fontWeight(.semibold)

    Text("Are you a productive person? I think you should click the add button and add a bunch of items to your to-do list 🤔")
}
.multilineTextAlignment(.center)
.padding(40)

Katta tugma qo'shish

Endi shu matnning ostiga tugma qo'yishni xohlayman — ekranning yuqori o'ng tomonida turgan "Add" tugmasiga qo'shimcha ravishda, foydalanuvchilarni "qo'shish" ekraniga olib boruvchi katta bir tugma ham qo'shaman. Buni bosganimizda, NavigationView ichida harakatlanishni xohlaymiz, shuning uchun bu NavigationLink bo'ladi — va biz AddViewga (bir necha video oldin yaratgan edik) o'tamiz.

NavigationLink qo'shamiz, destination va labelga ega variantdan foydalanamiz. Destination — bu bizning AddView, label esa, shunchaki "Navigate" deyish o'rniga, "Add Something" deb yozamiz, va Control+Command+Bo'shliq tugmalarini bosib, emoji klaviaturasini chiqarib, masalan tantana yuzi emojisini qo'shamiz — hamma uni yoqtiradi.

NavigationLink(destination: AddView()) {
    Text("Add Something 🎉")
        .font(.headline)
        .foregroundColor(.white)
        .frame(height: 55)
        .frame(maxWidth: .infinity)
        .background(Color.accentColor)
        .cornerRadius(10)
}

Bunga formatlash qo'shamiz: .foregroundColor(.white), .font(.headline), fon rangi sifatida Color.accentColor (buni keyinroq yangilaymiz — standart bo'yicha bu, ilovamizning butun joyida ko'rgan ko'k rang). Fondan oldin esa balandligi 55, maksimal kengligi cheksiz bo'lgan frame qo'shamiz, fondan keyin esa .cornerRadius(10) qo'shamiz.

Endi tugmamiz chiroyli ko'rinadi — buni shu holicha qoldirsak ham bo'ladi, ammo men biroz animatsiya ham qo'shmoqchiman, chunki animatsiyalar ilovangizni har doim biroz yaxshiroq ko'rinishga keltiradi, va bu bizga asosiy animatsiyalar bo'yicha amaliyot qilish imkonini beradi.


Animatsiya holatini yaratish

Shu view'ning yuqorisida, @State var animate: Bool = false deb e'lon qilamiz. Bu ekranga chiqqanida animatsiya boshlanishini xohlaymiz, shuning uchun VStackning pastiga, padding'dan keyin, .onAppear { } qo'shamiz.

Bu animatsiya chaqiruvida biroz mantiq bo'ladi, shuning uchun bodydan tashqarida, pastida, alohida funksiya yarataman. func addAnimation() deb yozamiz, qavslarini ochib-yopamiz, va buni onAppeardan chaqiramiz:

.onAppear {
    addAnimation()
}

Ekran ekranga chiqqanida, animatsiya boshlanishidan oldin bir soniyalik kechikish qo'shmoqchiman. Shuning uchun avval kechikish qo'shamiz: DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { }1.5 soniya kechikish bilan. execute ichida esa animatsiya kodimizni yozamiz: animatsiyani amalga oshirish uchun, albatta, animate Boolean'ini almashtiramiz, va buni animatsiya bilan birga qilishni xohlaymiz, shuning uchun withAnimation(.easeInOut) { animate.toggle() } deb yozamiz:

func addAnimation() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
        withAnimation(.easeInOut) {
            animate.toggle()
        }
    }
}

Demak, ekran paydo bo'lganda, addAnimation chaqiriladi, va addAnimation 1.5 soniya kutib, so'ngra animateni almashtiradi.


onAppear ikki marta chaqirilishining oldini olish

Tezda shuni ta'kidlab o'tmoqchiman: onAppear har safar shu ekran paydo bo'lganda chaqiriladi. Demak, agar foydalanuvchi ilovamizning asosiy ekranini ochsa, "Add" tugmasini bosib ikkinchi ekranga o'tsa, so'ngra "Back" tugmasini bosib shu ekranga qaytsa — onAppear ilova birinchi marta yuklanganida ham, foydalanuvchi orqaga qaytib kelganida ham chaqiriladi, chunki onAppear shu ekran har safar paydo bo'lganida chaqiriladi.

Bu yaxshi narsa bo'lsa-da, men animatsiyani ikki marta qo'shishni xohlamayman — chunki agar animatsiya allaqachon ishlayotgan bo'lsa, uni yana almashtirishimiz shart emas. Shuning uchun, bu yuz berishi mumkin bo'lgan holatlar uchun, tezkor bir tekshiruv qo'shamiz: animate Boolean'i false ekanligiga ishonch hosil qilamiz, chunki agar u true bo'lsa, demak animatsiya allaqachon ishlayapti.

Shuning uchun DispatchQueueni chaqirishdan oldin, guard !animate else { return } deb yozamiz — demak, animate rost emasligini, ya'ni false ekanligini tekshiramiz, aks holda funksiyadan chiqib ketamiz, chunki agar animate true bo'lsa, biz shunchaki funksiyadan chiqib ketamiz va davom etmaymiz:

func addAnimation() {
    guard !animate else { return }

    DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
        withAnimation(.easeInOut) {
            animate.toggle()
        }
    }
}

Demak, biz buni ikki marta chaqirmasligimizga ishonch hosil qildik.


Fon rangini animatsiya qilish

Endi biroz animatsiya qo'sha boshlaylik. Avval fondan boshlaymiz. Hozircha fon rangi Color.accentColor, ammo buni endi ternary operator orqali o'zgaruvchan qilamiz: agar animate bo'lsa — Color.red, aks holda — Color.accentColor:

.background(animate ? Color.red : Color.accentColor)

Live Preview'da "Play" tugmasini bosib ko'rsak, endi rang ko'kdan qizilga animatsiya bilan o'zgaradi — bu juda yaxshi, ammo buni biroz yanada yaxshilashimiz mumkin. Men shunchaki ko'kdan qizilga o'tib, to'xtab qolishini emas, balki bu o'tishni davom ettirib turishini xohlayman.

Shuning uchun animatsiyani moslashtiramiz: shunchaki .easeInOut o'rniga, buni o'chirib, Enter bosib, alohida qatorlarga joylashtiramiz, va maxsus Animation qo'shamiz: .easeInOut(duration: 2.0) — bu safar duration (davomiylik) parametridan foydalanib, oddiy holatdan ko'ra biroz uzunroq qilamiz, ya'ni 2.0 soniya. So'ngra .repeatForever()ni qo'shamiz:

withAnimation(.easeInOut(duration: 2.0).repeatForever()) {
    animate.toggle()
}

Endi Live Preview'da "Play" tugmasini bosib ko'rsak, rang 2 soniya davomida ko'kdan qizilga o'tishi va bu cheksiz takrorlanishi kerak — bu allaqachon biroz yaxshiroq ko'rinmoqda, ammo, menimcha, buni yanada yaxshiroq qilishimiz mumkin.


Padding, scale va offset animatsiyalari

Endi shu NavigationLinkning, ya'ni butun navigation link'ning o'ziga ozgina padding qo'shamiz: .padding(.horizontal, 30) deb yozib, ko'rinishini sinab ko'raylik. 30 qiymatida biroz kichikroq ko'rinadi, 50 qiymatida esa yanada kichikroq. Keling, shu ikkisi orasida animatsiya qilaylik: animate ? 30 : 50 deb yozamiz:

.padding(.horizontal, animate ? 30 : 50)

Endi tugmamiz biroz kattalashib-kichraytirib, kengayib-torayib turganga o'xshaydi — chunki biz tugmaning tashqarisidagi padding'ni animatsiya qilmoqdamiz. Endi unga biroz scale effect (kattalashtirish effekti) ham qo'shaylik: .scaleEffect qo'shib, CGFloat variantidan foydalanamiz: animate ? 1.1 : 1.0 — agar animatsiya bo'lsa, joriy o'lchamning 1.1 barobari, aks holda esa shunchaki joriy o'lcham:

.scaleEffect(animate ? 1.1 : 1.0)

Endi bu biroz kattalashib, so'ngra biroz kichraytirib turganga o'xshaydi. Keling, offsetni ham animatsiya qilaylik, shunda u biroz yuqoriga-pastga harakatlanayotganga o'xshaydi: .offset qo'shamiz, xdan foydalanmaymiz (shuning uchun olib tashlaymiz), y uchun esa: agar animatsiya bo'lsa — -7, aks holda — 0:

.offset(y: animate ? -7 : 0)

Endi bu yuqoriga-pastga harakatlanayotganga o'xshaydi — biroz yuqoriga ko'tariladi, biroz pastga tushadi, va yana yuqoriga ko'tariladi.


Qo'shimcha bo'shliq va soya (shadow) animatsiyasi

Bu ikki element orasiga biroz qo'shimcha bo'shliq qo'yamiz: matnning ostiga .padding(.bottom, 20) qo'shamiz — bu uni biroz pastga suradi.

Va nihoyat, shu yerga ozgina soya (shadow) qo'shamiz — menimcha, soya buni juda chiroyli qiladi, va shu soyani ham animatsiya qilamiz. Padding'dan keyin .shadow qo'shamiz, color va radiusga ega variantdan foydalanamiz, va barcha parametrlarni alohida qatorlarga joylashtiramiz.

Soyaning rangini ham, fonimiz rangini animatsiya qilgan kabi, xuddi shunday animatsiya qilamiz — shu kodni nusxalab, soyaga joylaymiz, ammo to'liq rang o'rniga, ikkalasiga ham .opacity(0.7) qo'shamiz (chunki soya odatda haqiqiy tugma rangidan unchalik kuchli bo'lmasligi kerak): animate ? Color.red.opacity(0.7) : Color.accentColor.opacity(0.7).

Radius uchun ham animatsiya qilamiz, va animatsiya paytida buni ancha katta qilamiz: animate ? 30 : 10. xni 0 qilib qoldiramiz, yni esa ham animatsiya qilamiz — buni biroz pastga, tugmaning ostiga itarib qo'yishni xohlayman, chunki tugmamiz yuqoriga ko'tarilib turibdi: animate ? 50 : 30 (bu, asosan, padding'ning teskarisiga o'xshash miqdor):

.shadow(
    color: animate ? Color.red.opacity(0.7) : Color.accentColor.opacity(0.7),
    radius: animate ? 30 : 10,
    x: 0,
    y: animate ? 50 : 30
)

Demak, soyamiz endi tugmaning ostida. Live Preview'da "Play" tugmasini bosib ko'rsak — tugmamiz endi biroz suzib turganday, hovaga ko'tarilib-tushganday ko'rinadi, va soya ham tugma bilan birga animatsiya qilinmoqda — bu juda tabiy ko'rinadi.


Accent color va secondary accent color'ni sozlash

Endi ushbu videoda qilishim kerak bo'lgan so'nggi narsa — shu ranglarni yangilash, chunki bu ko'k rang — accent color, va biz tugmalarimizning barchasini yozganimizda, ular standart bo'yicha ko'k bo'lib chiqishining sababi aynan shu — accent color standart bo'yicha ko'k. Ammo biz bu accent color'ni biroz moslashtirishimiz mumkin, va men shu qizil rangni ham almashtirib, uni ham maxsus rangga aylantirmoqchiman.

Shuning uchun biz butun ilovamiz bo'ylab accent color'ni globalcha yangilash uchun, accent color va secondary accent colorni yaratamiz. Navigator'ni ochib, Assets papkasiga o'tamiz — u yerda allaqachon AccentColor deb nomlangan rang bor, standart bo'yicha ko'k. Shu rangga bosib, Inspector'ni ochamiz, "Show Color Panel"ni bosamiz, va rangni xohlagan rangimizga o'zgartiramiz — men "Crayons" bo'limidan, masalan, to'q binafsha (baqlajon rangi)ni tanlayman.

Endi accent color'imiz ko'k o'rniga binafsha bo'ldi. Endi shu ustunga o'ng tugmani bosib, yangi color set yaratamiz — buni SecondaryAccentColor deb ataymiz. Bu rang uchun "Appearances"ni None qilib qo'yamiz (hozircha bu shunchaki oq). Endi shu rangga bosib, rang panelini ochamiz, va bu safar binafshadan farqli boshqa bir rangni tanlaymiz — chunki bu rang shu binafshadan boshqa rangga animatsiya qilinadi. Men, masalan, jigarrang-qizil (maroon) rangini tanlayman, chunki bu binafshadan jigarrang-qizilga o'tish biroz tabiyroq ko'rinadi, binafshadan sariqqa o'tishga qaraganda (albatta, o'zingiz xohlagan istalgan rangni tanlashingiz mumkin).


Yangi ranglarni qo'llash

Endi NoItemsViewga qaytamiz. Bu qizil rangni yangi rangimizga almashtiramiz, ammo biz buni pastda ham, ya'ni soyada ham ishlatamiz, shuning uchun, ikki marta yozish o'rniga, buni ekranning yuqorisida alohida o'zgaruvchiga chiqaraman.

animatening ostida, let secondaryAccentColor = Color("SecondaryAccentColor") deb yozamiz — bu nom aynan biz Assets papkasida shu rangga bergan nom.

Endi shu secondaryAccentColorni olib, Color.red bo'lgan barcha joylarda ishlatamiz — pastda ham, ya'ni Color.red o'rniga secondaryAccentColor, va secondaryAccentColor.opacity(...) ko'rinishida:

let secondaryAccentColor = Color("SecondaryAccentColor")

...

.background(animate ? secondaryAccentColor : Color.accentColor)

...

.shadow(
    color: animate ? secondaryAccentColor.opacity(0.7) : Color.accentColor.opacity(0.7),
    radius: animate ? 30 : 10,
    x: 0,
    y: animate ? 50 : 30
)

Canvas'da "Resume" tugmasini bosib ko'rsak, yangi ranglarimiz qo'shilganini ko'rishimiz kerak (agar yig'ilmasa, Command+Shift+K bosib tozalab, qaytadan yig'ish kerak). Endi accent color — binafsha, va animatsiya qilganimizda, biz shu binafshadan jigarrang-qizilga animatsiya qilamiz — bu juda chiroyli ko'rinadi.


NoItemsView'ni ilovaga ulash

Endi ushbu videoda qilishimiz kerak bo'lgan so'nggi narsa — NoItemsViewni qolgan ilovamizga ulash. Navigator'ni ochib, ListViewni ochamiz, va "No items" matni o'rniga, endi NoItemsView()ni qo'shamiz, qavslarini ochib-yopamiz.

Simulyatorda "Run" tugmasini bosib, sinab ko'raylik. 1.5 soniyadan keyin animatsiyamiz boshlanadi — agar shu yerga bossak, keyingi ekranga o'tishimiz kerak, va orqaga qaytganimizda esa, animatsiya hamon davom etayotganini ko'ramiz. Yana sinab ko'raylik: "Add"ga o'tib, orqaga qaytamiz — bu juda chiroyli ko'rinmoqda. Agar element qo'shsak, bu ekran g'oyib bo'lishi kerak.

Element qo'shaylik: "My first item" deb yozib, saqlaymiz — endi bizda shu ekran bor, albatta, uni almashtirishimiz, so'ngra o'chirishimiz mumkin, va o'chirganimizda, bizning "elementlar yo'q" ekranimiz qaytadan paydo bo'lishi kerak.


Ekranga kirish animatsiyasi (transition)

Yakunlash uchun, ushbu ekranga kichik bir transition ham qo'shaylik — shunda u ekranga shunchaki birdaniga chiqib qolmasdan, balki biroz animatsiya bilan paydo bo'ladi. Shu NoItemsViewga .transition(AnyTransition.opacity.animation(.easeIn)) qo'shamiz — bu, ekranga chiqqanida, biroz so'nib kirish (fade-in) animatsiyasini beradi:

NoItemsView()
    .transition(AnyTransition.opacity.animation(.easeIn))

Simulyatorda yana bir bor "Run" tugmasini bosib sinab ko'raylik — animatsiyamiz boshlanadi, element qo'shamiz — bu ishlayapti, va o'chirganimizda, yangi ekranimiz, ekranga chiqqanida, shaffoflik (opacity) bo'yicha animatsiya qilinishi kerak — va bu haqiqatan ham shunday ishlaydi, bu juda chiroyli ko'rinadi.


Xulosa

Biz ushbu ilovani yakunlashga juda yaqinlashib qoldik.

Buy mea coffee