ساخت اپلیکیشن کرنومتر با SwiftUI — از صفر تا صد

۸۶ بازدید
آخرین به‌روزرسانی: ۱۲ مهر ۱۴۰۲
زمان مطالعه: ۸ دقیقه
ساخت اپلیکیشن کرنومتر با SwiftUI — از صفر تا صد

در این مقاله روش ساخت یک اپلیکیشن کرنومتر با استفاده از SwiftUI توضیح داده می‌شود. در این نوشته شیوه ایجاد نماهای سفارشی با استفاده از SwiftUI و همچنین برخی مبانی اولیه layout و استفاده از ObjectBinding مورد بررسی قرار خواهد گرفت.

گرچه ما قصد داریم در این نوشته مراحل ساخت یک اپلیکیشن کرنومتر را توضیح دهیم، اما قصد نداریم وارد جزییات منطق کد کرنومتر شویم، چون خارج از حوصله این مقاله است. این راهنما بیشتر در مورد طراحی رابط کاربری اپلیکیشن‌ها با استفاده از SwiftUI است.

build-a-stopwatch-app-with-swiftui-

در تصویر فوق می‌توانید ببینید که در پایان کار ظاهر اپلیکیشن ما چگونه خواهد بود. گرچه نکته خاصی ندارد اما کاملاً عملیاتی است.

زمانی که اپلیکیشن کامل شود، قادر خواهید بود کرنومتر را به کار بیندازید، دور‌ه‌های زمانی را ثبت کنید و یا کرنومتر را متوقف یا معلق کنید.

حال که می‌دانیم چیزی که خواهیم ساخت چه شکلی است و چه کارکردهایی دارد، زمان آغاز به کار ساخت آن فرا رسیده است.

گام 1: افزودن کلاس StopWatch به پروژه

ما این کلاس را صرفاً برای این راهنما ساخته‌ایم. آن را نمی‌توان آماده بهره‌برداری دانست، چون هنوز به درستی تست نشده است و کد کتابخانه نیز چندان خوب نوشته نشده است. این کتابخانه صرفاً برای مقاصد آموزشی آمده شده است.

برای مشاهده کلاس StopWatch به این لینک (+) بروید و یا می‌توانید آن را از این گیست (+) دانلود کنید.

گام 2: افزودن ()timer Text

در کلاس اصلی ContentView و درون متغیر body باید یک VStack اضافه کنیم. بدن ترتیب کد ما به صورت زیر درمی‌آید:

1struct ContentView : View {
2    @ObjectBinding var stopWatch = StopWatch()
3    
4    var body: some View {
5        VStack {
6            // Code here
7        }
8    }
9}

این VStack همه کدهای مربوط به رابط کاربری را در خود نگهداری می‌کند. اکنون که VStack خود را داریم می‌توانیم نمای Text را نیز اضافه کنیم تا متن تایمر را نمایش دهد.

در SwiftUI نمای والد نمی‌تواند اندازه را روی نمای فرزند یا فرزندان الزام کند و از این رو باید بیشترین تلاش خود را روی نمای Text که می‌خواهیم اضافه کنیم قرار دهیم. کد زیر را درون VStock که اینک اضافه کردیم قرار دهید.

1Text(self.stopWatch.stopWatchTime)
2    .font(.custom("courier", size: 70))
3    .frame(width: UIScreen.main.bounds.size.width,
4           height: 300,
5           alignment: .center)

در کد فوق یک نمای Text جدید ایجاد می‌کنیم. متن پیش‌فرض را به صورت 00:00:00 قرار می‌دهیم که در ادامه به مقدار مناسب به‌روزرسانی خواهد شد.

پس از آن که کارمان با سبک‌بندی آغاز شد، فونت را به courier تغییر می‌دهیم، زیرا این فونت به دلیل این که monospace است عملکرد بهتری ارائه می‌کند. زمانی که از فونت استاندارد سیستم استفاده می‌کنیم، متن ممکن است بلرزد زیرا به طور مداوم تغییر می‌یابد. همچنین اندازه فونت را روی 70 تنظیم می‌کنیم زیرا زیبا و بزرگ است. کار بعدی که باید انجام دهیم این است که فریم را به‌روزرسانی کنیم. ما می‌خواهیم عرض فریم کل صفحه را بپوشاند، اما همزمان می‌خواهیم ارتفاع آن نیز زیاد باشد. بنابراین ارتفاع را روی 300 تنظیم می‌کنیم. از نظر فنی می‌توان تراز center. را بدون تغییر بر جای گذاشت، زیرا تنظیمات پیش‌فرض چنین است. اپلیکیشن ما اینک باید مانند زیر باشد:

نمودارهای React Native

