SwiftUI: Group and Sum by Core Data Attribute - core-data

The question is this: how do you group Core Data objects by attribute and then perform simple calculations (let's say, sum) on them?
I've found a few similar answers for this (like this one), but none seem conclusive and all are geared up towards Swift (whereas I am working on SwiftUI).
For example, I have a Core Data model that looks like this:
extension Items {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Items> {
return NSFetchRequest<Items>(entityName: "Items")
}
#NSManaged public var id: UUID?
#NSManaged public var cost: Int16
#NSManaged public var type: String
}
In the Home() view, I'd like to group by type and then find the total amount paid (i.e. sum of cost) for all objects in each type.
So far, I've tried making a special group() function (a bit like in this question), but get the error message "Cannot find 'i' in scope".
struct Home: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: Items.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Items.type, ascending: true)]) var items: FetchedResults<Items>
var body: some View {
ForEach(group(items), id: \.self) { [i] in
Text("\([i].cost.reduce(0,+))")
}
}
}
func group(_ result : FetchedResults<Items>)-> [[Items]] {
return Dictionary(grouping: result) { $0.type }
.sorted(by: {$0.key < $1.key})
.map {$0.value}
}
Surely this can't be so hard? Any help very welcome!

You should not have brackets around the i in [i] in. Brackets like that capture a value, but you don't have a variable to capture -- you're just getting a parameter for the closure.
ForEach(group(items), id: \.self) { i in
On your next line, you're dealing with a multidimensional array. You can't access a property directly, but you could map first:
Text("\(i.map(\.cost).reduce(0,+))")
You could also structure this slightly differently and remove the map by doing:
i.reduce(0,{ $0 + $1.cost })

Related

How can I run a simple function on Core Data results

Apologies if this is a very basic question, I'm just learning how to use Core Data.
I'm trying to run a simple function (Text("\(items.timestamp.count)")) on the data Apple gives you by default (a list of items with just one attribute: timestamp). However, I keep getting the error "Value of type 'FetchedResults' has no member 'timestamp".
Can anyone explain why this happens? When the timestamp attribute is accessed within the ForEach loop automatically provided by Apple, everything seems to works fine.
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
Text("\(items.timestamp.count)")
List {
ForEach(items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
it's because items (aka FetchedResults) is an array of item. So it can contain multiple returned Items.
The ForEach is working because a ForEach by nature iterates through the array and displays each one.
if you'd like to just display the count of items then remove the "timestamp" part:
Text("\(items.count)")
if you'd like to display the timestamp then you'd need to specify which item in the array you want to display the timestamp for. so to display the timestamp for the first item in the array then you can do:
Text("\(items[0].timestamp ?? Date())")
(note that if there are no items in the database then you will get today's date. Also it's more common to use a list and ForEach which will display all records in a list including timestamps (like you have in the rest of Apple's boilerplate code))

Crash when delete a record from coredata in SwiftUI

I make a list for audio items from coredata. after deleting, crash reported as "EXC_BREAKPOINT (code=1, subcode=0x1b8fb693c)", why?
When using
ForEach(items, id: \.self)
, it works. But My Audio has id property and follow Identifiable protocol.
UPDATE: I found adding a if{} clause will fix crash, but why? Breakpoint at "static UUID.unconditionallyBridgeFromObjectiveC(:) ()".
struct Test1View: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: Audio.fetchAllAudios()) var items: FetchedResults<Audio>
var body: some View {
List {
ForEach(items) { item in
if true { // <- this if clause fix crash, but why?
HStack {
Text("\(item.name)")
}
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
code as following:
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
struct Test1View: View {
#Environment(\.managedObjectContext) var context
var fetchRequest: FetchRequest<Audio> = FetchRequest<Audio>(entity: Audio.entity(), sortDescriptors: [NSSortDescriptor(key: "createAt", ascending: false)])
var items: FetchedResults<Audio> { fetchRequest.wrappedValue }
var body: some View {
List {
ForEach(items) { item in
HStack {
Text("\(item.name)")
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
I had the same problem as you.
Probably it is a bad idea to use your own id property to make it Identifiable because Core Data is setting all the properties to nil when the object is deleted, even if you declared it differently in your Swift CoreData class.
When deleting your entity, the id property gets invalidated and the objects .isFault property is set to true, but the SwiftUI ForEach still holds some reference to this ID object (=your UUID) to be able to calculate the "before" and "after" state of the list and somehow tries to access it, leading to the crash.
Therefore the following recommendations:
Protect the detail view (in the ForEach loop by checking isFault:
if entity.isFault {
EmptyView()
}
else {
// your regular view body
}
Expect your id property to be nil, either by defining it accordingly in your core data model as optional
#NSManaged public var id: UUID?
or by not relying on the Identifiable protocol in the SwiftUI ForEach loop:
ForEach(entities, id: \.self) { entity in ... }
or
ForEach(entities, id: \.objectID) { entity in ... }
Conclusion: you really do not need to make all your CoreData properties Swift Optionals. It's simply important that your id property referenced in the ForEach loop handles the deletion (=setting its value to nil) gracefully.
I found the reason of crash, must provide optional, because of OC/swift object conversion:
convert
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
to
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID?
#NSManaged public var name: String?
#NSManaged public var createAt: Date?
}
I had the same issue over the weekend. It looks like SwiftUI want's to unwrap the value i read from CoreData and as the value is already deleted it crashes.
In my case i did solve it with nil coalescing on all values i use from CoreData.
You can try to provide a default value on your item.name with
ForEach(items) { item in
HStack {
Text("\(item.name ?? "")")
}
}

How to reload a row of SwiftUI Core Data-backed list if object properties change?

I have a standard SwiftUI list setup, powered by Core Data FetchRequest.
struct SomeView: View {
var container: Container
var myObjects: FetchRequest<MyObject>
init(container: Container) {
let predicate : NSPredicate = NSPredicate(format: "container = %#", container)
self.container = container
self.myObjects = FetchRequest<MyObject>(entity: MyObject.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)], predicate: predicate)
}
var body: some View {
VStack(spacing: 0.0) {
List(myObjects.wrappedValue, id: \.uniqueIdentifier) { myObject in
rowView(for: myObject, from: self.myObjects.wrappedValue)
}
}
}
}
Everything works well when items are added and deleted. RowView returns a view that presents different content based on various properties of myObject.
Problem: when I modify a particular myObject elsewhere in the app (change one of its properties), and save the associated Core Data ManagedObjectContext, the List row representing that item is not updated/refreshed in the UI.
Possibly a cause for this is that I am updating my Core Data object by setting a property, that in turn sets another property. Maybe the associated signaling doesn’t reach the right place, and I should emit more notifications here.
Code in MyObject. ObjectType is an enum, typeValue is int32 backing this, that actually gets stored in CD database.
var type: ObjectType {
get {
return ObjectType(rawValue: typeValue)!
}
set {
self.typeValue = newValue.rawValue
}
}
How do I cause a list row to update when the backing Core Data object is modified and saved elsewhere in the app?
I finally figured this out on my own. The fix was not in the list, but further down the stack, in RowView.
RowView code was such:
struct RowView: View {
var myObject: MyObject
// Other code to render body etc
}
When doing this, the RowView works as expected, but it treats myObject as immutable. Any changes to myObject don’t cause a view redraw.
The one-keyword fix is to add #ObservedObject to the declaration:
struct RowView: View {
#ObservedObject var myObject: MyObject
}
It now works as expected, and any updates to MyObject cause a redraw.

SwiftUI error with ForEach within List: Cannot invoke initializer for type 'List' with an argument list of type '(#escaping () ... (see code)

I am trying to have a simple ForEach within a List (and have done this in another Project), but in this project I am getting the below mentioned error? How could I fix?
Error
Cannot invoke initializer for type 'List' with an argument list of type '(#escaping () -> ForEach, Int64, Text>)'
Code
import SwiftUI
struct ContentView: View {
#FetchRequest(fetchRequest: GCList.allListFetchRequest()) var gcLists: FetchedResults<GCList>
var body : some View {
NavigationView {
List { // <=== ERROR IS MARKED HERE IN XCODE
ForEach(self.gcLists) { gcList in
Text(gcList.title)
}
}
}
}
}
and
extension GCList : Identifiable {
#nonobjc public class func fetchRequest() -> NSFetchRequest<GCList> {
return NSFetchRequest<GCList>(entityName: "GCList")
}
#NSManaged public var id: Int64
#NSManaged public var title: String
}
extension GCList {
static func allListFetchRequest() -> NSFetchRequest<GCList> {
let request: NSFetchRequest<GCList> = GCList.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
return request
}
}
Turns out in Core Data I had an old entity that I had created called "List" that managed to much things up....

How can I clone an Object (deep copy) in Dart?

Is there a Language supported way to make a full (deep) copy of an Object in Dart?
If multiple options exist, what are their differences?
Darts built-in collections use a named constructor called "from" to accomplish this. See this post: Clone a List, Map or Set in Dart
Map mapA = {
'foo': 'bar'
};
Map mapB = new Map.from(mapA);
No as far as open issues seems to suggest:
https://github.com/dart-lang/sdk/issues/3367
And specifically:
... Objects have identity, and you can only pass around references to them. There is no implicit copying.
Late to the party, but I recently faced this problem and had to do something along the lines of :-
class RandomObject {
RandomObject(this.x, this.y);
RandomObject.clone(RandomObject randomObject): this(randomObject.x, randomObject.y);
int x;
int y;
}
Then, you can just call copy with the original, like so:
final RandomObject original = RandomObject(1, 2);
final RandomObject copy = RandomObject.clone(original);
I guess for not-too-complex objects, you could use the convert library:
import 'dart:convert';
and then use the JSON encode/decode functionality
Map clonedObject = JSON.decode(JSON.encode(object));
If you're using a custom class as a value in the object to clone, the class either needs to implement a toJson() method or you have to provide a toEncodable function for the JSON.encode method and a reviver method for the decode call.
Unfortunately no language support. What I did is to create an abstract class called Copyable which I can implement in the classes I want to be able to copy:
abstract class Copyable<T> {
T copy();
T copyWith();
}
I can then use this as follows, e.g. for a Location object:
class Location implements Copyable<Location> {
Location({
required this.longitude,
required this.latitude,
required this.timestamp,
});
final double longitude;
final double latitude;
final DateTime timestamp;
#override
Location copy() => Location(
longitude: longitude,
latitude: latitude,
timestamp: timestamp,
);
#override
Location copyWith({
double? longitude,
double? latitude,
DateTime? timestamp,
}) =>
Location(
longitude: longitude ?? this.longitude,
latitude: latitude ?? this.latitude,
timestamp: timestamp ?? this.timestamp,
);
}
To copy an object without reference, the solution I found was similar to the one posted here, however if the object contains MAP or LIST you have to do it this way:
class Item {
int id;
String nome;
String email;
bool logado;
Map mapa;
List lista;
Item({this.id, this.nome, this.email, this.logado, this.mapa, this.lista});
Item copyWith({ int id, String nome, String email, bool logado, Map mapa, List lista }) {
return Item(
id: id ?? this.id,
nome: nome ?? this.nome,
email: email ?? this.email,
logado: logado ?? this.logado,
mapa: mapa ?? Map.from(this.mapa ?? {}),
lista: lista ?? List.from(this.lista ?? []),
);
}
}
Item item1 = Item(
id: 1,
nome: 'João Silva',
email: 'joaosilva#gmail.com',
logado: true,
mapa: {
'chave1': 'valor1',
'chave2': 'valor2',
},
lista: ['1', '2'],
);
// -----------------
// copy and change data
Item item2 = item1.copyWith(
id: 2,
nome: 'Pedro de Nobrega',
lista: ['4', '5', '6', '7', '8']
);
// -----------------
// copy and not change data
Item item3 = item1.copyWith();
// -----------------
// copy and change a specific key of Map or List
Item item4 = item1.copyWith();
item4.mapa['chave2'] = 'valor2New';
See an example on dartpad
https://dartpad.dev/f114ef18700a41a3aa04a4837c13c70e
With reference to #Phill Wiggins's answer, here is an example with .from constructor and named parameters:
class SomeObject{
String parameter1;
String parameter2;
// Normal Constructor
SomeObject({
this.parameter1,
this.parameter2,
});
// .from Constructor for copying
factory SomeObject.from(SomeObject objectA){
return SomeObject(
parameter1: objectA.parameter1,
parameter2: objectA.parameter2,
);
}
}
Then, do this where you want to copy:
SomeObject a = SomeObject(parameter1: "param1", parameter2: "param2");
SomeObject copyOfA = SomeObject.from(a);
Let's say you a have class
Class DailyInfo
{
String xxx;
}
Make a new clone of the class object dailyInfo by
DailyInfo newDailyInfo = new DailyInfo.fromJson(dailyInfo.toJson());
For this to work your class must have implemented
factory DailyInfo.fromJson(Map<String, dynamic> json) => _$DailyInfoFromJson(json);
Map<String, dynamic> toJson() => _$DailyInfoToJson(this);
which can be done by making class serializable using
#JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false)
Class DailyInfo{
String xxx;
}
It only works for object types that can be represented by JSON.
ClassName newObj = ClassName.fromMap(obj.toMap());
or
ClassName newObj = ClassName.fromJson(obj.toJson());
Trying using a Copyable interface provided by Dart.
there is an easier way for this issue
just use ... operator
for example, clone a Map
Map p = {'name' : 'parsa','age' : 27};
Map n = {...p};
also, you can do this for class properties.
in my case, I was needed to clone a listed property of a class.
So:
class P1 {
List<String> names = [some data];
}
/// codes
P1 p = P1();
List<String> clonedList = [...p.names]
// now clonedList is an unreferenced type
There is no built-in way of deep cloning an object - you have to provide the method for it yourself.
I often have a need to encode/decode my classes from JSON, so I usually provide MyClass fromMap(Map) and Map<String, dynamic> toJson() methods. These can be used to create a deep clone by first encoding the object to JSON and then decoding it back.
However, for performance reasons, I usually implement a separate clone method instead. It's a few minutes work, but I find that it is often time well spent.
In the example below, cloneSlow uses the JSON-technique, and cloneFast uses the explicitly implemented clone method. The printouts prove that the clone is really a deep clone, and not just a copy of the reference to a.
import 'dart:convert';
class A{
String a;
A(this.a);
factory A.fromMap(Map map){
return A(
map['a']
);
}
Map<String, dynamic> toJson(){
return {
'a': a
};
}
A cloneSlow(){
return A.fromMap(jsonDecode(jsonEncode(this)));
}
A cloneFast(){
return A(
a
);
}
#override
String toString() => 'A(a: $a)';
}
void main() {
A a = A('a');
A b = a.cloneFast();
b.a = 'b';
print('a: $a b: $b');
}
There's no API for cloning/deep-copying built into Dart.
We have to write clone() methods ourselves & (for better or worse) the Dart authors want it that way.
Deep copy Object /w List
If the Object we're cloning has a List of Objects as a field, we need to List.generate that field and those Objects need their own clone method.
Example of cloning method (copyWith()) on an Order class with a List field of objects (and those nested objects also have a copyWith()):
Order copyWith({
int? id,
Customer? customer,
List<OrderItem>? items,
}) {
return Order(
id: id ?? this.id,
customer: customer ?? this.customer,
//items: items ?? this.items, // this will NOT work, it references
items: items ?? List.generate(this.items.length, (i) => this.items[i].copyWith()),
);
}
Gunter mentions this here.
Note, we cannot use List.from(items) nor [...items]. These both only make shallow copies.
Dart does not share Memory within multiple threads (isolate), so...
extension Clone<T> on T {
/// in Flutter
Future<T> clone() => compute<T, T>((e) => e, this);
/// in Dart
Future<T> clone() async {
final receive = ReceivePort();
receive.sendPort.send(this);
return receive.first.then((e) => e as T).whenComplete(receive.close);
}
}
An example of Deep copy in dart.
void main() {
Person person1 = Person(
id: 1001,
firstName: 'John',
lastName: 'Doe',
email: 'john.doe#email.com',
alive: true);
Person person2 = Person(
id: person1.id,
firstName: person1.firstName,
lastName: person1.lastName,
email: person1.email,
alive: person1.alive);
print('Object: person1');
print('id : ${person1.id}');
print('fName : ${person1.firstName}');
print('lName : ${person1.lastName}');
print('email : ${person1.email}');
print('alive : ${person1.alive}');
print('=hashCode=: ${person1.hashCode}');
print('Object: person2');
print('id : ${person2.id}');
print('fName : ${person2.firstName}');
print('lName : ${person2.lastName}');
print('email : ${person2.email}');
print('alive : ${person2.alive}');
print('=hashCode=: ${person2.hashCode}');
}
class Person {
int id;
String firstName;
String lastName;
String email;
bool alive;
Person({this.id, this.firstName, this.lastName, this.email, this.alive});
}
And the output below.
id : 1001
fName : John
lName : Doe
email : john.doe#email.com
alive : true
=hashCode=: 515186678
Object: person2
id : 1001
fName : John
lName : Doe
email : john.doe#email.com
alive : true
=hashCode=: 686393765
// Hope this work
void main() {
List newList = [{"top": 179.399, "left": 384.5, "bottom": 362.6, "right": 1534.5}, {"top": 384.4, "left": 656.5, "bottom": 574.6, "right": 1264.5}];
List tempList = cloneMyList(newList);
tempList[0]["top"] = 100;
newList[1]["left"] = 300;
print(newList);
print(tempList);
}
List cloneMyList(List originalList) {
List clonedList = new List();
for(Map data in originalList) {
clonedList.add(Map.from(data));
}
return clonedList;
}
This works for me.
Use the fromJson and toJson from your Object's Class on JSON serializing
var copy = ObjectClass.fromJson(OrigObject.toJson());
make a helper class:
class DeepCopy {
static clone(obj) {
var tempObj = {};
for (var key in obj.keys) {
tempObj[key] = obj[key];
}
return tempObj;
}
}
and copy what you want:
List cloneList = [];
if (existList.length > 0) {
for (var element in existList) {
cloneList.add(DeepCopy.clone(element));
}
}
Let's say, you want to deep copy an object Person which has an attribute that is a list of other objects Skills. By convention, we use the copyWith method with optional parameters for deep copy, but you can name it anything you want.
You can do something like this
class Skills {
final String name;
Skills({required this.name});
Skills copyWith({
String? name,
}) {
return Skills(
name: name ?? this.name,
);
}
}
class Person {
final List<Skills> skills;
const Person({required this.skills});
Person copyWith({
List<Skills>? skills,
}) =>
Person(skills: skills ?? this.skills.map((e) => e.copyWith()).toList());
}
Keep in mind that using only this.skills will only copy the reference of the list. So original object and the copied object will point to the same list of skills.
Person copyWith({
List<Skills>? skills,
}) =>
Person(skills: skills ?? this.skills);
If your list is primitive type you can do it like this. Primitive types are automatically copied so you can use this shorter syntax.
class Person {
final List<int> names;
const Person({required this.names});
Person copyWith({
List<int>? names,
}) =>
Person(names: names ?? []...addAll(names));
}
The accepted answer doesn't provide an answer, and the highest-rated answer 'doesn't work' for more complex Map types.
It also doesn't make a deep copy, it makes a shallow copy which seems to be how most people land on this page. My solution also makes a shallow copy.
JSON-cloning, which a few people suggest, just seems like gross overhead for a shallow-clone.
I had this basically
List <Map<String, dynamic>> source = [{'sampledata', []}];
List <Map<String, dynamic>> destination = [];
This worked, but of course, it's not a clone, it's just a reference, but it proved in my real code that the data types of source and destination were compatible (identical in my case, and this case).
destination[0] = source[0];
This did not work
destination[0] = Map.from(source[0]);
This is the easy solution
destionation[0] = Map<String, dynamic>.from(source[0]);

Resources