11import SwiftUI
22import struct Yosemite. Booking
33
4- struct BookingListView : View {
4+ struct BookingListView < Header : View > : View {
55 @ObservedObject private var viewModel : BookingListViewModel
66 @ObservedObject private var searchViewModel : BookingSearchViewModel
77
@@ -10,12 +10,16 @@ struct BookingListView: View {
1010
1111 @Binding var selectedBooking : Booking ?
1212
13+ private let header : Header
14+
1315 init ( viewModel: BookingListViewModel ,
1416 searchViewModel: BookingSearchViewModel ,
15- selectedBooking: Binding < Booking ? > ) {
17+ selectedBooking: Binding < Booking ? > ,
18+ @ViewBuilder header: ( ) -> Header ) {
1619 self . viewModel = viewModel
1720 self . searchViewModel = searchViewModel
1821 self . _selectedBooking = selectedBooking
22+ self . header = header ( )
1923 }
2024
2125 var body : some View {
@@ -98,20 +102,25 @@ private extension BookingListView {
98102 onNextPage: @escaping ( ) -> Void ,
99103 onRefresh: @escaping ( ) async -> Void ) -> some View {
100104 List ( selection: $selectedBooking) {
101- ForEach ( bookings) { item in
102- bookingItem ( item)
103- . tag ( item)
104- }
105-
106- InfiniteScrollIndicator ( showContent: viewModel. shouldShowBottomActivityIndicator)
107- . padding ( . top, Layout . viewPadding)
108- . onAppear {
109- onNextPage ( )
105+ Section {
106+ ForEach ( bookings) { item in
107+ bookingItem ( item)
108+ . tag ( item)
110109 }
110+
111+ InfiniteScrollIndicator ( showContent: viewModel. shouldShowBottomActivityIndicator)
112+ . padding ( . top, BookingListViewLayout . viewPadding)
113+ . onAppear {
114+ onNextPage ( )
115+ }
116+ } header: {
117+ header
118+ . listRowInsets ( EdgeInsets ( ) )
119+ }
111120 }
112121 . listStyle ( . plain)
122+ . listSectionSeparator ( . hidden, edges: . top)
113123 . background ( Color ( . listBackground) )
114- . accentColor ( Color ( . listSelectedBackground) )
115124 . refreshable {
116125 await onRefresh ( )
117126 }
@@ -140,50 +149,20 @@ private extension BookingListView {
140149 }
141150 }
142151 }
152+ . listRowBackground ( booking == selectedBooking ? Color ( . listSelectedBackground) : Color ( . listForeground( modal: false ) ) )
143153 }
144154
145155 func emptyStateView( isSearching: Bool , onRefresh: @escaping ( ) async -> Void ) -> some View {
146156 GeometryReader { proxy in
147157 ScrollView {
148- VStack ( spacing: Layout . emptyStatePadding) {
149- Spacer ( )
150- Image ( uiImage: isSearching ? . magnifyingGlassNotFound : . noBookings)
151- . resizable ( )
152- . aspectRatio ( contentMode: . fit)
153- . frame ( width: Layout . emptyStateImageWidth * scale)
154- . padding ( . bottom, Layout . viewPadding)
155- if isSearching {
156- Text ( Localization . emptySearchText)
157- . font ( . body)
158- . foregroundStyle ( Color . secondary)
159- } else {
160- VStack ( spacing: Layout . textVerticalPadding) {
161- Text ( viewModel. emptyStateTitle)
162- . font ( . title2)
163- . fontWeight ( . semibold)
164- . foregroundStyle ( . primary)
165- Text ( viewModel. emptyStateDescription)
166- . font ( . title3)
167- . foregroundStyle ( . secondary)
168- }
169- if viewModel. hasFilters {
170- VStack ( spacing: Layout . textVerticalPadding) {
171- Button ( " Change filters " ) {
172- // TODO
173- }
174- . buttonStyle ( PrimaryButtonStyle ( ) )
175- Button ( " Clear filters " ) {
176- // TODO
177- }
178- . buttonStyle ( SecondaryButtonStyle ( ) )
179- }
180- }
158+ LazyVStack ( spacing: 0 , pinnedViews: . sectionHeaders) {
159+ Section {
160+ emptyStateContent ( isSearching: isSearching)
161+ . frame ( minWidth: proxy. size. width, minHeight: proxy. size. height)
162+ } header: {
163+ header
181164 }
182- Spacer ( )
183165 }
184- . multilineTextAlignment ( . center)
185- . padding ( . horizontal, Layout . emptyStatePadding)
186- . frame ( minWidth: proxy. size. width, minHeight: proxy. size. height)
187166 }
188167 . refreshable {
189168 await onRefresh ( )
@@ -192,41 +171,79 @@ private extension BookingListView {
192171 . background ( Color ( . systemBackground) )
193172 }
194173
174+ func emptyStateContent( isSearching: Bool ) -> some View {
175+ VStack ( spacing: BookingListViewLayout . emptyStatePadding) {
176+ Spacer ( )
177+ Image ( uiImage: isSearching ? . magnifyingGlassNotFound : . noBookings)
178+ . resizable ( )
179+ . aspectRatio ( contentMode: . fit)
180+ . frame ( width: BookingListViewLayout . emptyStateImageWidth * scale)
181+ . padding ( . bottom, BookingListViewLayout . viewPadding)
182+ if isSearching {
183+ Text ( BookingListViewLocalization . emptySearchText)
184+ . font ( . body)
185+ . foregroundStyle ( Color . secondary)
186+ } else {
187+ VStack ( spacing: BookingListViewLayout . textVerticalPadding) {
188+ Text ( viewModel. emptyStateTitle)
189+ . font ( . title2)
190+ . fontWeight ( . semibold)
191+ . foregroundStyle ( . primary)
192+ Text ( viewModel. emptyStateDescription)
193+ . font ( . title3)
194+ . foregroundStyle ( . secondary)
195+ }
196+ if viewModel. hasFilters {
197+ VStack ( spacing: BookingListViewLayout . textVerticalPadding) {
198+ Button ( " Change filters " ) {
199+ // TODO
200+ }
201+ . buttonStyle ( PrimaryButtonStyle ( ) )
202+ Button ( " Clear filters " ) {
203+ // TODO
204+ }
205+ }
206+ }
207+ }
208+ Spacer ( )
209+ }
210+ . multilineTextAlignment ( . center)
211+ . padding ( . horizontal, BookingListViewLayout . emptyStatePadding)
212+ }
213+
195214 func errorSnackBar( onTap: @escaping ( ) -> Void ) -> some View {
196- Text ( Localization . errorMessage)
215+ Text ( BookingListViewLocalization . errorMessage)
197216 . foregroundStyle ( Color ( . listForeground( modal: false ) ) )
198217 . frame ( maxWidth: . infinity, alignment: . leading)
199- . padding ( Layout . viewPadding)
218+ . padding ( BookingListViewLayout . viewPadding)
200219 . background {
201- RoundedRectangle ( cornerRadius: Layout . cornerRadius)
220+ RoundedRectangle ( cornerRadius: BookingListViewLayout . cornerRadius)
202221 . fill ( Color ( . text) )
203222 }
204- . padding ( Layout . viewPadding)
223+ . padding ( BookingListViewLayout . viewPadding)
205224 . padding ( . bottom, connectivityMonitor. isOffline ? OfflineBannerView . height : 0 )
206225 . contentShape ( Rectangle ( ) )
207226 . onTapGesture { onTap ( ) }
208227 }
209228}
210229
211- private extension BookingListView {
212- enum Layout {
213- static let textVerticalPadding : CGFloat = 8
214- static let viewPadding : CGFloat = 16
215- static let emptyStatePadding : CGFloat = 24
216- static let emptyStateImageWidth : CGFloat = 67
217- static let cornerRadius : CGFloat = 8
218- }
230+ fileprivate enum BookingListViewLayout {
231+ static let textVerticalPadding : CGFloat = 8
232+ static let viewPadding : CGFloat = 16
233+ static let emptyStatePadding : CGFloat = 24
234+ static let emptyStateImageWidth : CGFloat = 67
235+ static let cornerRadius : CGFloat = 8
236+ }
219237
220- enum Localization {
221- static let errorMessage = NSLocalizedString (
222- " bookingList.errorMessage " ,
223- value: " Error fetching bookings " ,
224- comment: " Error message when fetching bookings fails "
225- )
226- static let emptySearchText = NSLocalizedString (
227- " bookingList.emptySearchText " ,
228- value: " We couldn’t find any bookings with that name — try adjusting your search term to see more results. " ,
229- comment: " Message displayed when searching bookings by keyword yields no results. "
230- )
231- }
238+ fileprivate enum BookingListViewLocalization {
239+ static let errorMessage = NSLocalizedString (
240+ " bookingList.errorMessage " ,
241+ value: " Error fetching bookings " ,
242+ comment: " Error message when fetching bookings fails "
243+ )
244+ static let emptySearchText = NSLocalizedString (
245+ " bookingList.emptySearchText " ,
246+ value: " We couldn't find any bookings with that name — try adjusting your search term to see more results. " ,
247+ comment: " Message displayed when searching bookings by keyword yields no results. "
248+ )
232249}
0 commit comments