How to replace #FetchRequest by dynamic call - swiftui

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.

Related

Published/Observed var not updating in view swiftui w/ called function

Struggling to get a simple example up and running in swiftui:
Load default list view (working)
click button that launches picker/filtering options (working)
select options, then click button to dismiss and call function with selected options (call is working)
display new list of objects returned from call (not working)
I'm stuck on #4 where the returned query isn't making it to the view. I suspect I'm creating a different instance when making the call in step #3 but it's not making sense to me where/how/why that matters.
I tried to simplify the code some, but it's still a bit, sorry for that.
Appreciate any help!
Main View with HStack and button to filter with:
import SwiftUI
import FirebaseFirestore
struct TestView: View {
#ObservedObject var query = Query()
#State var showMonPicker = false
#State var monFilter = "filter"
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Monday")
Spacer()
Button(action: {
self.showMonPicker.toggle()
}, label: {
Text("\(monFilter)")
})
}
.padding()
ScrollView(.horizontal) {
LazyHStack(spacing: 35) {
ForEach(query.queriedList) { menuItems in
MenuItemView(menuItem: menuItems)
}
}
}
}
.sheet(isPresented: $showMonPicker, onDismiss: {
//optional function when picker dismissed
}, content: {
CuisineTypePicker(selectedCuisineType: $monFilter)
})
}
}
The Query() file that calls a base query with all results, and optional function to return specific results:
import Foundation
import FirebaseFirestore
class Query: ObservableObject {
#Published var queriedList: [MenuItem] = []
init() {
baseQuery()
}
func baseQuery() {
let queryRef = Firestore.firestore().collection("menuItems").limit(to: 50)
queryRef
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.queriedList = querySnapshot?.documents.compactMap { document in
try? document.data(as: MenuItem.self)
} ?? []
}
}
}
func filteredQuery(category: String?, glutenFree: Bool?) {
var filtered = Firestore.firestore().collection("menuItems").limit(to: 50)
// Sorting and Filtering Data
if let category = category, !category.isEmpty {
filtered = filtered.whereField("cuisineType", isEqualTo: category)
}
if let glutenFree = glutenFree, !glutenFree {
filtered = filtered.whereField("glutenFree", isEqualTo: true)
}
filtered
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.queriedList = querySnapshot?.documents.compactMap { document in
try? document.data(as: MenuItem.self);
} ?? []
print(self.queriedList.count)
}
}
}
}
Picker view where I'm calling the filtered query:
import SwiftUI
struct CuisineTypePicker: View {
#State private var cuisineTypes = ["filter", "American", "Chinese", "French"]
#Environment(\.presentationMode) var presentationMode
#Binding var selectedCuisineType: String
#State var gfSelected = false
let query = Query()
var body: some View {
VStack(alignment: .center) {
//Buttons and formatting code removed to simplify..
}
.padding(.top)
Picker("", selection: $selectedCuisineType) {
ForEach(cuisineTypes, id: \.self) {
Text($0)
}
}
Spacer()
Button(action: {
self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text( "apply filters")
})
}
.padding()
}
}
I suspect that the issue stems from the fact that you aren't sharing the same instance of Query between your TestView and your CuisineTypePicker. So, when you start a new Firebase query on the instance contained in CuisineTypePicker, the results are never reflected in the main view.
Here's an example of how to solve that (with the Firebase code replaced with some non-asynchronous sample code for now):
struct MenuItem : Identifiable {
var id = UUID()
var cuisineType : String
var title : String
var glutenFree : Bool
}
struct ContentView: View {
#ObservedObject var query = Query()
#State var showMonPicker = false
#State var monFilter = "filter"
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Monday")
Spacer()
Button(action: {
self.showMonPicker.toggle()
}, label: {
Text("\(monFilter)")
})
}
.padding()
ScrollView(.horizontal) {
LazyHStack(spacing: 35) {
ForEach(query.queriedList) { menuItem in
Text("\(menuItem.title) - \(menuItem.cuisineType)")
}
}
}
}
.sheet(isPresented: $showMonPicker, onDismiss: {
//optional function when picker dismissed
}, content: {
CuisineTypePicker(query: query, selectedCuisineType: $monFilter)
})
}
}
class Query: ObservableObject {
#Published var queriedList: [MenuItem] = []
private let allItems: [MenuItem] = [.init(cuisineType: "American", title: "Hamburger", glutenFree: false),.init(cuisineType: "Chinese", title: "Fried Rice", glutenFree: true)]
init() {
baseQuery()
}
func baseQuery() {
self.queriedList = allItems
}
func filteredQuery(category: String?, glutenFree: Bool?) {
queriedList = allItems.filter({ item in
if let category = category {
return item.cuisineType == category
} else {
return true
}
}).filter({item in
if let glutenFree = glutenFree {
return item.glutenFree == glutenFree
} else {
return true
}
})
}
}
struct CuisineTypePicker: View {
#ObservedObject var query : Query
#Binding var selectedCuisineType: String
#State private var gfSelected = false
private let cuisineTypes = ["filter", "American", "Chinese", "French"]
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack(alignment: .center) {
//Buttons and formatting code removed to simplify..
}
.padding(.top)
Picker("", selection: $selectedCuisineType) {
ForEach(cuisineTypes, id: \.self) {
Text($0)
}
}
Spacer()
Button(action: {
self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text( "apply filters")
})
}
}

