ViewModifier سفارشی در SwiftUI — از صفر تا صد
در این مقاله در مورد پروتکل ViewModifier صحبت خواهیم کرد. مستندات اپل ViewModifier را به صورت زیر تعریف میکنند:
یک مادیفایر که روی یک نما (view) یا مادیفایر نمای دیگر اعمال میشود و نسخه متفاوتی از آن تولید میکند.
یکی از سادهترین روشها برای استفاده از ViewModifier، گروهبندی استایلبندیهای تکراری برای نماها است. بدین ترتیب اگر یک دکمه آبی و یک دکمه سبز دارید و لازم است که حاشیه خاصی به آنها بدهید، بهتر است یک ViewModifier ایجاد کنید و استایلبندی و منطق مورد نیاز برای شیوه نمایش دکمهها را در آن قرار دهید.
در این راهنما سه ViewModifier ایجاد میکنیم. هر مثال یک روش متفاوت استفاده از ViewModifier را نمایش میدهد. از آنجا که از structs برای ایجاد این ViewModifier-های سفارشی استفاده میکنیم، به همه کارکردهای یک struct معمولی دسترسی خواهیم داشت و به این ترتیب امکان ایجاد ViewModifier-های بسپار قدرتمند را مییابیم که چیزی بسیار فراتر از ViewModifier استایلبندی هستند.
الزامات ViewModifier
ViewModifier یک پروتکل است.
برای تطبیق با پروتکل ViewModifier باید یک متد را پیادهسازی کنید:
1struct BasicViewModifier: ViewModifier {
2 func body(content: Content) -> some View {
3 return content
4 }
5}
کد فوق کاری انجام نمیدهد، اما ابتداییترین پیادهسازی پروتکل ViewModifier را نشان میدهد.
گام 1: استایلبندی نما با یک ViewModifier
اکنون که مراحل مقدماتی را توضیح دادیم، میتوانیم شروع به ایجاد نخستین ViewModifier خود بکنیم. به این منظور یک struct به نام GreenButtonStyle میسازیم. این struct به ما امکان میدهد که به سادگی یک دکمه سبز ایجاد کنیم که حاشیه سبز تیره دارد. کد آن به صورت زیر است:
1struct GreenButtonStyle: ViewModifier {
2 func body(content: Content) -> some View {
3 return content
4 .foregroundColor(.white)
5 .background(Color.green)
6 .border(Color(red: 7/255,
7 green: 171/255,
8 blue: 67/255),
9 width: 5)
10 }
11}
Content مشابه View برای استایلبندی یا افزودن ژستها عمل میکند. بر اساس مستندات اپل ViewModifier مادیفایری است که میتواند روی یک View یا Modifier دیگر اعمال شود. در این مادیفایر GreenButtonStyle از foregroundColor. برای سفید کردن رنگ متن استفاده میکنیم. سپس رنگ پسزمینه را به سبز پیشفرض که اپل ارائه میکند تغییر میدهیم و در نهایت یک حاشیه اضافه میکنیم. این حاشیه به رنگ سبز تیره است و عرض آن را روی 5 قرار میدهیم.
در ادامه یک دکمه به اپلیکیشن خود اضافه میکنیم تا از این مادیفایر روی آن استفاده کنیم. ContentView خود را با کد زیر عوض کنید:
1struct ContentView: View {
2 var body: some View {
3 Button(action: {
4 print("Button Pressed")
5 }, label: {
6 Text("Button").padding()
7 })
8 .modifier(GreenButtonStyle())
9 }
10}
برای استفاده از مادیفایر باید از متدی به نام modifier استفاده کنیم. در مثال فوق یک وهله جدید از GreenButtonStyle ایجاد میکنیم که از طریق متد modifier ارسال میشود. با شروع به استفاده از آن با قدرتش آشنا میشوید و میتوانید یک struct ارسال کنید که امکان اجرای کارهای بسیار جالبی را فراهم میسازد. در ادامه این مقاله برخی از کاربردهای پیشرفته آن را معرفی خواهیم کرد.
اینک اگر اپلیکیشن را اجرا کنید چیزی مانند زیر میبینید:
ما نام ViewModifier خود را GreenButtonStyle گذاشتیم، اما این مادیفایر در عمل روی هر نمای دیگری نیز عمل میکند. این یکی از دلایلی است که چرا ViewModifier چنین قدرتمند است. اگر میخواستیم میتوانستیم به سادگی یک ViewModifier به نام BlueButtonStyle بسازیم. این کار را در ادامه انجام میدهیم:
1struct BlueButtonStyle: ViewModifier {
2 func body(content: Content) -> some View {
3 return content
4 .foregroundColor(.white)
5 .background(Color.blue)
6 .border(Color(red: 7/255,
7 green: 42/255,
8 blue: 171/255),
9 width: 5)
10 }
11}
اکنون میتوانید GreenButtonStyle را با BlueButtonStyle عوض کنید تا دکمهتان آبی شود. اگر اینک اپلیکیشن را اجرا کنید، باید با چیزی مانند تصویر زیر مواجه شوید:
پاکسازی کد
ما قصد نداشتیم این کد را پاکسازی کنیم، اما با کمی پاکسازی میتوانیم آن را بهبود ببخشیم. مطمئن هستیم که این کد میتواند بیش از این نیز پاکسازی شود، اما چنان که گفتیم مقصود ما در این راهنما این نیست.
یک struct جدید بسازید و نام آن را StyledButton بگذارید:
1struct StyledButton: ViewModifier {
2 enum ButtonColor {
3 case green
4 case blue
5
6 func backgroundColor() -> Color {
7 switch self {
8 case .green:
9 return Color.green
10 case .blue:
11 return Color.blue
12 }
13 }
14
15 func borderColor() -> Color {
16 switch self {
17 case .green:
18 return Color(red: 7/255,
19 green: 171/255,
20 blue: 67/255)
21 case .blue:
22 return Color(red: 7/255,
23 green: 42/255,
24 blue: 171/255)
25 }
26 }
27 }
28
29 let buttonColor: ButtonColor
30
31 func body(content: Content) -> some View {
32 return content
33 .foregroundColor(.white)
34 .background(buttonColor.backgroundColor())
35 .border(buttonColor.borderColor(),
36 width: 5)
37 }
38}
ViewModifier فوق کاملاً ساده است. اگر نگاهی به متد body داشته باشیم، میبینیم که مانند GreenButtonStyle و BlueButtonStyle قبلی ما است. تفاوت عمده در این بخش آن است که ما به جای color از buttonColor استفاده کردهایم.
دلیل این مسئله آن است که ما همه منطق رنگ را به یک enum به نام ButtonColor انتقال دادهایم. این enum دو حالت به صورت سبز و آبی دارد. همچنین دو متد به نامهای backgroundColor و borderColor دارد. این متدها به ما امکان میدهند که به سادگی رنگ پسزمینه و رنگ حاشیه را بر اساس آن مقداری که در مقداردهی StyledButton استفاده میکنیم، تعیین نماییم. ما رنگ را با مشخصه buttonColor کنترل میکنیم. این مشخصه از نوع ButtonColor است و به ما امکان میدهد که رنگی که میخواهیم را در زمان ایجاد وهلهای از StyledButton ارسال کنیم.
در ادامه طرز کار آن را در عمل بررسی میکنیم. ContentView را با محتوای زیر عوض کنید:
1struct ContentView: View {
2 var body: some View {
3 Button(action: {
4 print("Button Pressed")
5 }, label: {
6 Text("Button").padding()
7 })
8 .modifier(StyledButton(buttonColor: .blue))
9 }
10}
تنها تغییر در modifier است. ما به جای GreenStyleButton یا BlueStyleButton از StyledButton استفاده کردهایم. این StyledButton یک آرگومان برای buttonColor میگیرد. در این مورد مقدار .blue را به عنوان رنگ دکمه ارسال میکنیم. اگر اینک اپلیکیشن را اجرا کنیم میبینیم که نتیجه همانند وضعیت پیشین است که از BlueButtonStyle استفاده کرده بودیم.
گام 2: ردگیری لمس دکمه
در این بخش در مورد روش پیادهسازی منطق لمس دکمه در SwiftUI صحبت میکنیم. زمانی که از ViewModifier-ها استفاده میکنیم، میدانیم که یکی از روشهای ممکن است، اما شاید بهترین روش نباشد. در مثال بعدی که برای مقاصد مفهومی/نمایشی ارائه شده است این مسئله را بررسی میکنیم. در این بخش یک struct جدید به نام Track میسازیم که یک آرگومان به نام eventName میگیرد.
1struct Track: ViewModifier {
2 let eventName: String
3 func body(content: Content) -> some View {
4 return content.simultaneousGesture(TapGesture().onEnded({
5 print(self.eventName)
6 }))
7 }
8}
احتمالاً متوجه شدید که از چیزی به نام simultaneousGesture استفاده کردیم. بدون استفاده از آن نمیتوانستیم مقدار را در اکشن Button پرینت کنیم. اگر به مستندات اپل نگاه کنیم، میبینیم که در مورد simultaneousGesture چنین نوشته است:
- gestures را به خودش الصاق میکند به طوری که به صورت همزمان با ژستهای تعریفشده از سوی self پردازش میشود.
- این یک روش برای افزودن ژستهای دیگر به view-یی است که از قبل ژستهایی دارد. برای نمونه اگر از onTapGesture استفاده کرده باشید، کار نخواهد کرد و باید آن را در یک simultaneousGesture قرار دهید.
- این تنها نکته پیچیدهای است که در مورد این مادیفایر وجود دارد. دلیل اصلی گام 2 این است که به شما چیزی نشان دهیم که بتوانید از ViewModifier برای مقصودی به جز استایلبندی استفاده کنید.
برای تست مورد فوق ContentView خود را با کد زیر عوض کنید:
1struct ContentView: View {
2 var body: some View {
3 Button(action: {
4 print("Button Pressed")
5 }, label: {
6 Text("Button").padding()
7 })
8 .modifier(Track(eventName: "simpleEvent"))
9 }
10}
یکبار دیگر میبینیم که تنها چیزی که تغییر یافته، modifier است و به جای StyledButton عبارت Track را به عنوان آرگومان ارسال کردهایم.
گام 3: چندین ViewModifier
در گام قبلی مادیفایر StyledButton را حذف کردیم. اگر بخواهید ردگیری را نیز علاوه بر استایلبندی اضافه کنید، میتوانید را به صورت زیر تغییر دهید:
1struct ContentView: View {
2 var body: some View {
3 Button(action: {
4 print("Button Pressed")
5 }, label: {
6 Text("Button").padding()
7 })
8 .modifier(Track(eventName: "simpleEvent"))
9 .modifier(StyledButton(buttonColor: .blue))
10 }
11}
چنان که میبینید تنها کاری که انجام دادیم، افزودن یک مادیفایر جدید است. اگر خواسته باشیم، میتوانیم ViewModifier-های دیگر نیز اضافه کنیم. حتی میتوانیم یک ViewModifier جدید ایجاد کنیم که صرفاً ViewModifier خاصی را اعمال میکند.
گام 4: ViewModifier-های دارای حالت
هر مثالی که تا به اکنون بررسی کردیم، ما را به ساخت مثال زیر رهنمون ساخته است. در این مرحله قصد داریم یک ViewModifier بسازیم که حاشیه Button را بر مبنای تعداد دفعات لمس کاربر روی دکمه تغییر میدهد. یکی از کاربردهای این ViewModifier میتواند کنترل toggle باشد، اما با حفظ ایده state@ میتوانید برخی ViewModifier-های پیشرفته بسته به الزامات اپلیکیشن خود بسازید. هیچ کدام از این کدها پیچیده نیستند؛ اما به جای این که کد نهایی را سریعاً بنویسیم قصد داریم آن را گام به گام بسازیم تا هر بخش را توضیح دهیم.
ایجاد struct مبنا
کار خود را با struct مبنا آغاز میکنیم و نام آن را BorderChange میگذاریم. برای پیادهسازی مبنا باید حاشیه Button را سیاه تعیین کرده و borderWidth را روی 5 تنظیم کنیم.
1struct BorderChange: ViewModifier {
2 func body(content: Content) -> some View {
3 return content.border(Color.black, width: 5)
4 }
5}
ContentView خود را با کد زیر عوض کنید:
1struct ContentView: View {
2 var body: some View {
3 Button(action: {
4 print("Button Pressed")
5 }, label: {
6 Text("Button").padding()
7 })
8 .modifier(BorderChange())
9 }
10}
اینک اگر اپلیکیشن را build کرده و اجرا کنید، ظاهر آن مانند زیر خواهد بود:
ایجاد enum برای BorderColor
درون ViewModifier به نام BorderChange باید یک enum به نام BorderColor اضافه کنیم. با انجام این کار میتوانیم یک مشخصه currentState اضافه کنیم که در ادامه هنگامی که بین رنگهای مختلف سوئیچ میکنیم، استفاده خواهد شد. Enum و مشخصه زیر را درون BorderChange بالاتر از تابع body اضافه کنید:
1enum BorderColor {
2 case black
3 case blue
4 case red
5
6 func color() -> Color {
7 switch self {
8 case .black:
9 return .black
10 case .blue:
11 return .blue
12 case .red:
13 return .red
14 }
15 }
16}
17
18@State var currentState = BorderColor.black
ما یک enum برای رنگهایی که میخواهیم پشتیبانی کنیم میسازیم و سپس تابعی به نام color داریم که نوع بازگشتی آن Color است، به این ترتیب میتوانیم رنگ حاشیه نمای خود را تنظیم کنیم. currentState از یک wrapper مشخصه State@ استفاده میکند، زیرا این مقداری است که در ادامه وقتی کاربر روی دکمه ضربه بزند تغییر خواهد کرد. سپس تابع body خود را بهروزرسانی میکنیم. به این منظور محتوای تابع body را با کد زیر عوض میکنیم:
1func body(content: Content) -> some View {
2 return content.border(self.currentState.color(), width: 5)
3}
تغییری که در اینجا اتفاق افتاده، این است که به جای استفاده از Color.black به عنوان آرگومان نخست برای تابع border از ()self.currentState.color استفاده میکنیم. اگر اینک اپلیکیشن را build و اجرا کنید، با تصویری مانند زیر مواجه میشوید:
افزودن منطق چرخش رنگ
منظور از منطق چرخش رنگ این است که یک متد جدید به BorderColor خود اضافه میکنیم تا رنگ حاشیه دکمهها در هر بار ضربه کاربر تغییر یابد و همچنین بین رنگهای مختلف به صورت غیرقطعی بچرخد. نخستین کاری که باید انجام دهیم، تغییر enum به نام BorderColor برای تطبیق با CaseIterable است. با انجام این کار امکان چرخش روی حالتهای مختلف در BorderColor پدید میآید. BorderColor خود را طوری بهروزرسانی کنید که مانند زیر باشد:
1enum BorderColor: CaseIterable
اکنون متد next را طوری اضافه میکنیم که روی رنگهای مختلف بچرخد:
1func next() -> BorderColor {
2 // 1
3 let allColors = BorderColor.allCases
4
5 // 2
6 if let lastIndex = allColors.lastIndex(of: self) {
7
8 // 3
9 if lastIndex + 1 == allColors.count {
10 return allColors[0]
11 }
12
13 // 4
14 return allColors[lastIndex + 1]
15 }
16 // 5
17 return self
18}
در کد فوق:
- allCases. یک مجموعه از همه حالتها را در enum به نام BorderColor بازگشت میدهد.
- ما lastIndex مقدار کنونی را از مجموعه allColors میگیریم. استفاده از این lastIndex در این مثال مشکلی ندارد، زیرا از یک enum استفاده میکنیم که امکان وجود مقادیر تکراری را نمیدهد و از این رو lastIndex همواره مقدار صحیح را بازگشت میدهد.
- بررسی میکنیم که آیا lastIndex+1 برابر با اندازه مجموعه allColors است یا نه. اگر چنین باشد، رنگ نخست را از مجموعه بازگشت میدهیم.
- رنگ بعدی را بازگشت میدهیم.
- اگر lastIndex برابر با nil باشد، self بازگشت میدهیم.
منطق ضربه روی دکمه
اکنون که همه منطق برای enum به نام BorderColor تکمیل شده است، میتوانیم به بخش آخر کد برسیم. هنگامی که کاربر روی دکمه ضربه میزند، باید بین رنگهای موجود چرخیده و رنگ حاشیه دکمه را عوض کنیم. تابع body را به صورت زیر بهروزرسانی کنید:
1func body(content: Content) -> some View {
2 return content.border(self.currentState.color(),
3 width: 5).simultaneousGesture(TapGesture().onEnded({
4 self.currentState = self.currentState.next()
5 }))
6}
آن چه تا کنون انجام دادهایم افزودن یک TapGesture جدید است. به طور معمول، زمانی که یک TapGesture جدید اضافه میکنید، میتوانید از .onTapGesture استفاده کنید، اما در این حالت امکان چنین کاری وجود ندارد. زیرا دکمه از قبل به یک TapGesture پاسخ داده و ما باید از یک simultaneousGesture استفاده کنیم. بر اساس مستندات اپل، امکان افزودن یک ژست دیگر برای پردازش همزمان با ژستهای تعریف شده از سوی نما را میدهد.
هنگامی که ژست ما پایان مییابد، با فراخوانی متد ()next اقدام به بهروزرسانی currentState خود میکنیم. ما از مقدار بازگشتی از متد ()next برای بهروزرسانی currentState استفاده میکنیم. هنگامی که این کار را انجام دهیم، رنگ حاشیه به رنگ بعدی در enum به نام BorderColor تغییر مییابد، زیرا مشخصه currentState دارای یک پوشش مشخصه State@ است. اگر اینک اپلیکیشن را بیلد و اجرا کنید، باید رفتاری مانند زیر ببینید:
امیدواریم از مطالعه این راهنما بهره آموزشی لازم را برده باشید. ViewModfier-ها میتوانند ابزار بسیار مفیدی باشند و شما میتوانید کاربردهای بسیار بیشتری برای آنها در اپلیکیشنهای خود پیدا کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامه نویسی Swift (سوئیفت) برای برنامه نویسی iOS
- مجموعه آموزشهای دروس علوم و مهندسی کامپیوتر
- آموزش سوئیفت (Swift) — مجموعه مقالات مجله فرادرس
- عملگرهای سفارشی در سوئیفت — از صفر تا صد
==