نمای ناوبری سفارشی SwiftUI — از صفر تا صد
در این راهنما یک کامپوننت ناوبری سفارشی با ناوبری برنامهنویسی شده و توانایی سفارشیسازی ظاهرش معرفی میکنیم. ساخت یک کامپوننت ناوبری سفارشی برای SwiftUI امکان مفیدی است. پس از انتشار SwiftUI تا اکنون یعنی نسخه 5 بتای Xcode 11 یکی از مشکلاتی که توسعهدهندگان با آن مواجه بودند، سفارشیسازی نوار ناوبری بوده است. به بیان دقیقتر تغییر دادن رنگ نوار، رنگ یا فونت متن و یا ظاهر دکمه بازگشت کاری ناممکن و یا دستکم بسیار پردردسر تلقی میشود. ضمناً ناوبری برنامهنویسی شده چیزی است که اجرای آن به وسیله API های آماده عرضه شده از سوی اپل ناممکن است.
برخی راهحلها وجود دارند که شامل تغییر دادن ظاهر سراسری نوار ناوبری است:
1UINavigationBar.appearance().backgroundColor =.green
برخی راهحلهای دیگر شامل باز کردن مقصد ناوبری به صورت modal است. با این حال هیچ یک از این راهحلها نیازهای توسعهدهندگان را به طور کامل برطرف نمیکنند. این موارد بیشتر شبیه نوعی میانبر هستند تا راهحلهای کاملاً قبل اعتماد.
از این رو باید به فکر یک فریمورک ناوبری سفارشی باشیم که استفاده از آن آسان باشد و کارکردهای زیر را عرضه کند:
- توانایی ناوبری به سمت جلو و عقب در پشته ناوبری هم به صورت برنامهنویسی شده و هم با استفاده از دکمه بازگشت.
- قابلیت سفارشیسازی همه خصوصیتهای نوار ناوبری شامل رنگها، فونتها، متن و تصاویر دکمهها.
- توانایی اجرای منطق سفارشی پیش و پس از گذار به صفحه بعدی/قبلی.
در این مقاله راهحلی معرفی میکنیم که موارد پیشگفته را ارائه کرده است.
نمایش view
یک نوع ساده به صورت NavigationItem اعلان میکنیم که یک AnyView را کپسولهسازی میکند.
با پاک کردن نوع View میتوانیم انواع مختلفی از اشیای View را به یک روش ژنریک ذخیره کنیم که در غیر این حالت اجرای آن برای یک پروتکل با انواع مرتبط ناممکن بوده است.
1struct NavigationItem{
2 var view: AnyView
3}
معرفی NavigationStack
یک شیء observable برای پشته ناوبری پیادهسازی میکنیم که مشخصههای انتشاریافته را برای نمای کنونی و پشته ناوبری ارائه میکند. این پشته شامل نماهای قبلی است که تا به اینک نمایش یافتهاند تا کارکرد بازگشت کلاسیک را عرضه کند:
1final class NavigationStack: ObservableObject {
2 @Published var viewStack: [NavigationItem] = []
3 @Published var currentView: NavigationItem
4 init(_ currentView: NavigationItem ){
5 self.currentView = currentView
6 }
7}
متدهای ناوبری unwind و advance
در این بخش متدهایی ارائه شدهاند که امکان ناوبری در پشته را فراهم میکنند:
- unwind: به نمای قبلی حرکت میکند که در آخرین رکورد پشته ذخیره شده است.
- Advance: یک نمای جدید به صورت currentView ایجاد کرده و نمای قبلی را نیز به پشته اضافه میکند.
1func unwind(){
2 if viewStack.count == 0{
3 return }
4 let last = viewStack.count - 1
5 currentView = viewStack[last]
6 viewStack.remove(at: last)
7}
8func advance(_ view:NavigationItem){
9 viewStack.append( currentView)
10 currentView = view
11}
متد Home
در مورد این مثال، مفهوم صفحه Home را نیز داریم که میتوان هر زمان به آن بازگشت. بنابراین یک متد Home نیز تعریف میکنیم که در این مثال HomeView را نمایش میدهد. به علاوه همه تاریخچه ناوبری با پاک کردن همه عناصر از پشته ناوبری حذف میشود.
1func home( ){
2 currentView = NavigationItem( view: AnyView(HomeView()))
3 viewStack.removeAll()
4}
کلاس NavigationStack نهایی
در ادامه کلاس NavigationStack نهایی را میبینید که همه موارد پیشگفته را کنار هم گرد آورده است:
1final class NavigationStack: ObservableObject {
2 @Published var viewStack: [NavigationItem] = []
3 @Published var currentView: NavigationItem
4 init(_ currentView: NavigationItem ){
5 self.currentView = currentView
6 }
7 func unwind(){
8 if viewStack.count == 0{
9 return
10 }
11 let last = viewStack.count - 1
12 currentView = viewStack[last]
13 viewStack.remove(at: last)
14 }
15 func advance(_ view:NavigationItem){
16 viewStack.append( currentView) currentView = view
17 }
18 func home( ){
19 currentView = NavigationItem(view: AnyView(HomeView()))
20 viewStack.removeAll()
21 }
22}
مشاهده NavigationStack و واکنش نسبت به آن
برای مشاهده مشخصههای پشته Navigation و نمایش واکنش به آنها به یک نمای placeholder نیاز داریم که به تغییرهای currentView واکنش نشان داده و آنها را نمایش دهد. NavigationStack به عنوان EnvironmentObject در نما تزریق میشود و از این رو در دسترس این نما و همه نماهای فرزند آن قرار دارد:
1struct NavigationHost: View{
2 @EnvironmentObject var navigation: NavigationStack
3
4 var body: some View {
5 self.navigation.currentView.view
6 }
7}
BackView از پیش تعریف شده
پیش از بررسی کاربرد این کارکرد ناوبری، میخواهیم یک کلاس BackView ساده نیز ارائه کنیم که میتواند برای نمایش عنوان نمای جاری استفاده شود و همچنین دکمه بازگشت به نمای قبلی در پشته ناوبری و یک دکمه home برای بازگشت به HomeView ارائه شده است. اگر به دکمه Home علاقه ندارید میتوانید آن را حذف کنید:
1struct BackView: View{
2 var title: String var action: ()->Void
3 var homeAction: ()->Void
4 var body: some View {
5 ZStack{
6 Rectangle().fill(Color.gray).frame( height: 40 )
7 HStack{ Button( action: action){
8 Image(uiImage: UIImage(
9 systemName: "arrow.turn.down.left",
10 withConfiguration: UIImage.SymbolConfiguration(pointSize: 20,
11 weight: .bold, scale: .large))! )
12 .padding(.leading, 20) }
13 .foregroundColor(Color.black)
14 Spacer()
15 Text(title).padding(.leading, 20)
16 .font(Font.system(size: 20))
17 .padding(.trailing, 20)
18 Spacer()
19 Button( action: homeAction){
20 Image(uiImage: UIImage(
21 systemName: "house", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15.0,
22 weight: .bold, scale: .large))! )
23 .padding(.trailing, 20) }
24 .foregroundColor(Color.black)
25 }
26 }
27 }
28}
افزودن یک TitleView ساده
ما یک نمای title ساده نیز ایجاد کردهایم که شامل صرفاً یک عنوان و دکمه Home است، چون در این مورد نیازی به نمایش دکمه بازگشت نداریم:
1struct TitleView: View{
2 var title: String
3 var homeAction: ()->Void
4 var body: some View {
5 ZStack{
6 Rectangle().fill(Color.gray).frame( height: 40 )
7 HStack{
8 Spacer()
9 Text(title).padding(.leading, 20)
10 .font(Font.system(size: 20.0))
11 Spacer()
12 Button( action: homeAction){
13 Image(uiImage: UIImage(systemName: "house", withConfiguration: UIImage.SymbolConfiguration(
14 pointSize: 15, weight: .bold,
15 scale: .large))! )
16 .padding(.trailing, 20) }
17 .foregroundColor(Color.black)
18 }
19 }
20 }
21}
جمعبندی
اکنون همه بلوکهای تشکیلدهنده لازم برای ساخت یک اپلیکیشن ساده را برای ارائه نمونهای از کاربردهای عناصر فوق در اختیار داریم. کار خود را با HomeView آغاز میکنیم که صفحه آغازین در پشته ناوبری محسوب میشود. در این صفحه چیز چندان جالبی وجود ندارد، صرفاً یک Title در ابتدای صفحه وجود دارد و یک متن که پیام Move to NextView را نمایش میدهد. هنگام ضربه زدن روی این متن، نمای دیگری به NavigationStack اضافه میشود.
1struct HomeView: View {
2 @EnvironmentObject var navigation: NavigationStack
3 var body: some View {
4 VStack{
5 TitleView( title: "Home view",
6 homeAction: {
7 self.navigation.home()
8 })
9 List{
10 Text("Move to NextView").onTapGesture {
11 self.navigation.advance(
12 NavigationItem( view: AnyView(NextView())))
13 }
14 }
15 }
16 }
17}
NextView
NextView آن نمایی است با ضربه زدن روی Text به آن میرسیم. این نما یک BackView نمایش میدهد که با زدن دکمه بازگشت به HomeView بازمیگردیم. همچنین دکمه Home را نمایش میدهد که میتوان از آن برای رفتن به HomeView استفاده کنیم. البته اگر پشته عنصر دیگری داشته باشد، دکمه بازگشت و دکمه Home تأثیر یکسانی نخواهند داشت.
1struct NextView: View {
2 @EnvironmentObject var navigation: NavigationStack
3 var body: some View {
4 VStack{
5 BackView( title: "I am Next View",
6 action:{ self.navigation.unwind() },
7 homeAction: { self.navigation.home() })
8 List{
9 Text("I am NextView")
10 }
11 }
12 }
13}
نقطه ورود به اپلیکیشن
یک پیادهسازی کاملاً ساده از ContentView به جاسازی یک نمای NavigationHost با یک NavigationStack به عنوان شیء محیط میپردازد:
1struct ContentView: View {
2 var body: some View {
3 NavigationHost()
4 .environmentObject(NavigationStack(
5 NavigationItem( view: AnyView(HomeView()))))
6 }
7}
در تصویر زیر شیوه نمایش دو صفحه مختلف اپلیکیشن را میبینید:
سخن پایانی
راهحلی که در این نوشته ارائه کردیم، جایگزینی برای NavigationView در SwiftUI است. این راهحل یکی از مشکلات مهم NavigationView را حل میکند و به توسعهدهنده کنترل کاملی برای دستیابی به مواردی که در پس آن است میدهد:
این راهحل انعطافپذیری زیادی در جنبه بصری عناصر ناوبری ایجاد میکند. بدین ترتیب میتوانید آنها را دقیقاً به همان ترتیبی که میخواهید در بالا یا پایین صفحه قرار دهید یا حتی در صورت نیاز میتوانید به صورت یک دکمه گرد در میانه صفحه داشته باشید. کافی است یک شیء BackView جدید را پیادهسازی کنید و آن را در نماهای خود به روشی که میخواهید بگنجانید. حتی میتوانید نماهای back مختلفی در هر نما داشته باشید یا میتوانید اندازههای مختلف یا فونت یا تصویر متفاوتی برای نوای ناوبری در هر نما تعیین کنید.
توانایی فراخوانی همه این موارد به صورت برنامهنویسی شده وجود دارد. میتوانید پس از اجرای عملیات خاصی در نمای جاری به نمای قبلی در پشته بازگردید. اما میتوانید ناوبری دکمه بازگشت معمولی را نیز داشته باشید.
این راهحل امکان پیادهسازی ناوبریهای غیرخطی را نیز فراهم میسازد. در مواردی که بخواهید میتوانید با کمی دستکاری کلاس NavigationStack، مسیر ناوبری کاملاً پیچیدهای را در اپلیکیشن پیادهسازی کنید. به طور خلاصه امکان انجام هر کاری وجود دارد.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامهنویسی Swift (سوئیفت) برای برنامه نویسی iOS
- مجموعه آموزشهای دروس علوم و مهندسی کامپیوتر
- آموزش سوئیفت (Swift) — مجموعه مقالات مجله فرادرس
- ساخت اپلیکیشن با Firebase و SwiftUI — از صفر تا صد
==