البته این دقیقاً آن چیزی که می‌خواهیم نیست، اما برای شروع خوب است.

گام 3: ایجاد نمای دکمه‌ها

پیش از آن که کار خود را با لی‌آوت به پایان ببریم، باید دکمه‌ها را ایجاد کنیم. بدین ترتیب یک نمای سفارشی جدید ساخته می‌شود. کارکرد دکمه‌ها نیز مانند متن، بسته به حالت کرنومتر تغییر پیدا خواهد کرد.

برای مثال وقتی تایمر متوقف شود، ما دو دکمه خواهیم داشت که یکی برای ریست کردن تایمر و دیگری برای استارت تایمر استفاده می‌شود. اگر تایمر مشغول زمان‌بندی باشد، آن دکمه‌ها به دکمه دور (lap) و دکمه مکث (pause) تغییر می‌یابند.

از آنجا که اپلیکیشن کاملاً کوچک است، باید منطق کار را در دکمه قرار دهم. اگر اپلیکیشن بزرگ‌تر باشد، می‌توان منطق را از دکمه جدا کرد و از چیز دیگری برای ایجاد دکمه استفاده کرد که از طریق پارامترهای ارسالی به آن ایجاد می‌شود.

دکمه‌ها به چه چیزهایی نیاز دارند؟

هر دکمه باید دو عمل انجام دهد و دو رشته می‌گیرد. همچنین باید یک رنگ داشته باشند و باید بداند که آیا تایمر مکث یافته است یا نه.

کد

برای انجام این کار، یک struct جدید به نام StopWatchButton می‌سازیم و آن را به صورت زیر با پروتکل View هماهنگ می‌کنیم:

1struct StopWatchButton : View {
2    var body: some View {
3        // Buttons coming soon
4    }
5}

اینک می‌توانیم مشخصه‌هایی را درست بالاتر از مشخصه body اضافه کنیم. Struct خود را طوری تغییر می‌دهیم که به صورت زیر دربیاید:

1struct StopWatchButton : View {
2    var actions: [() -> Void]
3    var labels: [String]
4    var color: Color
5    var isPaused: Bool
6    var body: some View {
7        // Buttons coming soon
8    }
9}

این عالی است و اینک می‌توانیم منطق دکمه را در body اضافه کنیم. نخستین چیزی که باید به دکمه اضافه کنیم، یک متغیر عرض دکمه در body است. این مقدار کاملاً تصادفی است، اما برای نیازهای ما مناسب است. مشخصه body باید اینک به صورت زیر درآمده باشد:

1var body: some View {
2    let buttonWidth = (UIScreen.main.bounds.size.width / 2) - 12
3}

این کد خطایی ایجاد می‌کند، اما جای نگرانی نیست، چون آن را در ادامه حل خواهیم کرد. اکنون نمای دکمه را ایجاد می‌کنیم. این کار چندان پیچیده نیست. یک Button ابتدایی در SwiftUI دو آرگومان می‌گیرد که یکی action و دیگری نمای label است. در پارامترهای action و label بررسی می‌کنیم که آیا تایمر مکث کرده است یا نه و بر همین مبنا خواهیم دانست که چه تابعی را فراخوانی کنیم و کدام نمای Text باید نمایش پیدا کند.

متغیر body را طوری به‌روزرسانی کنید که به صورت زیر دربیاید:

1var body: some View {
2    let buttonWidth = (UIScreen.main.bounds.size.width / 2) - 12
3    
4    return Button(action: {
5            if self.isPaused {
6                self.actions[0]()
7            } else {
8                self.actions[1]()
9            }
10        }) {
11            if isPaused {
12                Text(self.labels[0])
13                    .color(Color.white)
14                    .frame(width: buttonWidth,
15                           height: 50)
16            } else {
17                Text(self.labels[1])
18                    .color(Color.white)
19                    .frame(width: buttonWidth,
20                           height: 50)
21            }
22        }
23        .background(self.color)
24    }
25}

در کد فوق همه کارهایی را که تا به اینجا انجام داده‌ایم را می‌توان دید. در بخش action (کلوژر اول) مشخصه isPaused بررسی می‌شود. اگر true باشد، در این صورت از اقدام نخست در آرایه actions خود استفاده می کیم، در غیر این صورت از بخش دوم استفاده خواهیم کرد.

همان نکته برای label نیز صحیح است. اگر isPaused صحیح باشد، از رتبه اول آرایه labels استفاده می‌کنیم و در غیر این صورت از رشته دوم استفاده خواهیم کرد.

