How do I use a property as an argument to a FetchRequest predicate - core-data

I am trying to select database items with a predicate before displaying a view and need to pass a property, but I'm in a catch-22 situation since the property may not be initialized, yielding the message: Cannot use instance member 'subject' within property initializer; property initializers run before 'self' is available
struct ShowInfo: View
{
#State var subject: Subject
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Info.entity(), sortDescriptors: [NSSortDescriptor(key: "title", ascending: true)],
predicate: NSPredicate(format: "subject.title == %#", $subject.title)
) var infos: FetchedResults<Info>
#State var size = UIScreen.main.bounds.width / 3
var body: some View
{
List
{
Text("Info").font(.system(size: 24)).foregroundColor(Color.green)
ForEach(infos, id: \.infoid)
{ info in
ZStack
{
if info.subject == self.subject.title
{
NavigationLink(destination: EditInfoView(info: info))
{
HStack
{
Text(info.title!).frame(width: 150, height: 40).background(Color.blue).foregroundColor(Color.white).cornerRadius(10)
Text(info.value!)
}
}
}
}
}.onDelete(perform: deleteInfo)
}
}
}

The answer found at SwiftUI View and #FetchRequest predicate with variable that can change is essentially correct, but I discovered that the order in which things appear inside the struct matters to avoid a slew of errors.
I modified my code as follows...
struct ShowInfo: View
{
init (subject: Subject)
{
self.subject = subject
self.infoRequest = FetchRequest<Info>(entity: Info.entity(), sortDescriptors: [NSSortDescriptor(key: "title", ascending: true)],predicate: NSPredicate(format: "subject.title == %#", subject.title!))
}
#Environment(\.managedObjectContext) var moc
var subject: Subject
var infoRequest: FetchRequest<Info>
var infos: FetchedResults<Info>{infoRequest.wrappedValue}
This cleared up all the errors and allowed the app to work correctly. When the init() was after the variable declarations, I got the same error as before as well as other errors. The compiler seemed to be getting confused about what had already been initialized and what had not. I'm not sure why the example shown for question 57871088 worked for others and mine required rearranging the declarations.

I thought the above answer solved my problem, and it did in that case, but in another view the same methodology yielded errors claiming that I hadn't initialized all the properties within the init. After much experimenting, I discovered all those errors went away if I commented out the #Environment statement. Of course that generated errors for the undefined variable moc, so I commented that code out to see what happened, then the original errors reappeared. It seems clear that the compiler is getting confused. Does anyone know a way around that issue?
Here's the code I'm working with now:
struct EditSubjectView: View
{
init(subject: Subject)
{
self.subject = subject
self.formRequest = FetchRequest(entity: Field.entity(), sortDescriptors: [NSSortDescriptor(key: "sequence", ascending: true)], predicate: NSPredicate(format: "subjectid == %#", subject.subjectid!.description))
}
#Binding var subject: Subject
var formRequest: FetchRequest<Field>
var fields : FetchedResults<Field>{formRequest.wrappedValue}
#Environment(\.managedObjectContext) var moc
var body: some View
{
return VStack
{
HStack
{
Text("Subject Title").padding(.leading)
//Spacer()
}
TextField(subject.title!, text: Binding($subject.title)!)
.padding(.all)
.background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0))
.cornerRadius(15)
Spacer()
Button("Save")
{
do
{
try self.moc.save()
}
catch
{
print(error.localizedDescription)
}
}.frame(width: 150, height: 30).background(Color.red).foregroundColor(Color.white).cornerRadius(15.0)
}.padding()
}
}

Related

Dynamic sort descriptor FetchRequest SwiftUI

