ساخت لیست پیشرفته در SwiftUI — از صفر تا صد
در این مقاله اقدام به پیادهسازی یک لیست دینامیک با حالت خالی، خطا و در حال بارگذاری میکنیم. شاید با دیدن عنوان این نوشته از خود پرسیده باشید که منظور از لیست پیشرفته در SwiftUI چیست. در برخی موارد لازم است روی لیستهای دینامیک زیادی کار کنیم که حالتهای مختلفی از قبیل empty ،error ،items یا loading را نمایش میدهند. به این منظور عموما از فریمورک داده-محور IGListKit استفاده میکنیم تا این نوع از لیستها را بتوانیم در پروژههای خود پیادهسازی کنیم. اما میتوانیم با بهره گرفتن از SwiftUI و صرفاً با نوشتن چند خط کد این نوع از لیستها را بسازیم. در بخش بعدی در مورد روش پیادهسازی این لیستها با استفاده از SwiftUI توضیح خواهیم داد.
پیادهسازی
در این بخش به بررسی روش پیادهسازی و کامپوننتهای مختلف مورد نیاز میپردازیم.
ListState
یک لیست پیشرفته (AdvancedList) باید بتواند حالتهای متفاوت را نمایش دهد. حالتها را میتوانیم به سادگی با یک Enum تعریف کنیم. در کد زیر از اضافه کردن حالت خالی پرهیز کردهایم.
1enum ListState {
2 case error(_ error: Error?)
3 case items
4 case loading
5}
6extension ListState {
7 var error: Error? {
8 switch self {
9 case .error(let error):
10 return error
11 default:
12 return nil
13 }
14 }
15}
ListService
این AdvancedList ما به یک کامپوننت نیاز دارد که ListState و items را ذخیره کرده و مدیریت کند. با کمک گرفتن از Combine و امکان data binding در SwiftUI تلاش میکنیم AdvancedList و ListService را به هم اتصال دهیم.
به این ترتیب حالت لیست را میتوان تغییر داده و آیتمها را از طریق ListService ویرایش کرده و AdvancedList را به طرز خودکار بهروزرسانی کرد.
1final class ListService: BindableObject {
2 private(set) var items: [AnyListItem] = [] {
3 didSet {
4 didChange.send()
5 }
6 }
7
8 private(set) var didChange = PassthroughSubject<Void, Never>()
9
10 var listState: ListState = .items {
11 didSet {
12 didChange.send()
13 }
14 }
15
16 func appendItems<Item: Identifiable>(_ items: [Item]) where Item: View {
17 let anyListItems = items.map { AnyListItem(item: $0) }
18 self.items.append(contentsOf: anyListItems)
19 }
20
21 func updateItems<Item: Identifiable>(_ items: [Item]) where Item: View {
22 let anyListItems = items.map { AnyListItem(item: $0) }
23 for anyListItem in anyListItems {
24 guard let itemIndex = self.items.firstIndex(where: { $0.id == anyListItem.id }) else {
25 continue
26 }
27
28 self.items[itemIndex] = anyListItem
29 }
30 }
31
32 func removeItems<Item: Identifiable>(_ items: [Item]) where Item: View {
33 let anyListItemsToRemove = items.map { AnyListItem(item: $0) }
34 self.items.removeAll(where: { item in
35 return anyListItemsToRemove.contains { item.id == $0.id }
36 })
37 }
38
39 func removeAllItems() {
40 items.removeAll()
41 }
42}
دو آیتم خاص در پیادهسازی ListService به نامهای Item و AnyListItem وجود دارند.
Item
ما میخواهیم بتوانیم آیتمهای مختلف را در لیست یکسانی نمایش دهیم. هر آیتم باید قابل شناسایی بوده و بتواند به صورت یک نمای SwiftUI قابل نمایش باشد. برای رسیدن به این مقصود Item را به Identifiable و پروتکل View مقید میکنیم.
func appendItems<Item: Identifiable>(_ items: [Item]) where Item: View {}
اما یک مشکل وجود دارد. ما نمیتوانیم اشیای سازگار با Identifiable و با پروتکل View را در یک آرایه درون ListService ذخیره کنیم، زیرا پروتکلها انواع مرتبط و قیدهای ژنریک دارند. به همین دلیل باید از type erasure استفاده کنیم.
یک نوع باکس به نام AnyListItem پیادهسازی کردهایم که اطلاعات نوع مشخصه body را با استفاده از AnyView و اطلاعات نوع id را با استفاده از AnyHashable پاک میکند.
1struct AnyListItem: Identifiable, View {
2 let id: AnyHashable
3 let body: AnyView
4
5 init<Item: Identifiable>(item: Item) where Item: View {
6 id = item.id
7 body = AnyView(item)
8 }
9}
ListService و AnyListItem به صورت درونی اطلاعات نوع را از هر Item که به لیست اضافه میشود پاک میکنند.
AdvancedList
در نهایت به بررسی پیادهسازی نمای SwiftUI به نام AdvancedList میپردازیم. دستگیرههای نما خودشان به حالت لیست جاری و آیتمهای جاری وابسته هستند. به این منظور نمای AdvancedList به یک وهله از ListService نیاز دارید که حالت و آیتمهای لیست را مدیریت میکند.
با بهرهگیری از پوشش مشخصه ObjectBinding روی متغیر listService باید به نمای AdvancedList اعلام کنیم که به تغییرها وابسته باشد. به علاوه کاربر نمای AdvancedList باید بتواند یک نما برای حالتهای empty ،loading و error لیست تعریف کند. ما از پوشش مشخصه ViewBuilder روی پارامترهای موجود در initializer برای نیل به این مقصود استفاده کردیم.
1struct AdvancedList<EmptyStateView: View, ErrorStateView: View, LoadingStateView: View> : View {
2 @ObjectBinding private var listService: ListService
3 private let emptyStateView: () -> EmptyStateView
4 private let errorStateView: (Error?) -> ErrorStateView
5 private let loadingStateView: () -> LoadingStateView
6
7 var body: some View {
8 return Group {
9 if listService.listState.error != nil {
10 errorStateView(listService.listState.error)
11 } else if listService.listState == .items {
12 if !listService.items.isEmpty {
13 List(listService.items.identified(by: \.id)) { item in
14 item
15 }
16 } else {
17 emptyStateView()
18 }
19 } else if listService.listState == .loading {
20 loadingStateView()
21 } else {
22 EmptyView()
23 }
24 }
25 }
26
27 init(listService: ListService, @ViewBuilder emptyStateView: @escaping () -> EmptyStateView, @ViewBuilder errorStateView: @escaping (Error?) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
28 self.listService = listService
29 self.emptyStateView = emptyStateView
30 self.errorStateView = errorStateView
31 self.loadingStateView = loadingStateView
32 }
33}
همچنان که پیشتر اشاره کردیم، به حالت خالی روی enum به نام ListState نیاز نداریم، زیرا میتوانیم از isEmpty روی items استفاده کنیم. یک نکته خاص دیگر نیز در پیادهسازی ما وجود دارد. حالت error مربوط به ListState یک مقدار مرتبط با نوع Error دارد. به همین جهت خطا را به نمای error ارسال میکنیم که در صورت نیاز آن را نمایش دهد. بدین ترتیب پیادهسازی لیست پیشرفته به پایان میرسد. در بخش بعدی کاربرد AdvancedList را مورد بررسی قرار میدهیم.
کاربرد نمونه
در این بخش یکی از کاربردهای نمونه نمای AdvancedList را در SwiftUI بررسی میکنیم.
نکته: کد کامل این پیادهسازی را میتوانید در ریپازیتوری گیت هاب ارائه شده در انتهای راهنما مشاهده کنید. ابتدا باید آیتمهایی که میخواهیم در لیست دیده شوند را ایجاد کنیم.
آیتمهای نمونه
ابتدا یک ContactListItem ایجاد کردیم که مخاطبان (نام، نشانی و غیره) را نشان میدهد. برای افزودن وهلههای این آیتم به لیست، باید با پروتکلهای Identifiable و View سازگار باشد. در اینجا یک کار خاص انجام دادهایم. میخواهیم این آیتم را بتوانیم به طرز متفاوتی بسته به نوع مشخصهاش رندر کنیم. به کاربرد viewRepresentationType در مشخصه body توجه کنید:
1struct ContactListItem: Identifiable {
2 @State private var collapsed: Bool = true
3
4 let id: String
5 let firstName: String
6 let lastName: String
7 let streetAddress: String
8 let zip: String
9 let city: String
10
11 var viewRepresentationType: ContactListItemViewRepresentationType = .short
12}
13extension ContactListItem: View {
14 var body: some View {
15 Group {
16 if viewRepresentationType == .short {
17 ContactListItemView(firstName: firstName,
18 lastName: lastName,
19 hasMoreInformation: false)
20 } else if viewRepresentationType == .detail {
21 NavigationLink(destination: ContactDetailView(listItem: self), label: {
22 ContactListItemView(firstName: firstName,
23 lastName: lastName,
24 hasMoreInformation: true)
25 })
26 } else if viewRepresentationType == .collapsable {
27 VStack {
28 if collapsed {
29 ContactListItemView(firstName: firstName,
30 lastName: lastName,
31 hasMoreInformation: false)
32 } else {
33 ContactDetailView(listItem: self)
34 }
35
36 Button(action: {
37 self.collapsed.toggle()
38 }) {
39 Text("\(collapsed ? "show" : "hide") details")
40 }.foregroundColor(.blue)
41 }
42 }
43 }
44 }
45}
به علاوه یک آیتم دوم نیز پیادهسازی میکنیم که یک تبلیغ ساده نمایش میدهد. کاری دقیقاً مشابه ContactListItem انجام دادهایم. AdListItem یک نوع دارد که تعریف نما را کنترل میکند.
1struct AdListItem: Identifiable {
2 @State private var isImageCollapsed: Bool = true
3
4 let id: String
5 let text: String
6 var viewRepresentationType: AdListItemViewRepresentationType = .short
7}
8extension AdListItem: View {
9 var body: some View {
10 Group {
11 if viewRepresentationType == .short {
12 NavigationLink(destination: AdDetailView(text: text), label: {
13 Text(text)
14 .lineLimit(1)
15 Text("ℹ️")
16 })
17 } else if viewRepresentationType == .long {
18 Text(text)
19 .lineLimit(nil)
20 } else if viewRepresentationType == .image {
21 VStack {
22 if !isImageCollapsed {
23 Image("restaurant")
24 .resizable()
25 .aspectRatio(contentMode: .fit)
26 .frame(height: 200)
27 }
28
29 Button(action: {
30 self.isImageCollapsed.toggle()
31 }) {
32 Text("\(isImageCollapsed ? "show" : "hide") image")
33 }.foregroundColor(.blue)
34 }
35 }
36 }
37 }
38}
نمای نمونه محتوا
در نهایت به بررسی استفاده از AdvancedList درون یک نمای محتوای ساده میپردازیم. شاید کنجکاو باشید که CustomListStateSegmentedControlView چیست. این یک نمای کمکی است که به ما کمک میکند به سادگی حالت لیست را تغییر داده و آیتمهای تصادفی به لیست اضافه کنیم.
1struct ContentView : View {
2 @ObjectBinding private var listService: ListService = ListService()
3
4 var body: some View {
5 NavigationView {
6 return GeometryReader { geometry in
7 VStack {
8 CustomListStateSegmentedControlView(listService: self.listService)
9
10 AdvancedList(listService: self.listService, emptyStateView: {
11 Text("No data")
12 }, errorStateView: { error in
13 Text("\(error?.localizedDescription ?? "Error")").lineLimit(nil)
14 }, loadingStateView: {
15 Text("Loading ...")
16 })
17 .frame(width: geometry.size.width)
18 }
19 .navigationBarTitle(Text("List of Items"))
20 }
21 }
22 }
23}
سخن پایانی
به این ترتیب به پایان این مقاله میرسیم. از این که این مقاله را مطالعه کردید، متشکریم. امیدواریم نمای AdvancedList را مفید یافته باشید و بتوانید با استفاده از SwiftUI کارهای جالبی انجام دهید. کد کامل موارد مطرح شده در این مقاله را میتوانید در این ریپوی گیت هاب (+) ملاحظه کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامه نویسی Swift (سوئیفت) برای برنامه نویسی iOS
- مجموعه آموزشهای دروس علوم و مهندسی کامپیوتر
- توسعه اپلیکیشن لیست وظایف با SwiftUI و Core Data — از صفر تا صد
- ساخت اپلیکیشن واقعیت افزوده با RealityKit و SwiftUI — از صفر تا صد
==