Unable to use ForEach for distinct records from coredata - core-data

I have a table that contains 20 songs from 5 different artists, this number can go up and down , now i want a View where i can display a navigation of these unique singers with their images that are also in table.
So i need only distinct records based on artistname, i query coredata but when i use it in View , i get Generic struct 'ForEach' requires that 'NSFetchRequest<NSDictionary>' conform to 'RandomAccessCollection'
Now to overcome this i use id:.self, this also does not work.
Now i read and learn that ForEach does not like any TYPE that is not sorted , but i have not been able to find a way around this, can any one kindly suggest any solutions, thanks.
This is how i fetch the unique records from core data
let fetchRequest: NSFetchRequest<NSDictionary> = NSFetchRequest(entityName: "Artists")
init(songLVM: Binding<SongListVM>){
_songLVM = songLVM
fetchRequest.propertiesToFetch = ["artistname"]
fetchRequest.returnsDistinctResults = true
fetchRequest.resultType = .dictionaryResultType
}
Below is the entire file
import Foundation
import SwiftUI
import CoreData
struct ArtistList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Binding var songLVM: SongListVM
#State var namesFilter = [String]()
//-----
let fetchRequest: NSFetchRequest<NSDictionary> = NSFetchRequest(entityName: "Artists")
init(songLVM: Binding<SongListVM>){
_songLVM = songLVM
fetchRequest.propertiesToFetch = ["artistname"]
fetchRequest.returnsDistinctResults = true
fetchRequest.resultType = .dictionaryResultType
}
//-----
var body: some View {
List {
ForEach(fetchRequest) { <---- Error here
idx in
NavigationLink(
destination: ArtistSongs(artistName: idx.artistname ?? "no name", songLVM: $songLVM)) {
ZStack(alignment: .bottomTrailing) {
Image(uiImage: UIImage(data: idx.artistImage ?? Data()) ?? UIImage())
.resizable()
.frame(width: 350, height: 350, alignment: .center)
Text(idx.artistname ?? "no name")
.font(Font.system(size: 50, design: .rounded))
.foregroundColor(.white)
}
}
}
}
}
}

once you figure out what your dictionary is, try this in your ForEach loop:
ForEach(dict.sorted(by: >), id: \.key) { key, value in
...
}
EDIT1:
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Artists.artistname, ascending: true)],
animation: .default)
private var artists: FetchedResults<Artists>
...
ForEach(artists, id: \.self) { artist in
...
}

Related

Core Data, Problem with updating (duplicating instead)

