في البداية قبل أن نبدأ خلونا نوضح بعض النقاط TabView هو نفسه TabBarController لذلك أحيانا اسميه TabView واحيانا TabBar في الأخير هو نفسه الفرق انه في SwiftUI يطلق عليه TabView وفي UIKit يطلق عليه TabBar
بشكل عام سوا SwiftUI او UIKit تخصيصه جداً محدود ، الي تقدر تعمله تغيير لون الـ Tab عند الضغط عليه واختيار لون الـ Tabs الغير مختاره ، والسماح بوضع نص او الاكتفاء بالأيقونات فقط وأيضا يمكن إخفاء الـ Tabbar في حال اردت اخفائه من بعض الصفحات الشكل الافتراضي

اذا كان تخصيص الـ TabView محدود ، فكيف يتم تخصيصه !؟
يتم تخصيصه بتصميم View بداخل HStack و يحتوي على Buttons وفي اعلاه View الصفحة اذا تغير الزر راح يتغير الـ View

بالنسبة للمستخدم لن يلاحظ اي فرق في الاداء ، بالنسبة للمطور هذا الامر يسهل عليه تخصيص الـ TabView ويظهر ابداعاته =)
لنبدا الشرح في البداية راح نحتاج الى Model لنميز كل Tab الي نحتاجه هو id و text و icon و tab الـ tab رقم الـ View
struct TabItem: Identifiable {
var id: Int
var text: String
var icon: String
var tab: Tab
}
enum Tab: Int {
case first
case second
}
قبل ان نبدا في اساس هذا الثريد ، الافضل تعمل View جديد تقدر تخليها نص او اي محتوى ثريده
انا عملت اثنين واحد باسم HomeView والاخر باسم ProfileView ومن اجل التأكد انه كل شي تمام اضفت List مع اضافة NavigationView
struct HomeView: View {
var body: some View {
List {
ForEach((1...100).reversed(), id: \.self) {
Text("\($0)…")
.foregroundColor(.red)
}
}
.listStyle(.plain)
}
}
struct ProfileView: View {
var body: some View {
Text("Profile")
}
}
الان لنبدا في صنع الـ TabBar اعمل View جديد باسم TabBar
في البداية بطبيعة الحال راح نحتاج نعرف الـ Tab المحدد بالاعتماد على متغير الـ selectedTab وايضا راح نحتاج array بجميع الايقونات راح نستخدم الـ Model الذي عملناه سابقا لاحظ اسم الايقونة الاولى home والثانية user
@SceneStorage("selectedTab") private var selectedTab: Tab = .first
private var tabItems = [
TabItem(id: 0, text: "Home", icon: "home", tab: .first),
TabItem(id: 1, text: "Profile", icon: "user", tab: .second),
]
الان نحتاج نصمم شكل الـ TabView عباره عن مستطيل بحواف ناعمه
ولاحظ استخدمت ultraThinMaterial هذه جديدة في iOS 15 تعطي تأثير Blur
راح تلاحظ شغلتين غريبه الاولى استخدمت GeometryReader والثانية متغير الـ hasHomeIndicator
struct TabBar: View {
@SceneStorage("selectedTab") private var selectedTab: Tab = .first
var tabItems : [TabItem]
var body: some View {
GeometryReader { proxy in
let hasHomeIndicator = proxy.safeAreaInsets.bottom > 0
HStack {
}
.padding(.bottom, hasHomeIndicator ? 16 : 0)
.frame(maxWidth: .infinity, maxHeight: hasHomeIndicator ? 88 : 49)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 34, style: .continuous))
.frame(maxHeight: .infinity, alignment: .bottom)
.ignoresSafeArea()
}
}
}
الـ GeometryReader يفيدك في معرفة حجم المساحة المستهلكه في الـ View
هنا استخدمته لمعرفة وجود شريط اللمس من عدمه الشريط الي بديل الـ Home Button من iPhone X ما اعرف ايش يطلق عليه xD
الفكره بإختصار اذا كانت المساحة 0 معناه الايفون 8 بزر الـ هوم فراح اجعل حجم الـ View بحجم 49
لكن اذا كان اكبر من 0 معناه الايقونة X وفوق ، الاجهزة التي تحتوي على شريط لمس فراح اجعل الـ View بحجم 88 الاحجام هذه هي احجام المتبعه في TabView مهي من عندي
ايضا راح اضيف 16 padding لرفع الايقونات عند وجود الشريط بعد اضافة الايقونات
جرب ازيل ال padding وشاهد الفرق
الان باقي الازرار ولكن قبل التطرق لها حمل الايقونات المستخدم ولا تنسى تغيير اسمها الى user و home
ايقونة home
ايقونة user
حملهم بصيغة Lottie JSON واضيفهم للمشروع بعد تغيير اسمائهم
الان حمل مكتبة Lottie واضيفها للمشروع بإستخدام SPM
اذا لاحظت في الكود السابق داخل الـ HStack مافي كود الان راح نضيفه
راح نعمل متغير tapButtons هو عباره عن ازرار الـ TabView وراح نضيفه لاحقا في داخل الـ Hstack
لاتنسى تضيف import Lottie
var tapButtons: some View {
ForEach(tabItems) { item in
VStack(spacing: 0) {
LottieButton(animation: .named(item.icon)) {
selectedTab = item.tab
}
.configure { lottieAnimationView in
// don't allow taping on same item when it enable
lottieAnimationView.isEnabled = selectedTab != item.tab
// play it once, when the item is active
if selectedTab == item.tab {
lottieAnimationView.animationView.play()
}
// return item to beginning when it not selected
if selectedTab != item.tab {
lottieAnimationView.animationView.currentProgress = 0
}
}
.frame(width: 44, height: 29)
Text(item.text)
.font(.caption2)
.lineLimit(1)
}
.frame(maxWidth: .infinity)
.foregroundColor(selectedTab == item.tab ? Color("sanmarino") : .secondary)
}
}
لاحظ في الكود وضحت ايش عملت اضفت Foreach ومريت على الاريه الي عملته سابقا ، تذكر الـ icon هو اسم ملف Lottie لكل ايقونة
بعدها استخدم LottieButton ومررت له اسم الايقونة وبداخل اقواس الـaction غيرت قيمة selectedTab بحيث عند الضغط على الزر يصبح قيمته قيمة الـ tab
اما بخصوص الـ configure هذه تخصيص للـ Lottiebutton
١- منعت المستخدم انه يضغط مرتين على نفس الزر ، مدام الزر مفعل فلن يستطيع المستخدم الضغط عليه
٢- جعلت الزر يعمل تلقائيا، هذه ضروريه بحيث لما تفتح التطبيق لاول مره راح يكون محدد على اول ايقونة فراح يشتغل الانميشين تلقائيا
٣- عند الضغط على ايقونة الاخرى راح ارجع قيمة الزر السابق الى القيمة الافتراضيه بحيث عند الضغط عليه فيما بعد يعمل الانميشين مره اخرى
الكود النهائي لهذه الصفحة سوف يصبح بهذا الشكل
import SwiftUI
import Lottie
struct TabBar: View {
@SceneStorage("selectedTab") private var selectedTab: Tab = .first
var tabItems : [TabItem]
var body: some View {
GeometryReader { proxy in
let hasHomeIndicator = proxy.safeAreaInsets.bottom > 0
HStack {
}
.padding(.bottom, hasHomeIndicator ? 16 : 0)
.frame(maxWidth: .infinity, maxHeight: hasHomeIndicator ? 88 : 49)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 34, style: .continuous))
.frame(maxHeight: .infinity, alignment: .bottom)
.ignoresSafeArea()
}
}
var tapButtons: some View {
ForEach(tabItems) { item in
VStack(spacing: 0) {
LottieButton(animation: .named(item.icon)) {
selectedTab = item.tab
}
.configure { lottieAnimationView in
// don't allow taping on same item when it enable
lottieAnimationView.isEnabled = selectedTab != item.tab
// play it once, when the item is active
if selectedTab == item.tab {
lottieAnimationView.animationView.play()
}
// return item to beginning when it not selected
if selectedTab != item.tab {
lottieAnimationView.animationView.currentProgress = 0
}
}
.frame(width: 44, height: 29)
Text(item.text)
.font(.caption2)
.lineLimit(1)
}
.frame(maxWidth: .infinity)
.foregroundColor(selectedTab == item.tab ? Color("sanmarino") : .secondary)
}
}
}
الان ما بقي شي الا اننا نغير الـ View حسب الزر باستخدم Switch مع ZStack فنقدر نعملها مباشره في ContentView والوضع راح يكون تمام لكني افضل احسن الكود اولا =)
انشات View جديدة بإسم TabBarView الـ View هذا فكرته بسيطه مررت له متغير الـ array الي هو tabItems
استفدت من الـ content ووضعته في zstack والـ Tabbar
وضعته تحته بما يعني الان الـ TabBar راح يظهر فوق الـ content
struct TabBarView <Content : View> : View {
var content : Content
var tabItems : [TabItem]
init(tabItems : [TabItem], @ViewBuilder content: () -> Content) {
self.tabItems = tabItems
self.content = content()
}
var body: some View {
ZStack(alignment: .bottom) {
content
TabBar(tabItems: tabItems)
}
}
}
ايش الفائدة من الكود السابق ؟ جعل تصميم الـ Custom TabView بنفس طريقة النظام بمعنى المبرمج يحتاج يستخدمه بهذا الشمل
TabBarView(tabItems: tabItems) {}
داخل الاقواس يضيف الـ View
انتهينا ؟ لا عملت View اخر بإسم MainTabView هذا الـ View الاساسي للتطبيق لاحظ قمت بازالت الاريه من TabBar ونقلته الى هنا
struct MainTabView: View {
@SceneStorage("selectedTab") private var selectedTab: Tab = .first
private var tabItems = [
TabItem(id: 0, text: "Home", icon: "home", tab: .first),
TabItem(id: 1, text: "Profile", icon: "user", tab: .second),
]
var body: some View {
NavigationView {
TabBarView(tabItems: tabItems) {
Group {
switch selectedTab {
case .first:
HomeView()
case .second:
ProfileView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("Test")
.navigationBarTitleDisplayMode(.inline)
}
}
}
اضفت الـ NavigationView اولا ومن ثم TabBarView
ومررت لها قيمة الاريه وضعت الـ switch اعتمادا على selectedTab
راح اعرض الـ View الي ضغط عليه المستخدم اذا ضغط على tab الاول راح اعرض HomeView
اذا على الثاني راح اعرض ProfileView
ايش الفائدة من Group ؟ Group في SwiftUI ماتعتبر View بمعنى تعتبر شي شفاف او مخفي وظيفتها فقط انه اذا استخدم Modifier تستخدمه على الكل عشان توضح الصورة الكود تقدر تكتبر كذا لكن التكرار ماله لزمه =)
اذا لاحظت استخدمت
.frame(maxWidth: .infinity, maxHeight: .infinity)
هذه وظيفتها انه تعطي للـ View كامل المساحه المتوفره بما اني راح اعرض View او صفحه فابغاها تاخد كامل المساحة المتوفره
النتيجة النهائية




