Binding var inside NSPredicate - SwiftUI - core-data

I am trying to compose a dynamic filter in my Codre Data filters like this (using SwiftUI):
#State var NumberOfRooms: Int
init(NumberOfRooms: Int) {
self.NumberOfRooms = NumberOfRooms
}
#FetchRequest(entity: Listing.entity(),sortDescriptors:[NSSortDescriptor(keyPath: \Listing.publishdate, ascending: false),],predicate: NSPredicate(format: "category == %# AND rooms ", "House", "\($NumberOfRooms)"))
In the view i want to modify the value of the variable like this:
Text("2")
.onTapGesture {
self.NumberOfRooms = 2
}
I get the error at NSPredicate:
Cannot use instance member '$NumberOfRooms' within property initializer; property initializers run before 'self' is available
Basically i want to dynamically compose the predicates.
Any help is deeply appreciated.

Initialize your request inside the init like this.
var fetchRequest: FetchRequest<Listing>
#State var NumberOfRooms: Int = 10
init(NumberOfRooms: Int) {
fetchRequest = FetchRequest<BlogIdea>(entity: Listing.entity(),sortDescriptors:[],predicate: NSPredicate(format: "category == %# AND rooms ", "House", "\(NumberOfRooms)"))
self.NumberOfRooms = NumberOfRooms
}

Related

Swiftui CoreData one to many relationship

For example two models Person and Car. Person has many cars. My question is how to add car to already created Person. If I tray in AddNewCarView:
var person: Person
...
var newCar = Car(context: viewContext)
newCar.name = name
newCar.model = model
And now, what...?
newCar.person = person (gives newCar to all persons)
Or.
person.addToCars(newCar)
Is that something I have to add in predicate? In Car I have:
static func fetchCars() -> NSFetchRequest<Car> {
let request: NSFetchRequest<Car> = Car.fetchRequest()
reqest.predicate = NSPredicate(format: "", ) ..?
request.sortDescriptors = [NSSortDescriptor(keyPath: \Car.timestamp, ascending: true)]
return request
}

SwiftUI and Core Data: Using a fetch request with instance member as argument inside a view

I seem to be caught in a Catch-22 situation. Perhaps my approach is entirely wrong here. I hope someone can help. I want to create a star-based rating display using feedback from users who visit a particular real world landmark.
In CoreData I have an entity called Rating with attributes called rating (Int32) and landmark (String). I want to get the average for all rating(s) associated with a given landmark in order to display stars in the view for each.
Here is the code for the View:
struct TitleImageView: View {
#Environment(\.managedObjectContext) var viewContext : NSManagedObjectContext
let landmark: Landmark
var body: some View {
Image(landmark.imageName)
.resizable()
.shadow(radius: 10 )
.border(Color.white)
.scaledToFit()
.padding([.leading, .trailing], 40)
.layoutPriority(1)
.overlay(TextOverlay(landmark: landmark))
.overlay(RatingsOverlay(rating: stars))
}
}
Here is the fetch (which works as expected when the argument for the fetch is hard coded):
let fetchRequest = Rating.fetchRequestForLandmark(landmark: landmark.name)
var ratings: FetchedResults<Rating> {
fetchRequest.wrappedValue
}
var sum: Int32 {
ratings.map { $0.rating }.reduce(0, +)
}
var stars : Int32 {
sum / Int32(ratings.count)
}
The problem is this: When I insert the fetch before the body of the view, I get the warning
"Cannot use instance member 'landmark' within property initializer; property initializers run before 'self' is available"
When I place the fetch after the body, I get:
"Closure containing a declaration cannot be used with function builder 'ViewBuilder'" (with reference to var ratings)
Is there an easy way out of this conundrum or must I go back to the proverbial drawingboard? Thanks.
How about wrapping the fetch in a calculated property that returns a sum & stars tuple? Slightly different shape with the same results. Calculated properties can reference other properties, so the swift-init hurdle is cleared!
var metrics : (sum: Int, stars: Int) {
let fetchRequest = Rating.fetchRequestForLandmark(landmark: landmark.name)
var ratings: FetchedResults<Rating> {
fetchRequest.wrappedValue
}
let sum = ratings.map { $0.rating }.reduce(0, +)
let stars = sum / ratings.count
return (sum: sum, stars: stars)
}
Thanks to those who offered suggestions. After a long hiatus, I returned to this problem and arrived at this solution (based on a HackingWithSwift tutorial: https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui)
Here's the code:
struct RatingsView: View {
var fetchRequest: FetchRequest<Rating>
var body: some View {
HStack(spacing: 0.4){
ForEach(1...5) { number in
if number > stars {
//Image(systemName: "star")
} else {
Image(systemName: "star.fill")
}
}
}
}
init(filter: String) {
fetchRequest = FetchRequest<Rating>(entity: Rating.entity() , sortDescriptors: [], predicate: NSPredicate(format: "%K == %#", "landmark" , filter))
}
var sum: Int16 {
fetchRequest.wrappedValue.reduce(0) { $0 + $1.rating }
}
var stars : Int {
var starCount:Int
if fetchRequest.wrappedValue.count > 0 {
starCount = Int(sum) / fetchRequest.wrappedValue.count
} else {
starCount = 0
}
return starCount
}
}
I'm not sure if this is the absolute best solution, but it's working as intended.
Hope this can help others.

