So I'm new to using SwiftUI. Normally I'd have the fetch request in the viewdidload method but not sure how to go about it now as the method doesn't seem to have an equivalent.
So I have a view where I'm passing a variable to another view like so
NavigationLink(destination: ItemAddView(series: series)) {
Label("Add Item", systemImage: "plus")
}
I'm now wanting to use this variable in a predicate on a fetch request.
The fetch request is setup as follows
struct ItemAddView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var series: Series
#State private var searchText = ""
var body: some View {
List {
ForEach(items) { item in
NavigationLink(destination: ItemDetailView(item: item)) {
Image(item.mainImage ?? "")
.renderingMode(.original)
.resizable()
.scaledToFit()
.frame(width: 100.0)
Text("\(item.name ?? "Error")")
}
}
}
.searchable(text: $searchText)
.onSubmit(of: .search) {
items.nsPredicate = NSPredicate(format: "series = %#", series)
if(!searchText.isEmpty) {
items.nsPredicate = NSPredicate(format: "series = %# AND amount = 0 AND name CONTAINS[cd] %#", series, searchText)
}
}
.onChange(of: searchText) { _ in
items.nsPredicate = NSPredicate(format: "series = %#", series)
if(!searchText.isEmpty) {
items.nsPredicate = NSPredicate(format: "series = %# AND amount = 0 AND name CONTAINS[cd] %#", series, searchText)
}
}
.navigationBarTitle("\(series.name ?? "Error") Add Items", displayMode: .inline)
.onAppear() {
items.nsPredicate = NSPredicate(format: "series = %# AND amount = 0", series)
}
}
}
I get an error if I use the predicate here
Cannot use instance member 'series' within property initializer; property initializers run before 'self' is available
I've taken to using the predicate in the onAppear method as such
.onAppear() {
items.nsPredicate = NSPredicate(format: "series = %# AND amount > 0", series)
}
But the issue is I'm seeing the full list before it then goes to the filtered request. What's the correct way of doing this?
You just need to declare the variable in the header. You can then do the initialization in the init() and use the variable that is passed in. If you aren't subsequently needing series, you do not need to have any variable in the view to assign it to. Also, as you didn't post your full view, I had to guess at the type of series.
struct ItemAddView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest private var items: FetchedResults<Item>
init(series: Series) {
let request = Item.fetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)], predicate: NSPredicate(format: "series = %# AND amount > 0", series), animation: .default)
_items = FetchRequest(fetchRequest: request)
// Initialize anything else that is necessary
}
...
}
Related
I was not sure how to word the question. I am making a simple notes app and am using a three column view. I want the user to create a new folder which is initially empty but they can add notes to it. My problem is that every folder goes to the same notes. If a new note is created in one folder, you see it in all the others.
Example (yes I know both folders are generically set to "New Folder", I'm working on trying to let the user set names):
I know the general problem is that when a new folder is created, they all go to the "NoteView". But I want a newly created folder to go to a different VERSION of the "NoteView" that is initially empty. Like you see in any Notes app, I want each folders' notes to be different, not them all referencing the same notes.
My code is below. Sidebar is where the folders are displayed. NoteView is where the corresponding notes are supposed to be displayed.
struct ContentView: View {
var body: some View {
NavigationView{
Sidebar()
Text("No folder selected")
Text("No note selected")
}
}
}
struct Sidebar: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var isPresented: Bool = false
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Folder.folderName, ascending: true)])
var folders: FetchedResults<Folder>
#State private var selectedFoldersIds: Set<Folder.ID> = []
private var selectedFolder: Folder?{
guard let selectedFolderId = selectedFoldersIds.first,
let selectedFolder = folders.filter ({$0.id == selectedFolderId}).first else{
return nil
}
return selectedFolder
}
var body: some View {
List(folders, selection: $selectedFoldersIds){folder in
NavigationLink(folder.folderName, destination:NoteView()) //each folder created navigates to NoteView() which stores all created notes. But I want each folder to have empty notes until one is added.
}
.listStyle(SidebarListStyle())
.onDeleteCommand(perform: deleteSelectedFolders)
.toolbar{
ToolbarItem(placement: .primaryAction){
Button(action: createFolder){
Label("Create Quick Note", systemImage: "square.and.pencil")
}
}
ToolbarItem(placement: .primaryAction){
Button(action: deleteSelectedFolders){
Label("Delete note", systemImage: "trash")
}
}
}
}
private func createFolder(){
createFolder(name: "New Folder")
}
private func createFolder(name: String) {
withAnimation {
let folder = Folder(context: viewContext)
folder.folderId = UUID()
folder.folderName = name
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteSelectedFolders(){
withAnimation{
let selectedFolders = folders.filter {selectedFoldersIds.contains($0.id)}
deleteFolders(folders: selectedFolders)
}
}
private func deleteFolders(folders: [Folder]){
viewContext.perform{ folders.forEach(viewContext.delete)}
}
}
struct NoteView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var isPresented: Bool = false
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Note.name, ascending: true)])
var notes: FetchedResults<Note>
#State private var selectedNotesIds: Set<Note.ID> = []
private var selectedNote: Note?{
guard let selectedNoteId = selectedNotesIds.first,
let selectedNote = notes.filter ({$0.id == selectedNoteId}).first else{
return nil
}
return selectedNote
}
var body: some View {
List(notes, selection: $selectedNotesIds){ note in
NavigationLink(note.name, destination: NoteEditor(note: note ))
}
.toolbar{
ToolbarItem(placement: .primaryAction){
Button(action: createNote){
Label("Create Quick Note", systemImage: "square.and.pencil")
}
}
ToolbarItem(placement: .primaryAction){
Button(action: deleteSelectedNotes){
Label("Delete note", systemImage: "trash")
}
}
}
}
private func createNote(){
createNote(name: "New note", text: "")
}
private func createNote(name: String, text: String) {
withAnimation {
let note = Note(context: viewContext)
note.id = UUID()
note.name = name
note.text = text
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteSelectedNotes(){
withAnimation{
let selectedNotes = notes.filter {selectedNotesIds.contains($0.id)}
deleteNotes(notes: selectedNotes)
}
}
private func deleteNotes(notes: [Note]){
viewContext.perform{ notes.forEach(viewContext.delete)}
}
}
I have a view that outputs rows of data using ForEach. The data is an array of defined data records from an FMDB database. I use List / ForEach and LazyVGrid to populate the rows in the view.
My goal is to take two of the output fields in the LazyVGrid and use onGesture to invoke one of two .sheet views for the details behind the value.
In the onGesture I want to capture the dataRecord used to populate that row and use the data in that record to call the view in .sheet
It all seems to work until I get to the .sheet(isPresented...
There the #State variable I populate in the onGesture is Nil
I'm putting in the code I use along with some print output that seems to show that the #State variable is populated in onGesture, but later in the .sheet(isPresented it is nil
I'm not sure I understand the scope of visibility for that #State variable and wonder if someone can help me figure this out...
MY CODE IS:
import SwiftUI
struct BudgetedIncomeView: View {
#State var account_code: Int
#State var budgetYear: Int
#State var budgetMonth: Int
#State private var isAdding = false
#State private var isEditing = false
#State private var isDeleting = false
#State private var budgetRec = BudgetedIncome.BudgetedIncomeRecord()
#State private var isBudgeted = false
#State private var isReceived = false
// Environment and ObservedObjects
#Environment(\.presentationMode) var presentationMode
#Environment(\.editMode) var editMode
#StateObject var toolbarViewModel = ToolbarViewModel()
var displayMonthYearString: String {
return calendarMonths(budgetMonth) + "-" + String(budgetYear)
}
let columns = [
GridItem(.flexible(), alignment: .leading),
GridItem(.fixed(UIScreen.main.bounds.width * 0.20), alignment: .trailing),
GridItem(.fixed(UIScreen.main.bounds.width * 0.20), alignment: .trailing)
]
let numberColor = Color.black
let negativeColor = Color.red
// Array of Budgeted Income records for View List
var budgetedIncome: [BudgetedIncome.BudgetedIncomeRecord] {
return BudgetedIncome.shared.selectBudgetedIncomeForAccountWithMonthAndYear(withAccountCode: self.account_code, withYear: self.budgetYear, withMonth: self.budgetMonth)
}
var body: some View {
ZStack {
VStack {
BudgetedIncomeHeader(headerText: "Budgeted")
.padding(.top, 10)
List {
ForEach (self.budgetedIncome, id: \.self) { budgetRecord in
LazyVGrid(columns: columns, spacing: 5) {
Text("\(budgetRecord.description)")
.lineLimit(1)
.padding(.leading, 5)
Text("\(NumberFormatter.formatWithComma(value: budgetRecord.income_budget))")
.foregroundColor((budgetRecord.income_budget < 0 ) ? self.negativeColor : self.numberColor)
.onTapGesture {
//
// PRINT STATEMENTS THAT SHOW THE DATA IS CAPTURED
//
let _ = print("budgetRecord in onGesture = \(budgetRecord)\n\n")
budgetRec = budgetRecord
let _ = print("budgetRec in onGesture = \(budgetRec)\n\n")
isBudgeted.toggle()
}
Text("\(NumberFormatter.formatWithComma(value: budgetRecord.income_received))")
.underline()
.padding(.trailing, 15)
}// END OF LAZYVGRID
} // END OF FOREACH
} // END OF LIST
BudgetedIncomeFooter(accountCode: $account_code, budgetYear: $budgetYear, budgetMonth: $budgetMonth)
.padding(.top, 5)
} // END OF VSTACK
.sheet(isPresented: $isBudgeted ){
//
// PRINT STATEMENT THAT SHOWS THE DATA IS NIL HERE
//
let _ = print("budgetRec in .sheet = \(budgetRec)\n\n")
BudgetedIncomeDetailsView(accountCode: budgetRec.account_code, incomeCode: budgetRec.income_code, budgetYear: budgetRec.budget_year, budgetMonth: budgetRec.budget_month)
.opacity(isBudgeted ? 1 : 0)
.zIndex(isBudgeted ? 1 : 0)
}
if isReceived {
BudgetedIncomeDetailsView(accountCode: 12345678, incomeCode: 50060, budgetYear: 2020, budgetMonth: 12)
.opacity(isReceived ? 1 : 0)
.zIndex(isReceived ? 1 : 0)
}
} // END OF ZSTACK
.navigationTitle("Budgeted Income for \(self.displayMonthYearString)")
.navigationBarBackButtonHidden(true)
.toolbar {
ToolBarCancelDeleteAdd() {
toolbarViewModel.cancelContent(editMode: editMode)
presentationMode.wrappedValue.dismiss()
}
adding: {
toolbarViewModel.addingContent(isAdding: &isAdding, editMode: editMode)
}
} // END OF TOOLBAR
} // END OF BODY VIEW
} // END OF STRUCT VIEW
struct BudgetedIncomeView_Previews: PreviewProvider {
static var previews: some View {
BudgetedIncomeView(account_code: 12345678,
budgetYear: 2020,
budgetMonth: 12)
.environmentObject(ApplicationSettings())
.environmentObject(Budget())
.environmentObject(GlobalSettings())
}
}
The output from these print statements is:
budgetRecord in onGesture = BudgetedIncomeRecord(income_id: Optional(589), account_code: Optional(12345678), income_code: Optional(50060), budget_year: Optional(2020), budget_month: Optional(12), description: Optional("ADD BACK SET ASIDE"), category: Optional("*Exclude From Reports"), income_budget: Optional(3600.0), income_received: Optional(3600.0), unexpected_income: Optional(0.0), category_code: Optional(99999), set_aside: Optional(true), set_aside_id: nil)
budgetRec in onGesture = BudgetedIncomeRecord(income_id: Optional(589), account_code: Optional(12345678), income_code: Optional(50060), budget_year: Optional(2020), budget_month: Optional(12), description: Optional("ADD BACK SET ASIDE"), category: Optional("*Exclude From Reports"), income_budget: Optional(3600.0), income_received: Optional(3600.0), unexpected_income: Optional(0.0), category_code: Optional(99999), set_aside: Optional(true), set_aside_id: nil)
budgetRec in .sheet = BudgetedIncomeRecord(income_id: nil, account_code: nil, income_code: nil, budget_year: nil, budget_month: nil, description: nil, category: nil, income_budget: nil, income_received: nil, unexpected_income: nil, category_code: nil, set_aside: nil, set_aside_id: nil)
It seems to me that somewhere the data goes nil. I don't seem to follow where the data is going out of scope???
I have used this technique in other views where I have used a view for the row and let the whole row be selected .onGesture.
I haven't tried it using LazyVGrid and selecting specific output values..
Any help would be greatly appreciated..
Bob
I have a list of entries each with an attached date. I would like to display the date only if there is a change in date. I first developed this software in iOS 14.4 that resulted in a view immutable error. This was because I was storing and changing a copy of the entry date.
Now in version iOS 14.5 I don't see the immutable error. But my software still doesn't work. If you run the code and look in the console you will note that Xcode is going through my six entries twice: the first time is always true (show the date) and the second time always false (don't show the date). Why?
In my actual code I am using dates of type Date instead of Strings in this example code. In my actual code, operation hangs as it loops endlessly through my function checkDate (Many times more than the number of entries). Does date of type Date include the time causing the compare to fail?
Is there a better way to prevent display of the date if it is the same as the previous entry?
struct KitchenItem: Codable, Identifiable {
var id = UUID()
var item: String
var itemDate: String
var itemCost: Double
}
class Pantry: ObservableObject {
#Published var oldDate: String = ""
#Published var kitchenItem: [KitchenItem]
init() {
self.kitchenItem = []
let item0 = KitchenItem(item: "String Beans", itemDate: "1/13/2021", itemCost: 4.85)
self.kitchenItem.append(item0)
let item1 = KitchenItem(item: "Tomatoes", itemDate: "1/22/2021", itemCost: 5.39)
self.kitchenItem.append(item1)
let item2 = KitchenItem(item: "Bread", itemDate: "1/22/2021", itemCost: 4.35)
self.kitchenItem.append(item2)
let item3 = KitchenItem(item: "Corn", itemDate: "3/18/2021", itemCost: 2.75)
self.kitchenItem.append(item3)
let item4 = KitchenItem(item: "Peas", itemDate: "3/18/2021", itemCost: 7.65)
self.kitchenItem.append(item4)
let item5 = KitchenItem(item: "Ice Cream", itemDate: "4/12/2021", itemCost: 7.95)
self.kitchenItem.append(item5)
}
}
struct ContentView: View {
#ObservedObject var pantry: Pantry = Pantry()
var body: some View {
LazyVStack (alignment: .leading) {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
ForEach(0..<pantry.kitchenItem.count, id: \.self) { item in
VStack (alignment: .leading) {
showRow(item: item)
}
}
}
}
}
struct showRow: View {
#ObservedObject var pantry: Pantry = Pantry()
var item: Int
var body: some View {
// don't show the date if is the same as the previous entry
let newDate = pantry.kitchenItem[item].itemDate
if checkDate(newDate: newDate) == true {
Text("\n\(newDate)")
.font(.title2)
.padding(.leading, 10)
}
HStack {
Text("\(pantry.kitchenItem[item].item)")
.padding(.leading, 50)
.frame(width: 150, alignment: .leading)
Text("\(pantry.kitchenItem[item].itemCost, specifier: "$%.2f")")
}
}
func checkDate(newDate: String) -> (Bool) {
print(" ")
print("new date = \(newDate)")
if newDate == pantry.oldDate {
print("false: don't show the date")
return false
} else {
pantry.oldDate = newDate
print("old date = \(pantry.oldDate)")
print("true: show the date")
return true
}
}
}
Actual code:
struct ListView: View {
#EnvironmentObject var categories: Categories
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var viewContext
var money: String = ""
var xchRate: Double = 0.0
var cat: Int = 0
var mny: String = ""
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
GeometryReader { g in
ScrollView {
LazyVStack (alignment: .leading) {
TitleView()
ForEach(currTrans, id: \.self) { item in
showRow(item: item, priorDate: priorDate(forItemIndex: item), g: g)
}
.onDelete(perform: deleteItem)
}
.font(.body)
}
}
}
private func priorDate(forItemIndex item: Int) -> Date? {
guard item > 0 else { return nil }
return currTrans[item - 1].entryDT
}
}
struct showRow: View {
#EnvironmentObject var userData: UserData
var item: CurrTrans
var priorDate: Date?
var g: GeometryProxy
var payType = ["Cash/Debit", "Credit"]
var body: some View {
// don't show the date if is the same as the previous entry
let gotDate = item.entryDT ?? Date()
let newDate = gotDate.getFormattedDate()
Text("\(newDate)")
.opacity(gotDate == priorDate ? 0 : 1)
.font(.title2)
.padding(.leading, 10)
displays entry parameters in HStack...
Thou shalt not mutate thy data inside thy body method, for it is an abomination in the eyes of SwiftUI.
Modifying oldDate inside the body method is wrong. SwiftUI will get confused if you modify the data it is observing while it is rendering your views. Furthermore, SwiftUI doesn't make any guarantees about the order in which it renders the children of a LazyVStack (or any other container).
Is there a better way to prevent display of the date if it is the same as the previous entry?
Yes. Pass the current entry, and the prior entry's date, to the entry view.
Here's your data model and store, without the cruft:
struct KitchenItem: Codable, Identifiable {
var id = UUID()
var item: String
var itemDate: String
var itemCost: Double
}
class Pantry: ObservableObject {
#Published var kitchenItems: [KitchenItem] = [
.init(item: "String Beans", itemDate: "1/13/2021", itemCost: 4.85),
.init(item: "Tomatoes", itemDate: "1/22/2021", itemCost: 5.39),
.init(item: "Bread", itemDate: "1/22/2021", itemCost: 4.35),
.init(item: "Corn", itemDate: "3/18/2021", itemCost: 2.75),
.init(item: "Peas", itemDate: "3/18/2021", itemCost: 7.65),
.init(item: "Ice Cream", itemDate: "4/12/2021", itemCost: 7.95),
]
}
For each KitchenItem, you need to also extract the prior item's date, if there is a prior item. We'll use a helper method, priorDate(forItemIndex:), to do that. Also, you need to use StateObject, not ObservedObject, if you're going to create your store inside the view. Thus:
struct ContentView: View {
#StateObject var pantry: Pantry = Pantry()
var body: some View {
ScrollView(.vertical) {
LazyVStack (alignment: .leading) {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
ForEach(0 ..< pantry.kitchenItems.count) { i in
if i > 0 {
Divider()
}
KitchenItemRow(item: pantry.kitchenItems[i], priorDate: priorDate(forItemIndex: i))
}
}
}
}
private func priorDate(forItemIndex i: Int) -> String? {
guard i > 0 else { return nil }
return pantry.kitchenItems[i - 1].itemDate
}
}
Here is KitchenItemRow. You can see that it makes the date Text transparent if the date is the same as the prior item's date. I keep it in place but make it transparent so the row lays out the same:
struct KitchenItemRow: View {
var item: KitchenItem
var priorDate: String?
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(item.item)
Spacer()
Text("\(item.itemCost, specifier: "$%.2f")")
}
Text(item.itemDate)
.opacity(item.itemDate == priorDate ? 0 : 1)
}
.padding([.leading, .trailing], 10)
}
}
And here is TitleView, extracted from ContentView for hygiene:
struct TitleView: View {
var body: some View {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
}
}
Result:
UPDATE
Since your “real code” uses onDelete, it's important to give ForEach an id for each item instead of using the indexes.
Note that onDelete only works inside List, not inside LazyVStack.
So we need to map each item to its index, so we can find the prior item. Here's a revised version of my ContentView that uses onDelete:
struct ContentView: View {
#StateObject var pantry: Pantry = Pantry()
var body: some View {
let indexForItem: [UUID: Int] = .init(
uniqueKeysWithValues: pantry.kitchenItems.indices.map {
(pantry.kitchenItems[$0].id, $0) })
List {
TitleView()
ForEach(pantry.kitchenItems, id: \.id) { item in
let i = indexForItem[item.id]!
KitchenItemRow(item: item, priorDate: priorDate(forItemIndex: i))
}
.onDelete(perform: deleteItems(at:))
}
}
private func priorDate(forItemIndex i: Int) -> String? {
guard i > 0 else { return nil }
return pantry.kitchenItems[i - 1].itemDate
}
private func deleteItems(at offsets: IndexSet) {
pantry.kitchenItems.remove(atOffsets: offsets)
}
}
In my testing, this works and allows swipe-to-delete. I trust you can adapt it to your “real” code.
I have this code in my SwiftUI project in works well
struct ContentView: View {
#State private var selectedCountry: Country?
#State private var showSetting = false
#FetchRequest(entity: Country.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Country.cntryName, ascending: true)]
) var countries: FetchedResults<Country>
var body: some View {
NavigationView {
VStack {
Form {
Picker("Pick a country", selection: $selectedCountry) {
ForEach(countries, id: \Country.cntryName) { country in
Text(country.cntryName ?? "Error").tag(country as Country?)
}
}
if selectedCountry != nil {
DetailView(cntryName: (selectedCountry?.cntryName!)!)
}
}
}
.navigationBarTitle("UNECE Data")
.navigationBarItems(trailing: Button("Settings", action: {
self.showSetting.toggle()
}))
}
.sheet(isPresented: $showSetting) {
SettingsView(showSetting: self.$showSetting)
}
}
}
However I need to call FetchRequest dynamically end reload Picker view when SettingsView dismiss. Possibly I should use #ObservableObject but how to put there fetch request and use result in the Picker view ForEach? Thanks for hints.
You can customize most part of your FetchRequest:
#FetchRequest(entity: Country.entity(),
sortDescriptors: ObservableObject.sortDesc,
predicate : ObservableObject.predicate
) var countries: FetchedResults<Country>
I have reworked code like this
var body: some View {
NavigationView {
VStack {
Form {
//Text(String(describing: countries.count))
Picker("Pick a country", selection: $selectedCountry) {
ForEach(getAllCountries().wrappedValue, id: \Country.cntryName) { country in
Text(country.cntryName ?? "Error").tag(country as Country?)
}
}
if selectedCountry != nil {
DetailView(cntryName: (selectedCountry?.cntryName!)!)
}
}
}
.navigationBarTitle("UNECE Data")
.navigationBarItems(trailing: Button("Settings", action: {
self.showSetting.toggle()
}))
}
.sheet(isPresented: $showSetting) {
SettingsView(showSetting: self.$showSetting)
}
}
func getAllCountries() -> FetchRequest<Country> {
let request = FetchRequest<Country>(entity: Country.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Country.cntryName, ascending: true)])
return request
}
but it reports fatal error "Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)" on ForEach line when runs.
In SettingsView I delete all data in Country entity, parse JSON file stored on my iCloud and save all data in Country entity.
I've a list displaying object from CoreData using #FetchRequest, I want to provide the user with a bar button that when clicked will filter the displayed list.
How can I change the #FetchRequest predicate and rerun it dynamically to rebuild the list with the filtered items?
struct EmployeeListView : View {
#FetchRequest(
entity: Department.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Department.name, ascending: false)],
)
var depts: FetchedResults<Department>
#Environment(\.managedObjectContext) var moc
var body: some View {
NavigationView {
List {
ForEach(depts, id: \.self) { dept in
Section(header: Text(dept.name)) {
ForEach(dept.employees, id: \.self) { emp in
Text(emp.name)
}
}
}
}
.navigationBarTitle("Employees")
}
}
}
I know how to provide a filter, what I don't know how is changing the property wrapper predicate and rerunning the fetch request.
You can change your results based on a binding in your fetch predicate, but with Bool vars, I've found it is difficult to do. The reason is, the predicate to test a Bool in CoreData is something like NSPredicate(format: "myAttrib == YES") whereas your Bool binding variable will be true or false, not YES or NO... So if you NSPredicate(format: "%K ==%#", #keypath(Entity.seeMe), seeMe.wrappedValue), this will always be false. Maybe I'm wrong, but this is what I've experienced.
You can filter your fetch based on String data easier.. But it works a little differently than my example below because your need to run your fetch in the init() of the View like this:
#Binding var searchTerm:String
var fetch: FetchRequest<Entity>
var rows: FetchedResults<Entity>{fetch.wrappedValue}
init(searchTerm:Binding<String>) {
self._searchTerm = searchTerm
self.fetch = FetchRequest(entity: Entity.entity(), sortDescriptors: [], predicate: NSPredicate(format: "%K == %#", #keyPath(Entity.attribute),searchTerm.wrappedValue))
}
To accomplish the task you've described, clicking on a bar button item thereby toggling a Bool, the below example is what I would recommend:
This example will accomplish your goal without changing the fetch predicate. It uses logic to decide whether or not to display a row of data based on the entry in the data model and the value of your #State variable.
import SwiftUI
import CoreData
import Combine
struct ContentView: View {
#Environment(\.managedObjectContext) var viewContext
#State var seeMe = false
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Entity.attribute, ascending: true)],
animation: .default)
var rows: FetchedResults<Entity>
var body: some View {
NavigationView {
VStack {
ForEach(self.rows, id: \.self) { row in
Group() {
if (self.validate(seeMe: row.seeMe)) {
Text(row.attribute!)
}
}
}
.navigationBarItems(leading:
Button(action: {
self.seeMe.toggle()
}) {
Text("SeeMe")
}
)
Button(action: {
Entity.create(in: self.viewContext, attribute: "See Me item", seeMe: true)
}) {
Text("add seeMe item")
}
Button(action: {
Entity.create(in: self.viewContext, attribute: "Dont See Me item", seeMe: false)
}) {
Text("add NON seeMe item")
}
}
}
}
func validate(seeMe: Bool) -> Bool {
if (self.seeMe && seeMe) {
return true
} else if (!self.seeMe && !seeMe ){
return true
} else {
return false
}
}
}
extension Entity {
static func create(in managedObjectContext: NSManagedObjectContext,
attribute: String,
seeMe: Bool
){
let newEvent = self.init(context: managedObjectContext)
newEvent.attribute = attribute
newEvent.seeMe = seeMe
}
static func save(in managedObjectContext: NSManagedObjectContext) {
do {
try managedObjectContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
To use this example, create a core data model with an entity named "Entity" and two attributes, one named 'attribute' as a String and the other named 'seeMe' as a Bool. Then run it, press the buttons to create the two types of data and then click the bar button item at the top to select which to display.
I'ts not the prettiest of examples, but it should demonstrate the functionality of what you are trying to accomplish.
Use a predicate on the fetch request to search for departments with a specific name like this:
struct ContentView: View {
#State var deptName = "Computing Science"
var body: some View {
EmployeeListView(name:deptName)
}
}
struct EmployeeListView : View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest var depts : FetchedResults<Department>
init(name: name) {
_depts = FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Department.name, ascending: false)], predicate: NSPredicate(format: "name = %#", name)
}
var body: some View {
NavigationView {
List {
ForEach(depts) { dept in
Section(header: Text(dept.name)) {
ForEach(dept.employees, id: \.self) { emp in
Text(emp.name)
}
}
}
}
.navigationBarTitle("Employees")
}
}
}