How to disable updates in SwiftUI List

I'm working on a SwiftUI + CoreData application which holds (for several entities) a list view with navigation to an edit view where I can make changes.
I decided to directly pass the managed object from the list to the edit view (observed) to avoid even one more code duplication of listing all attributes. My edit view is able to revert the changes made to the managed object when I go back to list without saving.
However I have a weird behavior: whenever I edit the field which is responsible for the sorting in the list view, e.g. title, and this changes the order in the list, the view slides back to list view and immediately forth to edit view. I can see, that the edited item changed position in the list. This happens on the first keypress that changes the title accordingly.
I want to avoid this (because it's distracting and I lose focus on the title field).
Any way to disable updates to the list or at least disable the switching to list and back?
struct GameListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \Game.title, ascending: true),
NSSortDescriptor(keyPath: \Game.externalId, ascending: true),
],
animation: .default)
private var games: FetchedResults<Game>
#State private var showDetailView = false
var body: some View {
NavigationView {
List {
ForEach(games) { game in
NavigationLink(destination: GameEditView(game: game)) {
HStack {
Image("game-icon")
Text("\(game.title)")
}
}
}
}
.navigationBarTitle(NSLocalizedString("Games", comment: "NavigationBar title"))
}
}
}
struct GameEditView: View {
#ObservedObject var game: Game
#State private var showingCancelSheet = false
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
var body: some View {
VStack {
// TODO: add all the attributes of Game
GameFormView(title: $game.title, onSave: {
try game.validate()
try game.save()
presentationMode.wrappedValue.dismiss()
})
}
.navigationBarTitle(NSLocalizedString("Edit Game", comment: "NavigationBar title"))
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
if game.hasChanges {
self.showingCancelSheet = true
} else {
self.presentationMode.wrappedValue.dismiss()
}
},
label: {
Image(systemName: "chevron.left")
Text("Games", comment: "Button label back to list of games")
})
}
}
}
}
struct GameAddView: View {
#State private var title = ""
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
var body: some View {
GameFormView(title: $title, onSave: {
try Game.validate(title: self.title, game: nil, context: viewContext)
let game = Game.create(title: self.title, context: viewContext)
try game.save()
presentationMode.wrappedValue.dismiss()
})
.navigationBarTitle(NSLocalizedString("New Game", comment: "NavigationBar title"))
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(action: cancel) {
Text("Cancel", comment: "Button label cancel add game")
}
}
}
}
func cancel() {
self.presentationMode.wrappedValue.dismiss()
}
}
struct GameFormView: View {
#Binding var title : String
// TODO: add all the attributes of Game
public var onSave: () throws -> Void
#State private var titleError: String? = nil
var body: some View {
Form {
Section(header: Text("Game Title", comment: "Section header")) {
VStack {
TextField(NSLocalizedString("Title", comment: "TextField label game form"), text: $title)
if self.titleError != nil {
Text(self.titleError!)
.fontWeight(.light)
.font(.footnote)
.foregroundColor(.red)
}
}
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: saveForm,
label: { Text("Save", comment: "Button label save game") })
}
}
}
func saveForm() {
do {
try self.onSave()
} catch GameValidationError.emptyName {
self.titleError = NSLocalizedString("Title can't be empty", comment: "Validation error on game")
} catch GameValidationError.duplicateTitle(_) {
self.titleError = NSLocalizedString("Title already exists", comment: "Validation error on game")
} catch let error as NSError {
print("Unexpected error: \(error), \(error.userInfo)")
}
}
}

How to delete all items from Picker view programmatically

I have following code in my SwiftUI app
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)
}
}
}
I do CoreData Country entity update in SettingView and once app is back in ContentView I`d like to delete all items from the Picker and load fresh data. Code above duplicate items in the Picker - add new ones to old set.

Not getting any result out of my SwiftUI Picker for a complex type

I wanted to use a SwiftUI Picker for a complex type. I see the picker and I can select a value, but I never get the didSet output and category always stays nil. Any suggestions?
struct EntryView: View {
#State private var category: UUID? = UUID() {
didSet {
print("category changed to \(category!)")
}
}
#FetchRequest(
entity: Category.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Category.title, ascending: true)
]
) var categories: FetchedResults<Category>
var body: some View {
NavigationView {
Form {
Section {
Picker("Meter", selection: $category) {
ForEach(categories) { cat in
Text(cat.title ?? "")
}
}
}
}
}
}
}

How to rerun #FetchRequest with new predicate based on user input?

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")
}
}
}