How to use AppStorage in coreData's predicate

I tried to use the value of AppStore in CoreData's predicate.
struct MyView: View {
#AppStorage("valueFilter") private var valueFilter = ""
#Environment(\.managedObjectContext) var moc
#FetchRequest(
entity: Value.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "value_filter == %#", valueFilter) // report error in this line.
) var values: FetchedResults<Value>
...
}
But it report Cannot use instance member 'valueFilter' within property initializer; property initializers run before 'self' is available in predicate line.

How to update core data info in a SwiftUI view

I have reached an impasse. I would like to display data from CoreData to be edited and then update it back into the database. There doesn't seem to be any way to do this but it is such a basic operation I'm sure the problem is on my end.
Here's the code I have
import SwiftUI
import CoreData
struct CustomerEditView: View
{
#Environment(\.managedObjectContext) var context
#State var firstName = ""
#State var lastName = ""
#State var phone = ""
#State var email = ""
#State var birthday = ""
var customer: Customer
var body: some View
{
//firstName = customer.firstName!
//lastName = customer.lastName!
//phone = customer.phone!
//email = customer.email!
return VStack
{
TextField("First Name", text: $firstName)
TextField("Last Name", text: $lastName)
TextField("Cell Phone", text: $phone)
TextField("EMail", text: $email)
TextField("Birthday", text: $birthday)
Button("Save")
{
do
{
//customer.birthday = self.birthday
customer.firstName = firstName
customer.lastName = lastName
customer.phone = phone
customer.email = email
try context.save()
}
catch
{
print(error.localizedDescription)
}
}
}
}
}
I've tried a number of ways to update the State variables from the CoreData Customer entity but there doesn't seem to be any way in SwiftUI to do this without triggering warnings about modifying the state values during an update operation. I'll accept any solution but it seems like there should be a means of accomplishing this without needing to use temporary state variables, just references to the core data attributes.
Here is a way to go
struct CustomerEditView: View
{
#Environment(\.managedObjectContext) var context
#State var firstName: String // << declare only
// ... other states are the same
var customer: Customer
init(customer: Customer) {
self.customer = customer
// create state with initial value from customer
_firstName = State(initialValue: customer.firstName ?? "") // << here !!
// ... other states are the same
}
var body: some View
{
// .. other your code

reload FetchedResults when changing the predicate in swiftUI

edit: so far i think i've narrowed the problem done to the fetchresult in listview.
when the fetchrequest(predicate) changes, it doesn't refetch.
i'm building a todo app with swiftUI and Core Data.
in the listView i have a button that'll change the predicate for the fetchrequest, but i can't get it to work.
code in the listView
struct HomeListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: ItemEntity.loadItems()) var items: FetchedResults<ItemEntity>
Text("someText")
.onTapGesture {
ItemEntity.predicateType.next() //next() is a method defined in core data entity class
}
code in the core data entity class
extension ItemEntity {
static var sortType = sort.ascending
static var predicateType = predicate.all
static func loadItems() -> NSFetchRequest<ItemEntity> {
var request: NSFetchRequest<ItemEntity> = ItemEntity.fetchRequest() as! NSFetchRequest<ItemEntity>
var predicate: NSPredicate {
switch predicateType {
case .all:
return NSPredicate(format: "type = %i", 0)
case .imur:
return NSPredicate(format: "type = %i", 0)
case .unimur:
return NSPredicate(format: "type = %i", 1)
case .imunur:
return NSPredicate(format: "type = %i", 2)
case .unimunur:
return NSPredicate(format: "type = %i", 3)
case .entry:
return NSPredicate(format: "type = %i", 4)
}
request.sortDescriptors = [sortDescriptor]
request.predicate = predicate
return request
}
enum predicate {
case all
case imur
case unimur
case imunur
case unimunur
case entry
mutating func next() {
switch self {
case .all:
self = .imur
case .imur:
self = .unimur
case .unimur:
self = .imunur
case .imunur:
self = .unimunur
case .unimunur:
self = .entry
case .entry:
self = .all
}
}
}
}
the idea is when user tap on the button on list view, it call the next() method and set predicateType property in ItemEntity class to another value, then the predicate property in loadItems() will update the fetchrequest, then the listview will reload.
i know there is something wrong with this approach, but i can't figure out how to fix it.
Thanks for helping!
You need to change some state so that the view re-renders.
Try adding a #State var, change it from the button action and use it in the view:
struct HomeListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: ItemEntity.loadItems()) var items: FetchedResults<ItemEntity>
#State private var refresh: Bool = false // add a state
var body: some View {
Text("someText")
.onTapGesture {
ItemEntity.predicateType.next()
refresh.toggle() // change the state
}
.background(toggle ? Color.clear : Color.clear) // use the state
}
}

Resources