I'm having trouble figuring out how to dynamically update Core Data's FetchRequest's NSSortDescriptor in SwiftUI. I'm not sure if setting it in .onAppear is the correct way which it doesn't as I'm seeing a strange rearranging of my list. I'm using an AppStorage variable to store my sorting then I set the NSSortDescriptor .onAppear, and save when state changes. Also I detect any changes in my Picker selection and apply it to my FetchRequest configuration's sort descriptor like Apple describe's here. If I remove the FetchRequest's animation I don't see the strange rearranging but I also don't get any animations which doesn't look nice. I'm really unsure how to solve this or if I'm even doing this right.
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
// Set default sort descriptor as empty because it's set in onAppear
#FetchRequest(
sortDescriptors: [],
animation: .default) // or set to .none
private var items: FetchedResults<Entity>
#AppStorage("sortby") var sortby: SortBy = .title
var body: some View {
NavigationView {
List {
ForEach(items) { item in
Text(item.title)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
ForEach(sort.allCases) { sort in
Button(sort.title, action: {
sortby = sort
})
}
}
}
// When sort changes set the new value here
.onChange(of: sortby, perform: { value in
items.nsSortDescriptors = //NSSortDescriptor here
})
// Set initial sort descriptor here
.onAppear(perform: {
items.nsSortDescriptors = //NSSortDescriptor here
})
}
}
}
FetchRequest's animation is set to default and rearranging.
FetchRequest's animation is set to none, it works but then no animations become available when sorting.

SwiftUI List with Sections fetched from Core Data

Back in objective-c, I could fetch a sectioned list of objects from Core Data using something like this:
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"ispurchased"
cacheName:nil];
And then the NSFetchedResultsController would automatically give me the data in sections and rows.
I'm experimenting with SwiftUI for the first time and I'm trying to figure out how to achieve a sectioned List like that. I've found lots of examples that use some canned array data that is pre-structured in the sectioned fashion, but I can't figure out how to do a sectioned FetchRequest nor how to integrate that with the List.
struct EasyModeList: View {
#FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \EasyMode.ispurchased, ascending: true),
NSSortDescriptor(keyPath: \EasyMode.ispurchasable, ascending: false),
NSSortDescriptor(keyPath: \EasyMode.name, ascending: true),
],
animation: .default)
var easymodes: FetchedResults<EasyMode>
#Environment(\.managedObjectContext)
var viewContext
var body: some View {
List {
ForEach(self.easymodes, id: \.self) { easymode in
NavigationLink(
destination: DetailView(easymode: easymode)
) {
VStack {
Text("\(easymode.name!)")
}
}
}
}
}
}
Does SwiftUI easily support those kinds of sectioned lists? Is there a different paradigm that I should be shifting my brain to?
I am also looking for a proper solution to this. For now I'll share what I've tried. My sections are by string titles, but I'll adapt it for your data.
#FetchRequest(...) var managedEasyModes: FetchedResults<EasyMode>
private var easyModes: [String: [EasyMode]] {
Dictionary(grouping: managedEasyModes) { easymode in
easymode.ispurchased ? "Purchased" : "Not Purchased"
}
}
private var sections: [String] {
easyModes.keys.sorted(by: >)
}
var body: some View {
ForEach(sections, id: \.self) { section in
Section(header: Text(section)) {
ForEach(easyModes[section]!) { easyMode in
NavigationLink(destination: EasyModeView(easyMode: easyMode)) {
EasyModeRowView(easyMode: easyMode)
}
}
}
}
}

edit a Core Data Object in SwiftUI

I push an object that comes from the Core Data Database to a detail page with text fields.
When the user changes the text in the textfields and presses save the changes should be saved to the core data DB.
Problem: I have no clue how to modify/update/change an existing core data entity.
I probably need to get the original via a #FetchRequest but every time I try I get some problems.
Question 1: Let's say the entity has object.id as UUID, how can I
fetch that object in SwiftUI?
Question 2: How can I overwrite the fetched object with the changed
content of the textfields?
struct ProductDetail: View {
#State var barcode: Barcode
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var context
//let datahandler = Datahandler()
var body: some View {
VStack {
HStack {
Text("Barcode: ")
Spacer()
TextField("Barcode", text: Binding($barcode.code, ""))
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 250, alignment: .trailing)
}
HStack {
Text("Amount: ")
Spacer()
TextField("Amount", text: Binding($barcode.amount, ""))
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 250, alignment: .trailing)
.keyboardType(.numberPad)
}
HStack{
Button("Back") {
self.presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save"){
//self.datahandler.updateBarcode(barcode: self.object)
self.editBarcode(barcode: barcode)
self.presentationMode.wrappedValue.dismiss()
}
}
.padding()
}
.padding()
}
func editBarcode(barcode: Barcode) {
// Question 1: Fetch Original object using barcode.id
// Question 2: How to but barcode into context so it can overwrite the core data original?
try? context.save()
}
Attempt:
func editBarcode(barcode: Barcode) {
#FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "self.id IN %#", barcode.id)) var results: FetchedResults<Barcode>
results.first?.amount = barcode.amount
results.first?.code = barcode.code
try? context.save()
}
Errors:
Argument type 'UUID?' does not conform to expected type 'CVarArg'
Cannot use instance member 'barcode' within property initializer;
property initializers run before 'self' is available
Property wrappers are not yet supported on local properties

