Hey ho. I'm trying to make the famous filtered list more reusable and did so in a first step.
Now I want to try out something more advanced - but am stuck.
Have a look here:
/// A container that presents rows of data from a Core Data fetch request, which is generated with the provided sort descriptors and predicate.
///
/// In its simplest form, it is used inside a view providing typical Core Data assets.
/// ```
/// var body: some View {
/// FilteredList(sortDescriptors: []) { (item: Item) in
/// Text(item.title)
/// }
/// }
/// ```
/// More commonly you specify how the list is sorted and filtered, in which case Swift can infer the type of your items.
/// As an option, swipe to delete can be enabled, using the environments managed object context to delete the selected item.
/// You can also provide a special view in case the result set is empty.
/// ```
/// var body: some View {
/// FilteredList(sortDescriptors: sortDescriptors, predicate: predicate, canDelete: true) { item in
/// Text(item.title)
/// } emptyView: {
/// Text("There are currently no items available.")
/// }
/// }
/// ```
struct FilteredList<T: NSManagedObject, Content: View, Empty: View>: View {
@Environment(\.managedObjectContext) var viewContext
@FetchRequest var fetchRequest: FetchedResults<T>
let canDelete: Bool
let content: (T) -> Content
let emptyView: Empty
var body: some View {
if fetchRequest.isEmpty {
emptyView
} else {
List {
ForEach(fetchRequest, id: \.self) { item in
self.content(item)
}
.onDelete(perform: canDelete ? delete : nil)
}
}
}
init(sortDescriptors: [SortDescriptor<T>] = [], predicate: NSPredicate? = nil, canDelete: Bool = false,
@ViewBuilder content: @escaping (T) -> Content,
@ViewBuilder emptyView: () -> Empty
) {
_fetchRequest = FetchRequest<T>(sortDescriptors: sortDescriptors, predicate: predicate, animation: .default)
self.canDelete = canDelete
self.content = content
self.emptyView = emptyView()
}
func delete(_ offsets: IndexSet) {
for offset in offsets {
let item = fetchRequest[offset]
viewContext.delete(item)
}
try? viewContext.save()
}
}
extension FilteredList where Content: View, Empty == EmptyView {
init(sortDescriptors: [SortDescriptor<T>] = [], predicate: NSPredicate? = nil, canDelete: Bool = false,
@ViewBuilder content: @escaping (T) -> Content) {
self.init(sortDescriptors: sortDescriptors, predicate: predicate, canDelete: canDelete,
content: content, emptyView: { EmptyView() })
}
}
When I want to use it together with a view model, I ended up with this integration code:
FilteredList(sortDescriptors: viewModel.sortDescriptors, predicate: viewModel.predicate, canDelete: true) { player in
HStack {
NavigationLink(
destination: PlayerDetailView(
viewModel: PlayerViewModel(dataController: dataController, player: player)
)
) {
Label(player.playerName, systemImage: "person")
}
Spacer()
}
} emptyView: {
NoDataView(text: "There are currently no players available.")
}
and thought: everything could be encapsulated in the view model.
Now my next approach would be to
- provide a
FilteredListViewModelProtocol
which requires certain properties/functions
- pass in the viewmodel as only parameter to the
FilteredList
.
- When there is an optional delete function defined in the view model, enable swipe to delete, otherwise not.
and I started here:
protocol FilteredListViewModel {
associatedtype Entity: NSManagedObject
var sortDescriptors: [SortDescriptor<Entity>] { get }
var predicate: NSPredicate? { get }
var allowsDeletion: Bool { get }
func delete(_ offsets: IndexSet)
}
with a new initializer
init<VM: FilteredListViewModel>(viewModel: VM,
@ViewBuilder content: @escaping (T) -> Content,
@ViewBuilder emptyView: () -> Empty
) {
_fetchRequest = FetchRequest<T>(sortDescriptors: viewModel.sortDescriptors, // <--- Cannot convert value of type '[SortDescriptor<VM.Entity>]' to expected argument type '[NSSortDescriptor]'
predicate: viewModel.predicate,
animation: .default)
self.canDelete = viewModel.allowsDeletion
self.content = content
self.emptyView = emptyView()
}
- I receive the inline mentioned compile error and wonder: there is an initializer for
SortDescriptor
rather than NSSortDescriptor
. Why am I getting this and how to get around?
- What would be a feasible way - if at all possible - to avoid having to include
allowDeletion
but rather check whether the (then optional) delete function is defined in the view model? If I provide a default implementation, it would still be defined and I can't check whether it does something or not (or is there a way?). If I mark it as @objc optional
I have to mark the whole protocol as @objc
and get compile errors
Associated type 'Entity' cannot be declared inside '@objc' protocol 'FilteredListViewModel
Property cannot be a member of an @objc protocol because its type cannot be represented in Objective-C
on the associatedtype
and sortDescriptors
declaration respectively.