I am new to Swift UI. Could you please help me with core data updating?
Here is the point of a problem:
I am building a WatchOS app. There are 3 Views there:
FirstView - a view with a button to Add a new Goal and a List of added Goals.
AddGoalView - appears after pressing Add new Goal.
RingView - a view with a Goal Ring (similar to activity ring mechanics) and all the data presented.
The point of the problem is the next:
After adding a new Goal everything is alright. The Data passes correctly from AddGoalView to the FirstView. I need only 2 items to be passed out of AddGoalView (One String and one Double).
Then, after pressing on the recently created Goal I appear on the Ring View. I successfully pass there 2 items (that String and Double I mentioned).
On the RingView I want to update the 3-rd Item (double) and send it back. So it can be updated on the FirstView.
the Result:
Instead of updating this 3-d Item it just seems to create a completely new Goal on the First View below the previous Goal. Photo
My Code (FirstView):
struct FirstView: View {
#FetchRequest (
entity:NewGoal.entity(),
sortDescriptors:[NSSortDescriptor(keyPath: \NewGoal.dateAdded, ascending: false)],
animation: .easeInOut )
var results:FetchedResults<NewGoal>
#State var showMe = false
var body: some View {
ScrollView{
VStack{
VStack(alignment: .leading){
Text("My Goals:")
NavigationLink(
destination: AddGoalView(),
isActive: $showMe,
label: {
Image(systemName: "plus")
Text("Set Money Goal")
})
Text("Recents:")
ForEach(results){ item in
VStack(alignment: .leading){
NavigationLink(
destination: RingView(GTitle: item.goalTitle ?? "", Sum: item.neededSum),
label: {
HStack{
Image(systemName: "gear")
VStack(alignment: .leading){
Text(item.goalTitle ?? "")
HStack{
Text("$\(item.yourSum, specifier: "%.f")") ///This item doesn't update
Text("/ $\(item.neededSum, specifier: "%.f")")
}
}
}
})
}
}
}
}
}
}
}
My Code (AddGoalView):
struct AddGoalView: View {
#State private var goalTitle = ""
#State private var showMe:Bool = true
#State private var neededSum:Double = 0.0
#State private var isFocusedNum = false
#Environment(\.managedObjectContext) var context
#Environment(\.presentationMode) var presentationMode
var body: some View {
ScrollView{
VStack (alignment: .leading, spacing: 6){
TextField("Goal Name...", text: $goalTitle)
HStack{
Text("$\(neededSum, specifier: "%.f")")
.overlay(
RoundedRectangle(cornerRadius: 9)
.stroke(isFocusedNum ? Color.red : Color.white, lineWidth: 1)
.opacity(1.0))
.focusable(true) { newState in isFocusedNum = newState}
.animation(.easeInOut(duration: 0.1), value: isFocusedNum)
.digitalCrownRotation(
$neededSum,
from: 0,
through: 100000,
by: 25,
sensitivity: .high)
}
Button(action: addGoal) {
Text("Add Goal")
}
.disabled(neededSum == 0.0)
.disabled(goalTitle == "")
.navigationTitle("Edit")
}
}
}
private func addGoal(){
let goal = NewGoal(context: context)
goal.goalTitle = goalTitle
goal.dateAdded = Date()
goal.neededSum = neededSum
do{
try context.save()
presentationMode.wrappedValue.dismiss()
}catch let err{
print(err.localizedDescription)
}
}
My Code (RingView Code):
struct RingView: View {
#State private var isFocusedSum = false
#State private var yournewSum:Double = 0.0
var goalItem: NewGoal?
var Sum:Double
var GTitle:String
#Environment(\.managedObjectContext) var context
#Environment(\.presentationMode) var presentationMode
#FetchRequest var results: FetchedResults<NewGoal>
init(GTitle: String, Sum: Double){
self.GTitle = GTitle
self.Sum = Sum
let predicate = NSPredicate(format:"goalTitle == %#", GTitle)
self._results=FetchRequest(
entity: NewGoal.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \NewGoal.dateAdded, ascending: false)],
predicate: predicate,
animation: .easeInOut
)
}
var body: some View {
ZStack{
ForEach(results) { item in
RingShape(percent:(yournewSum/item.neededSum*100), startAngle: -90, drawnClockwise: false) /// Ring
.stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round))
.fill(AngularGradient(gradient: Gradient(colors: [.red, .pink, .red]), center: .center))
.frame(width: 155, height: 155)
HStack(alignment: .top){
Spacer()
Button(action: addSum) { ///BUTTON TO Update
Image(systemName: "gear")
}
.clipShape(Circle())
}
VStack(alignment: .trailing, spacing: 0.0){
Spacer()
Text("$\(yournewSum, specifier: "%.f")") /// Here is the data I want to change via Digital Crown and update
.font(.title3)
.overlay(
RoundedRectangle(cornerRadius: 7)
.stroke(Color.white, lineWidth: 2)
.opacity(isFocusedSum ? 1.0:0.0)
)
.focusable(true) { newState in isFocusedSum = newState}
.animation(.easeInOut(duration: 0.3), value: isFocusedSum)
.digitalCrownRotation(
$yournewSum,
from: 0,
through: Double((item.neededSum)),
by: 10,
sensitivity: .high)
Text("/ $\(item.neededSum, specifier: "%.f")") ///Here is the Double data I entered in AddGoalView
.font(.caption)
}
.frame(width: 200, height: 230)
.padding(.top, 7)
VStack(alignment: .center, spacing: 1.0){
Text(item.goalTitle ?? "Your Goal Name") ///Here is the String data I entered in AddGoalView
.foregroundColor(.gray)
}
.padding(.top, 200.0)
}
}
.padding([.top, .leading, .trailing], 5.0)
}
private func addSum(){
let goal = goalItem == nil ? NewGoal(context: context): goalItem
goal?.yourSum = yournewSum //// I am trying to update the Data here, but after running the func it creates a duplicate.
do{
try context.save()
presentationMode.wrappedValue.dismiss()
} catch let err{
print(err.localizedDescription)
}
}
You never give var goalItem: NewGoal? the initial value of the item you want to update.
try replacing this
RingView(GTitle: item.goalTitle ?? "", Sum: item.neededSum)
with
RingView(goalItem: item, GTitle: item.goalTitle ?? "", Sum: item.neededSum)
and of course your have to change your initializer for RingView to
init(goalItem: NewGoal? = nil, GTitle: String, Sum: Double){
and add to the initializer this line
self.goalItem = goalItem

Updating core data entity with observedobject

I have created a list of clients which get saved in core data. I added some attributes through an AddView and now I am trying to add a new attribute using a different view. In order to do that I understood I need to use the observedObject property wrapper so that it doesn't create a new client but update the existing one. This is the code:
import SwiftUI
import CoreData
struct TopicView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Environment (\.presentationMode) var presentationMode
#ObservedObject var topic: StudentData
#State var content = ""
#State private var show = false
#StateObject private var keyboard = Keyboard()
var body: some View {
Form{
Section (header: Text("Topics covered")
.bold()
.padding(.all)
.font(.title3)) {
TextEditor(text: Binding(
get: {self.topic.content ?? ""},
set: {self.topic.content = $0 } ))
.frame(height: 250)
}
}.onTapGesture {
hideKeyboard()
}
}
}
Button ("Submit")
{
self.topic.content = self.content
self.topic.objectWillChange.send()
try? self.managedObjectContext.save()
self.presentationMode.wrappedValue.dismiss()
}.foregroundColor(.white)
This is the view where the new attribute should be shown:
import SwiftUI
import CoreData
import MapKit
struct StudentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: StudentData.entity(),
sortDescriptors: [],
animation: .spring()) var content: FetchedResults<StudentData>
#Environment (\.presentationMode) var presentationMode
#ObservedObject var topic: StudentData
#State var showModalView = false
#State private var showingSheet = false
let myStudent: StudentData
var body: some View {
NavigationView {
VStack{
Text("Topics Covered")
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundColor(.red)
.padding(.horizontal)
Text("\(myStudent.content ?? "")")
.font(.title2)
.bold()
.foregroundColor(.white)
.padding(.horizontal) }}
Spacer()
Spacer()
}.sheet(isPresented: $showModalView, content: {
TopicView(topic: topic)})
The problem is, if I type in the texteditor some text without save it then it shows correctly in the other view. However, when I save it, it does not show in the other view.
Is there anything wrong in my save code?

Core Data Count SwiftUI

When I add a new reminder to my list I can not see the count + 1 simultaneously. But when I re-run the program I see the count is correct.
https://vimeo.com/545025225
struct ListCell: View {
var list : CDListModel
#State var count = 0
#State var isSelected: Bool = false
var body: some View {
HStack{
Color(list.color ?? "")
.frame(width: 30, height: 30, alignment: .center)
.cornerRadius(15)
Text(list.text ?? "")
.foregroundColor(.black)
.font(.system(size: 20, weight: .regular, design: .rounded))
Spacer()
Text(String(count))
.foregroundColor(.gray)
.onAppear{
DispatchQueue.main.async {
self.count = list.reminders!.count
}
}
}
}
}
If CDListModel is a CoreData entity, then you can just add this:
#ObservedObject var list : CDListModel
Also remove the State for the count.
Then display the count like this:
Text(String(list.reminders!.count))
As hint: I wouldn't use force unwrapping aswell. Maybe provide a default value instead of force unwrapping

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