How to deal with master/detail CoreData between SwiftUI views

I'm dealing with issue how to pass parameter selected in master view to CoreData predicate in detail view. I have this master view
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: \.self) { country in
Text(country.cntryName ?? "Error").tag(country as Country?)
}
}
if selectedCountry != nil {
Years(cntryName: selectedCountry?.cntryName! ?? "")
}
}
}
.navigationBarTitle("UNECE Data")
.navigationBarItems(trailing: Button("Settings", action: {
self.showSetting.toggle()
}))
}
.sheet(isPresented: $showSetting) {
SettingsView(showSetting: self.$showSetting)
}
}
}
where I use Picker to select country name (from CoreData entity Country and its attribute cntryName) and pass it as String value to Years view which is coded like this
struct Years: View {
var cntryName: String
#State private var selectedDataRow: Data?
#State private var result: NSFetchRequestResult
#FetchRequest(entity: Data.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Data.dataYear, ascending: true)],
predicate: NSPredicate(format: "dataCountry == %#", "UK"), animation: .default
) var data: FetchedResults<Data>
var body: some View {
Picker("Year", selection: $selectedDataRow) {
ForEach(data, id: \.self) { dataRow in
Text(dataRow.dataYear ?? "N/A")
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: CGFloat(UIScreen.main.bounds.width), height: CGFloat(100))
.clipped()
.onAppear() {
let request = NSFetchRequest<Data>(entityName: "Data")
request.sortDescriptors = [NSSortDescriptor(key: "dataYear", ascending: true)]
request.predicate = NSPredicate(format: "dataCountry == %#", self.cntryName)
do {
self.result = try context.fetch(request) as! NSFetchRequestResult
print(self.result)
} catch let error {
print(error)
}
}
}
}
It works fine with #FetchRequest and FetchedResults stored in var data but I'm wondering how to build predicate here based on passed country name. To overcome this I considered to use onAppear section and classic NSFetchRequest and NSFetchRequestResult which causes compiler error "'Years.Type' is not convertible to '(String, NSFetchRequestResult, FetchRequest) -> Years'" in the line
Years(cntryName: selectedCountry?.cntryName! ?? "")
of ContentView struct. Error disappear if I comment the line
#State private var result: NSFetchRequestResult
in Years struct but it obviously causes another error. So I'm lost in circle. What`s recommended practice here, please?
Thanks.
Finally I found the way thanks to this post SwiftUI use relationship predicate with struct parameter in FetchRequest
struct Years: View {
var request: FetchRequest<Data>
var result: FetchedResults<Data> {
request.wrappedValue
}
#State private var selectedDataRow: Data?
init(cntryName: String) {
self.request = FetchRequest(entity: Data.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Data.dataYear, ascending: true)],
predicate: NSPredicate(format: "dataCountry == %#", cntryName), animation: .default)
}
var body: some View {
VStack {
Picker("Year", selection: $selectedDataRow) {
ForEach(result, id: \.self) { dataRow in
Text(dataRow.dataYear ?? "N/A").tag(dataRow as Data?)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: CGFloat(UIScreen.main.bounds.width), height: CGFloat(100))
.clipped()
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Total polutation: ")
.alignmentGuide(.leading) { dimension in
10
}
if selectedDataRow != nil {
Text(String(describing: selectedDataRow!.dataTotalPopulation))
} else {
Text("N/A")
}
}
}}
}
}

Is there a way to modify fetched results with a predicate after they are initialized?