علاوه بر آن هر نمای Text استایل‌بندی مشابهی دارد و می‌توانیم نمای متنی سفارشی خاص خود را برای آن بسازیم، اما از آنجا که این کار کاملاً ساده است در حال حاضر نیازی به توضیح آن نمی‌بینیم. اگر بخواهیم از نماهای متنی مشابه این‌ها در جاهای دیگر نیز استفاده کنیم، در این صورت قطعاً باید یک نمای متنی سفارشی برای آن بسازیم.

نکته‌ی لازم به اشاره این است که فریم نمای Text و نه فریم دکمه‌ها تنظیم شده است. همان طور که پیش‌تر اشاره کردیم SwiftUI ما را ملزم می‌کند که فریم را روی هر نمای فرزندی که والد نمی‌تواند به فرزند بگوید باید چه کار بکند تنظیم شود. آخرین کاری که باید انجام دهیم تنظیم رنگ پس‌زمینه برای دکمه است. کد نهایی برای StopWatchButton به صورت زیر است:

1struct StopWatchButton : View {
2    var actions: [() -> Void]
3    var labels: [String]
4    var color: Color
5    var isPaused: Bool
6    
7    var body: some View {
8        let buttonWidth = (UIScreen.main.bounds.size.width / 2) - 12
9        
10        return Button(action: {
11            if self.isPaused {
12                self.actions[0]()
13            } else {
14                self.actions[1]()
15            }
16        }) {
17            if isPaused {
18                Text(self.labels[0])
19                    .color(Color.white)
20                    .frame(width: buttonWidth,
21                           height: 50)
22            } else {
23                Text(self.labels[1])
24                    .color(Color.white)
25                    .frame(width: buttonWidth,
26                           height: 50)
27            }
28        }
29        .background(self.color)
30    }
31}

گام 4: افزودن دکمه‌ها

اینک اغلب بخش‌های پیچیده حل شده است و می‌توانیم به طرح‌بندی رابط کاربری‌مان بپردازیم. کار بعدی که باید انجام دهیم این است که دو وهله از نمای StopWatchButton خود اضافه کنیم. پیش از انجام این کار باید یک HStack ایجاد کنیم که بتوانیم دکمه‌ها را درون آن قرار بدهیم. Struct به نام ContentView را طوری به‌روزرسانی می‌کنیم که به شکل زیر دربیاید:

1struct ContentView : View {
2    @ObjectBinding var stopWatch = StopWatch()
3    
4    var body: some View {
5        VStack {
6            Text(self.stopWatch.stopWatchTime)
7                .font(.custom("courier", size: 70))
8                .frame(width: UIScreen.main.bounds.size.width,
9                       height: 300,
10                       alignment: .center)
11            
12            HStack{
13                // Our buttons will go here
14            }
15        }
16     }
17 }

چنان که می‌بینید چیز زیادی به جز این که یک HStack زیر نمای Text داریم تغییر نیافته است. در این بخش مقدار رشته‌های تایمر کنونی نمایش پیدا می‌کند. درون این HStack باید کد زیر را اضافه کنیم:

1StopWatchButton(actions: [self.stopWatch.reset, self.stopWatch.lap],
2                labels: ["Reset", "Lap"],
3                color: Color.red,
4                isPaused: self.stopWatch.isPaused())
5StopWatchButton(actions: [self.stopWatch.start, self.stopWatch.pause],
6                labels: ["Start", "Pause"],
7                color: Color.blue,
8                isPaused: self.stopWatch.isPaused())

پس از افزودن کد فوق اپلیکیشن به صورت زیر درمی‌آید:

نمودارهای React Native

این وضعیت شاید کمی سردرگم‌کننده باشد، در کد فوق ما دو وهله از StopWatchButton ایجاد کرده‌ایم.

چنان که قبلاً توضیح دادیم، این دکمه‌ها دارای چهار مشخصه هستند. مشخصه اول تابع‌هایی خواهد بود که می‌خواهیم بسته به حالت isPaused فراخوانی شوند. اولین دکمه که ایجاد می‌شود، دکمه Reset/Lap خواهد بود و دومی نیز دکمه Start/Pause است. در تصویر فوق نمی‌توانید دکمه Lap و دکمه Pause را ببینید، اما اگر اپلیکیشن را اجرا کرده و روی دکمه start کلیک کنید، این دکمه‌ها را مشاهده می‌کنید. توجه داشته باشید که اگر اپلیکیشن را اجرا کرده و روی دکمه Lap بزنید، از نظر بصری هیچ اتفاقی نمی‌افتد. کد به درستی کار خواهد کرد، اما در گام بعدی شیوه دریافت تعداد دورها را در لیست نشان خواهیم داد.

گام 5: نمایش تعداد دورها

این بخش کاملاً ساده است. تنها کاری که باید انجام دهیم این است که یک VStack داشته باشیم که نمای Text و یک نمای List را در بر بگیرد.

