ساخت کنترل Overlay تعاملی با SwiftUI — از صفر تا صد
در این مقاله به بررسی شیوه ساخت کنترل Overlay تعاملی با SwiftUI میپردازیم. به طور خاص قصد نداریم به بررسی کنترلهای Overlay به عنوان روشی برای تغییر دادن خود طراحی نگاه کنیم، بلکه میخواهیم یک کنترل تعاملی بسازیم که با والدین خود ارتباط برقرار سازد.
با این که کنترلهای Overlay گاهی اوقات مناسب به نظر نمیرسند، با برخی استانداردهای طراحی ناسازگار هستند و در برخی موارد به نظر میرسد روی همه چیز قرار گرفتهاند، اما تغییری در این واقعیت ایجاد نمیکند که برخی افراد کنترل overlay تعاملی را دوست دارند. این کنترلهای تعاملی مانند منوی بازشدنی راست-کلیک روی رایانههای دسکتاپ عمل میکنند. این کنترلها یک روش ساده برای جلب توجه کاربر به UI محسوب میشوند و برخی اطلاعات و قابلیتهای موجود در آن نتیجه را در اختیار وی قرار میدهند. بهترین نکته در مورد این نوع کنترلها آن است که زمانی عرضه میشوند که کاربر به آنها نیاز داشته باشد و تقاضا کند.
با بهرهگیری از رویکرد UI اعلانی جدید در SwiftUI، کنترلهای Overlay تعاملی از برخی مزیتهای تازه برخوردار شدهاند. در مورد تازهکارها این مزیت آن است که اتصال دادن یک نمای Overlay به نمای والد نیازمند کد بسیار کمتری است. منظور از کد این است که Overlay-ها نیز در دسته عناصر سازنده UI جای میگیرند و دیگر لازم نیست در استوریبوردها و یا XIB-های جداگانه قرار گیرند. در پشت صحنه مانند نوعی در پشتی مخفی به اپلیکیشن اتصال پیدا کنند. در نهایت به لطف این ترکیب میتوانیم روابط قویتری بین کنترلهای Overlay و والدینشان سازیم. منظور از ترکیب، گرد هم آوردن خانوادههای UI در کنار هم است.
ساخت رابط کاربری
Xcode 11 خود را باز کنید و یک پروژه جدید با فعالسازی SwiftUI باز کنید. فایل از پیش ساخته شده ContentView.swift را باز کنید و نمای متنی Hello World را باقی بگذارید. این والد ما خواهد بود.
برای ساخت UI ابتدایی خود تنها کاری که باید انجام دهیم، ساخت یک فریم پیرامون نمای متنی و تعیین یک رنگ است و هدف ما ساخت یک کنترل Overlay است که آن رنگ را تغییر دهد. در این مسیر ظاهر آن را مقداری مناسبسازی نیز میکنیم.
1Text(“Hello World”)
2 .frame(width: 100, height: 100)
3 .background(Color.blue)
استفاده از ZStack
اکنون پیش از تنظیم Overlay باید به ZStack اشاره کنیم. ZStack به نماهای stack امکان میدهد که روی همدیگر قرار گیرند و وانمود کنیم که یک overlay هستند. این وضعیت جالبی است، اما لزوماً در کد موجب نمیشود که یک نما والد دیگری شود. یک overlay صریحاً یک نمای لایهبندی شده را به عنوان فرزند دیگری اعلان میکند و این مسئله از روی پارهای مشخصهها مانند مشخصه offset یک نمای overlay که بر مبنای مشخصههای والدش است، هویدا است. میتوان از ZStack برای ساخت مصنوعی چنین روابطی بهره گرفت، اما این وضعیت همچنان در عمل یک همنیا محسوب میشود.
از آنجا که میخواهیم کنترل ما به طور خاص روی والدی که قرار است با آن تعامل پیدا کند متمرکز باشد، کار خود را با یک overlay ادامه میدهیم. اینک overlay خود را روی نمای متنی اعلان میکنیم. در حال حاضر صرفاً یک دایره میسازیم تا در پیشنمایش دیده شود.
1.overlay(Circle())
تا به اینجا که زشت دیده میشود؛ اما جای نگرانی نیست، با کمی کار بیشتر روی UI میتوانیم دایره خود را تغییر دهیم. برای این دایره یک Stroke به قدر کافی بزرگ تعیین میکنیم که بتوان روی آن ضربه زد و همچنین یک فریم به قدر کافی بزرگ را به دایره میدهیم تا فریم متنی ما را احاطه کند.
1Circle()
2 .stroke(Color.red, lineWidth: 30)
3 .frame(width: 300, height: 300)
اینک که این تنظیمات را داریم، حلقه خود را تعاملی میسازیم. هدف ما این است که نمای متنی را به رنگ حلقه درآوریم. به این منظور یک حالت رنگی ایجاد میکنیم که پسزمینه متن به آن تنظیم میشود و با یک ژست ضربه زدن روی دایره تعیین میشود.
1//Add this to our ContentView
2@State var textColor = Color.blue
3//Change this for our Text View
4.background(textColor)
5//Add this to our Circle
6.onTapGesture { self.textColor = Color.red }
تنظیم زمان نمایش ZStack
در نهایت کاری میکنیم که overlay تنها زمانی که عرضه میشود، نمایان شود. در این حالت، از ژست ضربه زدن دیگری روی نمای متنی بهره میگیریم. بدین ترتیب حالت دیگری ایجاد میشود که به عنوان شرط ما برای نمایش یا پنهان کردن overlay مورد استفاده قرار میگیرد.
1//Add this to our ContentView
2@State var showOverlay = false
3//Add this to our Text View
4.onTapGesture { self.showOverlay.toggle() }
5//Cmd+Click Circle, select “Make Conditional”,
6//and set self.showOverlay as our condition
7VStack {
8 if self.showOverlay {
9 Circle()
10 .stroke(Color.red, lineWidth: 30)
11 .frame(width: 300, height: 300)
12 .onTapGesture { self.textColor = Color.red }
13 } else {
14 EmptyView()
15 }
16}
هنگامی که دایره را از طریق Cmd+Click به صورت شرطی درمیآوریم، Xcode به صورت خودکار آن را درون یک ZStack قرار میدهد و یک گزاره if/else تعریف میکند که else آن یک ()EmptyView است. به نظر میرسد این وضعیت ترجیحی اپل برای نمایش یا پنهان کردن نماها است. البته این گزاره ابتدایی به نظر میرسد و ما میخواهیم یک گزاره سهتایی داشته باشیم، اما فعلاً حالت جدیدی به صورت شرط قرار داده و آن را به حال خود رها میکنیم.
اینک به هدف خود دست یافتهایم. دستاوردهای ما به شرح زیر بوده است:
- یک overlay ایجاد کردیم.
- overlay ما زمانی که کاربر روی آن کلیک میکند نمایان میشود.
- تعامل با overlay ما یک تغییر در نمای والد ایجاد میکند.
کدی که تا به اینجا نوشتهایم به صورت زیر است:
فایل OverlayControl.swift
1struct ContentView: View {
2 @State var textColor = Color.blue
3 @State var showOverlay = false
4
5 var body: some View {
6 Text("Hello World")
7 .frame(width: 100, height: 100)
8 .background(textColor)
9 .onTapGesture { self.showOverlay.toggle() }
10 .overlay(
11 VStack {
12 if self.showOverlay {
13 Circle()
14 .stroke(Color.red, lineWidth: 30)
15 .frame(width: 300, height: 300)
16 .onTapGesture { self.textColor = Color.red }
17 } else {
18 EmptyView()
19 }
20 }
21 )
22 }
23}
زیباسازی overlay
ما موفق شدیم که یک overlay تعاملی بسازیم اما UI/UX آن چندان زیبا نیست. خوشبختانه میتوانیم روی آن کار کنیم و ظاهرش را بهبود ببخشیم.
ایجاد نسخه بهبود یافته فوق از این کنترل تنها به 19 خط کد نیاز دارد که در ادامه مشاهده میکنید. کد زیر دایره را میگیرد و آن را درون یک نمای سفارشی قرار میدهد، همچنین از trim برای ایجاد افکت کمان استفاده شده و از یک ZStack استفاده کردهایم تا با استفاده از چند کمان به همراه حلقه ForEach ظهر یک دایره کامل را ایجاد کنیم. سپس برخی اجزای UI و انیمیشنها را اضافه کردهایم. اینک کنترل Overlay ما ظاهر بسیار خوشایندتری یافته است.
1struct ContentView: View {
2 @State var showOverlay = false
3 @State var curColor = Color.blue
4
5 var body: some View {
6 Text("Hello World")
7 .frame(width: 100, height: 100)
8 .background(curColor)
9 .cornerRadius(20)
10 .onTapGesture { self.showOverlay.toggle() }
11 .overlay( ArcSelectionView(isShowing: self.$showOverlay, curColor: self.$curColor) )
12 }
13}
14
15struct ArcSelectionView: View {
16 @Binding var isShowing : Bool
17 @Binding var curColor : Color
18
19 let colors = [Color.blue, Color.red, Color.green, Color.yellow]
20
21 var body: some View {
22 ZStack {
23 ForEach(1 ..< 5, id: \.self) { item in
24 Circle()
25 .trim(from: self.isShowing ? CGFloat((Double(item) * 0.25) - 0.25) : CGFloat(Double(item) * 0.25),
26 to: CGFloat(Double(item) * 0.25))
27 .stroke(self.colors[item - 1], lineWidth: 30)
28 .frame(width: 300, height: 300)
29 .animation(.linear(duration: 0.4))
30 .onTapGesture {
31 self.curColor = self.colors[item - 1]
32 self.isShowing.toggle()
33 }
34 }
35 }
36 .opacity(self.isShowing ? 1 : 0)
37 .rotationEffect(.degrees(self.isShowing ? 0 : 180))
38 .animation(.linear(duration: 0.5))
39 }
40}
حالتهای یکپارچه نماها
اگر بخواهیم از موضوع مقاله چندان دور نشویم باید در مورد روش مدیریت حالتهای کنترل خود صحبت کنیم. یک تفاوت بزرگ بین نسخه پایه و بهبود یافته کنترل فوق این است که overlay به عنوان یک نمای مستقل استخراج یافته است. ما همچنین حالتهای خود را به عنوان پارامترهای اتصال به نمای جدید ارسال میکنیم.
اگر با نمونه پروژهها و مقالات SwiftUI آشنا باشید، میدانید که گردش داده بین سلسلهمراتب نما علیرغم این که ادعا میشود یک روش «تعریف شدنی» (Definitive) است به چند طریق مختلف مدیریت میشود.
دلیل این که ما روش خودمان را انتخاب کردیم دو مورد است:
- والدی که overlay را مصرف میکند نباید با مشخصه نماهای فرعی درگیر شود، مگر این که مختص نیازهای والد باشد. دلیلی وجود ندارد که وقتی سلسلهمراتب ما میتواند تمیز و خودکفا باشد، آن را شلوغ بکنیم.
- قابلیت استفاده مجدد و کنار هم نگه داشتن context برای ما جهت ایجاد تابعها و البته نماها بسیار مهم است. اگر این وضعیت روی نماهای فرعی تأثیر میگذارد و میتواند درون یک نمای فرعی قرار گیرد، چرا باید تلاش کنیم آن را در جای دیگری قرار دهیم؟ در این مورد نیز این کار موجب شلوغ شدن والد میشود و استفاده از نمای فرعی در جای دیگر نیازمند داشتن ارجاعی به آن است که در والد اصلی انجام یافته و آن را در والد مورد نظر کپی میکنیم.
نکاتی در مورد باگهای SwiftUI
در زمان نگارش این مقاله یعنی Xcode 11 Beta 7 قطعاً برخی باگها در SwiftUI وجود دارند. برای نمونه در شرط سهتایی برای مشخصه trim دایره کد ما کامپایل نمیشد و باید آیتم را به صورت double درمیآوردیم و چنان که میدانید امکان اجرای هیچ نوع عملیانی درون این تبدیل وجود ندارد. بنابراین هنگامی که با حجم بالای کدنویسی در آن یخش مواجه میشوید، علت آن را بدانید.
سخن پایانی
روشهای زیاد دیگری برای ویرایش کردن این overlay و یا ایجاد انواع دیگر وجود دارند. میتوان از انواع مختلف ژستها مانند فشردن طولانی به عنوان محرک استفاده کرد. همچنین میتوان از یک انیمیشن اسپرینت به جای یک انیمیشن خطی روی Stack استفاده کرد.
در نهایت همه چیز به UI/UX اپلیکیشن بستگی دارد و این که کدام حالت مناسبتر است. یکی از مهمترین نکاتی که باید به خاطر سپرد این است که کاری کنید که کاربر وجود کنترل overlay را بداند. این کار از طریق یک ماسک آموزش یکبارمصرف با استفاده از Z-Layer روی نما و یا در مرحله باز شدن اپلیکیشن میسر است. زمانی که کاربر اقدام به بررسی کنترل overlay کرد و توانست آن را باز کند، مطمئن میشویم که قابلیتها و تجربیات جالب دیگر اپلیکیشن را نیز مورد بررسی قرار خواهد داد.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامه نویسی Swift (سوئیفت) برای برنامه نویسی iOS
- مجموعه آموزشهای دروس علوم و مهندسی کامپیوتر
- آموزش سوئیفت (Swift) — مجموعه مقالات مجله فرادرس
- ساخت اپلیکیشن کرنومتر با SwiftUI — از صفر تا صد
==