I am trying to build a search view for an existing CoreData app (simple logging app).
I have all the data stored with CoreData and is fetched with the #FetchRequest:
#State private var searchPredicate: NSPredicate? = NSPredicate(format: "title contains[c] %#", "")
#FetchRequest( entity: Item.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Item.title, ascending: true)],
predicate: NSPredicate(format: "title contains[c] %#", "h")
)
var items: FetchedResults<Item>
It now only fetches the items that pass the predicate test which in this case are all the ones that contain an "h".
I then display the results in a List in the body of the SearchView:
List {
ForEach(Items) { Item in
ListViewItem(title: Item.title!, subTitle: Item.subTitle!, createdAt: "\(Item.createdAt!)")
}
}
I then created a new class "Searchbar" which is called in the searchview and is supposed to create a predicate based on the input of the search field and then pass it on as a Binding to the parent where then based on that predicate the correct items can be displayed.
Calling the searchbar at the top of a VStack in the searchview:
SearchBar(text: $searchText, predicate: $searchPredicate)
The Bindings change depending on the user input in the "searchBar":
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
predicate = NSPredicate(format: "title contains[c] %#", searchText)
}
So far so good...
The problem I have now run into is that we have a predicate that works but can't be called within the #Fetchrequest in the definitions since it would be called before its initialized.
#State private var searchPredicate: NSPredicate? = NSPredicate(format: "title contains[c] %#", "")
#FetchRequest(
entity: Item.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Item.title, ascending: true)
],
predicate: searchPredicate
) var items: FetchedResults<Item>
This gives me the error that the property initializer can't be run before self is available which is logical but makes me wonder and brings me back to my question: Is there a way to modify fetched results with a predicate after they are initialized?
I have also tried calling predicate related methods on the fetched results in the ForEach() statement but none of them seemed to have worked.
If there are any questions please do not hesitate to ask.
Is there a way to modify fetched results with a predicate after they
are initialized?
Well... no, not in the way you try to do this, and even if you'd try to create it with NSFetchRequest instance, which is reference, and allows to change predicate later, that wouldn't work, because SwiftUI's FetchRequest stores copy of provided fetch request (or creates own with provided parameters)... so, no. But...
You can break apart view providing fetch request parameters with view constructing fetch request and showing result.
Here is a demo of approach (important part of it) which gives you possibility to get results with different dynamically changed predicates:
struct MasterView: View {
#State var predicate: NSPredicate? = nil
var body: some View {
VStack {
Button(action: { // button just for demo
self.predicate = NSPredicate(format: "title contains[c] %#", "h")
}, label: { Text("Filter") })
ResultView(predicate: self.predicate)
}
}
}
struct ResultView: View {
#FetchRequest
var events: FetchedResults<Event>
#Environment(\.managedObjectContext)
var viewContext
init(predicate: NSPredicate?) {
let request: NSFetchRequest<Event> = Event.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Event.timestamp, ascending: true)]
if let predicate = predicate {
request.predicate = predicate
}
_events = FetchRequest<Event>(fetchRequest: request)
}
var body: some View {
List {
ForEach(events, id: \.self) { event in
...
I decided to post the fully working version possible with the great answer provided by Asperi since I haven't found a working solution anywhere else.
struct MasterView: View {
#State var predicate: NSPredicate? = nil
#State private var searchText = ""
var body: some View {
VStack {
TextField("Search", text: $searchText, onEditingChanged: {_ in
self.predicate = NSPredicate(format: "title contains[c] %#", "\(self.searchText)")
print("THE PREDICATE: \(String(describing: self.predicate))")
}, onCommit: {
print("onCommit")
}).foregroundColor(.primary)
SearchView(predicate: self.predicate)
}
}
}
And the SearchView:
struct SearchView: View {
#State private var searchText = ""
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest var items: FetchedResults<Item>
init(predicate: NSPredicate?) {
let request: NSFetchRequest<Item> = Item.fetchRequest() as! NSFetchRequest<Item>
request.sortDescriptors = [NSSortDescriptor(keyPath: \Item.title, ascending: true)]
if let predicate = predicate {
request.predicate = predicate
}
_items = FetchRequest<Item>(fetchRequest: request)
}
var body: some View {
VStack {
List {
...
As a little update to what #Asperi answered (Swift 5.4 Xcode 12.5), in the ResultView you can do this now:
var fetchRequest: FetchRequest<Event>
init(predicate: NSPredicate?) {
fetchRequest = FetchRequest<Event>(entity:
Event.entity(), sortDescriptors: [NSSortDescriptor(keyPath:
\Event.timestamp, ascending: true)], predicate: predicate)
}
As FetchRequest takes NSPredicate? in its constructor:
public init(entity: NSEntityDescription, sortDescriptors: [NSSortDescriptor], predicate: NSPredicate? = nil, animation: Animation? = nil)

Resources