در ادامه یک HStack را می‌بینید که در گام قبلی ایجاد شده و کد زیر را به آن اضافه می‌کنیم:

1VStack(alignment: .leading) {
2    Text("Laps")
3        .font(.title)
4        .padding()
5List { 
6        ForEach(self.stopWatch.laps.identified(by: \.uuid)) { (lapItem) in
7            Text(lapItem.stringTime)
8        }
9    }
10}

اکنون که آن را اضافه کردیم، اپلیکیشن شما باید به شکل زیر دیده شود:

نمودارهای React Native

توجه داشته باشید که تصاویر این صفحه در Xcode گرفته شده است. اگر اپلیکیشن را اجرا کنید سلول‌های List را خواهید دید.

اینک به بررسی کد می‌پردازیم. نخستین تفاوت این است که VStack یک آرگومان برای alignment می‌گیرد. دلیل این کار آن است که اگر آن را حذف کنیم، نمای متنی Laps در میانه صفحه قرار می‌گیرد و ما ترجیح می‌دهیم در سمت چپ باشد.

درون VStack یک نمای Text اضافه می‌کنیم. اندازه فونت را روی اندازه title. می‌گذاریم که اندازه عناوین اپلیکیشن ما است. همچنین مقداری حاشیه به نمای Text اضافه می‌کنیم. بدین ترتیب فاصله مناسبی ایجاد می‌شود که آن را از دکمه‌های بالایش جدا می‌کند.

بخش نهایی کار اضافه کردن نمای List است. نماهای List نیازمند شناسه یکتایی برای هر cell هستند. متأسفانه هر lapItem دارای مشخصه شناسه یکتایی نیست و از این رو باید یک مشخصه uuid اختصاص دهیم که در ForEach استفاده می‌شود. سپس به lapItem دسترسی خواهیم داشت. اکنون می‌توانیم نمای Text دیگری را اضافه کنیم که می‌تواند هر یک از دورهای ذخیره شده را نمایش دهد. به این منظور کافی است به مشخصه stringTime روی شیء lapTime که داریم دسترسی پیدا کنیم و آن را به نمای Text ارسال کنیم.

اگر در حال حاضر اپلیکیشن را بیلد کرده و اجرا کنید، می‌توانید کرنومتر را به کار بیندازید، روی دکمه Lap کلیک کنید تا تعداد دورها در List پایینی نمایش یابد. همچنین می‌توانید روی دکمه Pause بزنید تا کرنومتر تعلیق شود و زمانی که تعلیق شود، می‌توانید روی دکمه Reset بزنید تا زمان کرنومتر ریست شود و همه تعداد دورها از List پاک شود. کد کامل به صورت زیر است:

1import SwiftUI
2struct StopWatchButton : View {
3    var actions: [() -> Void]
4    var labels: [String]
5    var color: Color
6    var isPaused: Bool
7    
8    var body: some View {
9        let buttonWidth = (UIScreen.main.bounds.size.width / 2) - 12
10        
11        return Button(action: {
12            if self.isPaused {
13                self.actions[0]()
14            } else {
15                self.actions[1]()
16            }
17        }) {
18            if isPaused {
19                Text(self.labels[0])
20                    .color(Color.white)
21                    .frame(width: buttonWidth,
22                           height: 50)
23            } else {
24                Text(self.labels[1])
25                    .color(Color.white)
26                    .frame(width: buttonWidth,
27                           height: 50)
28            }
29        }
30        .background(self.color)
31    }
32}
33struct ContentView : View {
34    @ObjectBinding var stopWatch = StopWatch()
35    
36    var body: some View {
37        VStack {
38            Text(self.stopWatch.stopWatchTime)
39                .font(.custom("courier", size: 70))
40                .frame(width: UIScreen.main.bounds.size.width,
41                       height: 300,
42                       alignment: .center)
43            
44            HStack{
45                StopWatchButton(actions: [self.stopWatch.reset, self.stopWatch.lap],
46                                labels: ["Reset", "Lap"],
47                                color: Color.red,
48                                isPaused: self.stopWatch.isPaused())
49StopWatchButton(actions: [self.stopWatch.start, self.stopWatch.pause],
50                                labels: ["Start", "Pause"],
51                                color: Color.blue,
52                                isPaused: self.stopWatch.isPaused())
53            }
54VStack(alignment: .leading) {
55                Text("Laps")
56                    .font(.title)
57                    .padding()
58List {
59                    ForEach(self.stopWatch.laps.identified(by: \.uuid)) { (lapItem) in
60                        Text(lapItem.stringTime)
61                    }
62                }
63            }
64        }
65    }
66}

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۲ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
programming-with-swift
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *