CoreData in SwiftUI: How to fetch a property distinctly? - ios

I'm spending hours to figure out how to fetch values uniquely from a property of a data model (CoreData).
For example, if I have 3 records that have "Apple", "Banana" and "Apple" respectively in their wrappedName property, the ListView shows "Apple", "Banana" and "Apple" (3 rows).
But I want to show only "Apple" and "Banana" (2 rows).
How can I achieve this?
//ListView.swift
import SwiftUI
import CoreData
struct ListView: View {
#FetchRequest(entity: Data.entity(), sortDescriptors: []) var data: FetchedResults<Data>
var body: some View {
NavigationView{
List{
ForEach(data, id: \.self) { d in
NavigationLink(destination: ChartView(title: d.wrappedName) {
Text(d.wrappedName)
}
}
}
}
}
}
//Data+CoreDataProperties.swift
import Foundation
import CoreData
extension Data {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Data> {
return NSFetchRequest<Data>(entityName: "Data")
}
#NSManaged public var date: Date?
#NSManaged public var id: UUID?
#NSManaged public var name: String?
public var wrappedName: String {
name ?? "Unknown"
}
}

A Custom Notification that sets a Published var would be better
public init() {
self.uniqueDict = [NSDictionary]()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(setUniqueDictValue), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: managedObjectContext)
//set the uniqueDict variable
setUniqueDictValue()
}
private func getEntityFetchRequest() -> NSFetchRequest<NSDictionary>
{
let fetchRequest = NSFetchRequest<NSDictionary>(entityName: "YourModel")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType
fetchRequest.propertiesToFetch = ["name"]
fetchRequest.returnsDistinctResults = true
return fetchRequest
}
///Single Fetch of objects willSet uniqueDict variable
func setUniqueDictValue() {
do {
try self.uniqueDict = managedObjectContext.fetch(getEntityFetchRequest())
} catch {
fatalError("Failed to fetch entity: \(error)")
}
}
Distinct Results Sample Project
https://www.youtube.com/watch?v=xl3KVmBJrSg

You need to change your Core Data model. The model should be designed how you want to display it in your UI. Don't design it like it is a database.
In your case an another Entity called Fruit, with an int fruitType that is a unique key, 1 for Apple and 2 for Banana. In your Data entity have a relation fruit but instead of fetching Data now fetch for Fruit. If you want to show something about Data in your list cell then simply add a property to the Fruit class to retrieve it from the related datas. And using KVO observing you can make it so the context detects a change to Fruit when something in its related Data changes.

Related

Problems creating a generic MVVM connector to CoreData

First of all, sorry about the post length but I am very new to iOS and SwiftUI development and I don't want to miss any details. I did some small projects with Kotlin on Android and Flutter, so I had some experience in app development.
Context
I trying to create a simple app that persists the user data on CoreData and I trying to follow MVVM architecture to develop the app. I was inspired by the following post on Medium. And I have the following files:
DataSource.swift: Class that abstracts the initialization of NSPersistentContainer.
Entity.swift: Protocol for CoreData entity class standardization.
ProductEntity.swift: Particular CoreData class definition that conforms Entity protocol.
Model.swift: Class with Entity generic that abstracts the model instantiation and updating process.
ProductModel.swift: Particular CoreData entity model definition that inherits Model<ProductEntity> (where exception raises).
The exception
I got an exception initializing the ProductsModel class (ProductsModel.swift, check it below) and I don't have any idea about where are the error source and its reason.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'
I hope you can give me some clues! :)
The code
DataSource.swift:
import Foundation
import CoreData
let defaultDatabase = "DB"
class DataSource {
static let shared = DataSource()
public let container: NSPersistentContainer
init(dbName: String = defaultDatabase) {
container = NSPersistentContainer(name: dbName)
container.loadPersistentStores { (_, err) in
if let error = err as NSError? {
print("NSError \(error) - \(error.userInfo)")
return
}
}
}
func save() {
do {
print("Saving context")
try self.container.viewContext.save()
print("Successfully saved context")
} catch {
print("ERROR: \(error as NSObject)")
}
}
}
Entity.swift:
import CoreData
protocol Entity: NSFetchRequestResult {
associatedtype CurrentEntity: NSManagedObject
static var name: String { get }
}
ProductEntity.swift:
import os
import CoreData
#objc(ProductEntity)
public class ProductEntity: NSManagedObject, Entity {
typealias CurrentEntity = ProductEntity
static let name: String = "Product"
}
extension ProductEntity : Identifiable {
public var ID: String {
self.objectID.uriRepresentation().absoluteString
}
}
extension ProductEntity {
#NSManaged public var desc: String?
#NSManaged public var name: String
#NSManaged public var price: Double
#NSManaged public var rations: Int16
#NSManaged public var shoppingList: NSSet?
}
Model.swift:
import Combine
import CoreData
import os
class Model<T: Entity>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var records = CurrentValueSubject<[T.CurrentEntity], Never>([])
private let controller: NSFetchedResultsController<T.CurrentEntity>
override init() {
controller = NSFetchedResultsController(
fetchRequest: NSFetchRequest<T.CurrentEntity>(entityName: T.name),
managedObjectContext: DataSource.shared.container.viewContext,
sectionNameKeyPath: nil, cacheName: nil
)
super.init()
controller.delegate = self
do {
try controller.performFetch()
records.value = (controller.fetchedObjects ?? []) as [T.CurrentEntity]
} catch {
NSLog("Error: could not fetch objects")
}
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let records = controller.fetchedObjects as? [T.CurrentEntity] else { return }
self.records.value = records
}
public func save() {
DataSource.shared.save()
}
}
ProductModel.swift:
import os
class ProductsModel: Model<ProductEntity> {
static let shared: ProductsModel = ProductsModel() // <-- This line raise the exception
}
NSFetchedResultsController is a tool which could manage your search results from Core Data. It needs at least one sort descriptor to maintain the list of your fetch request.
So you should improve the NSFetchRequest you defined like below to resolve the exception.
let req = NSFetchRequest<T.CurrentEntity>(entityName: T.name)
req.sortDescriptors = [NSSortDescriptor(key: "someKeyForSort", ascending: true)]
In addition, "someKeyForSort" is the name of a property of T.CurrentEntity. If ProductEntity is the type, "name" could be the key assuming you want NSFetchedResultsController to maintain the fetched results sorted by name in ascending order.

Convert firebase sub collection to custom struct - SwiftUI

I have a collection of teams in my Firebase Cloud Firestore each of which have players. I have previously saved each of these players in an array inside the team document however found this to be inefficient and slow the app dramatically. After some research I have converted this array into a sub collection called players and want to access it the same way.
Here is my team and player model (simplified):
struct DBTeam: Identifiable, Codable{
#DocumentID var id: String?
var name: String
var colour: Int
var dateCreated: Date
var players: [DBPlayer] = [DBPlayer]()
var userID: String?
}
struct DBPlayer: Identifiable, Equatable, Codable{
var id: String = UUID().uuidString
var name: String
static func ==(lhs: DBPlayer, rhs: DBPlayer) -> Bool {
return lhs.name == rhs.name && lhs.id == rhs.id
}
}
I then use this repository to get all the data and feed it into my view model:
class TeamRepository: ObservableObject{
var db = Firestore.firestore()
#Published var teams = [DBTeam]()
init(){
loadData()
}
func loadData(){
let userID = Auth.auth().currentUser?.uid
db.collection("teams")
.order(by: "name")
.whereField("userID", isEqualTo: userID ?? "")
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot{
self.teams = querySnapshot.documents.compactMap { document in
do{
let x = try document.data(as: DBTeam.self)
return x
}
catch{
print(error)
}
return nil
}
}
}
}
}
I have tried running this code again to see if by any chance it would work, but it didn't. By using these new sub collections, is this the most efficient way to store a bunch of players to one team and then reference them? If not any other suggestions? And how would I access these sub collections and store them into the array of players found in the DBTeam struct?

Combine: Listen to internal collection changes

I have a data manager that encapsulates a collection of objects. I want to listen to changes in that manager, as well as changes in collection objects. I came up with the solution using PassthroughSubject and sink, but I am pretty new to Combine and wondering is it correct and is there a better way to do that.
import Combine
class Item {
var data = false {
didSet {
self.subject.send()
}
}
let subject = PassthroughSubject<Void, Never>()
}
class DataManager {
private(set) var items = [Item]() {
didSet {
self.subject.send()
}
}
let subject = PassthroughSubject<Void, Never>()
func addItem(_ item: Item) {
self.items.append(item)
item.subject.sink { [weak self] in
self?.subject.send()
}
}
}
var item = Item()
var manager = DataManager()
manager.subject.sink {
print("Received Update")
}
manager.addItem(item) // Received Update
item.data = false // Received Update
item.data = true // Received Update
If you have control over the stored items, making them all structures should work. Arrays are structures, so will trigger the didSet when changed. Structures inside of arrays should change the value of the array and cause didSet to trigger for the array. Classes will not because the reference value of the class never changes. The current stance is that you should use structures over classes unless you have a good reason to use a class. Swift documentation for more info.
The other option is to do what you are already doing and make all of the classes conform to some protocol like BindableObject, then monitor didChange for each object.
Currently though you are not handling cancelation when an item is removed from the array. You should subscribe the didChange of DataManager to the didChange of every element. Then take the resultant AnyCancellable and add it to a dictionary keyed under the the item. Then once that item is removed from the array you should remove the associated AnyCancellable which will cancel the subscription.
For the newest version of SwiftUI, I will pass down the objectWillChange.send function to each item in the #Published array. Then, for each property of each item, I will call the update handler in the willSet property change handler.
Here's an example:
import Combine
final class User {
let prepareForUpdate: (() -> Void)?
var name: String {
willSet {
prepareForUpdate?()
}
}
init(prepareForUpdate: (() -> Void)? = nil, name: String) {
self.prepareForUpdate = prepareForUpdate
self.name = name
}
}
final class UserStore: ObservableObject {
#Published var users: [User]
init(_ users: [User] = []) {
self.users = users
}
func addUser(name: String) {
// Pass in our objectWillChange.send to the User object to listen for updates
let user = User(prepareForUpdate: objectWillChange.send, name: name)
users.append(user)
return user
}
}
Using this method, the view will be updated whenever a User in the users array is changed.

How to Store and Fetch Json Model data to Coredata in Swift

I have used coredata long back. But, I know basics of coredata for storing data and fetching.
But, Presently I am working with Swift language.
I have local json file and I am doing parsing that by decoder and displaying that data in tableview.
let path = Bundle.main.path(forResource: "file", ofType: "json")
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path ?? ""), options: .mappedIfSafe)
let decoder = JSONDecoder()
do {
quData = try decoder.decode(quData.self, from: data)
DispatchQueue.main.async {
self.myTableView.reloadData()
}
} catch {
print("Json decoder error")
}
} catch {
print(LocalizedError.self)
}
For that I have created model class based on the key values of json.
But, Now I have to store that data to Coredata and Fetch back, Need to show in same tableview.
But, I am getting confusion how many key values should I need to create.
My model class is :
class QuData: Codable {
let qu: Qu
init(qu: Qu) {
self.qu = qu
}
}
class Qu: Codable {
let music: Music
let dance: dance
init(music: Music, dance: dance) {
self.music = music
self.dance = dance
}
}
class Maths: Codable {
let q1, q2: Q1
init(q1: Q1, q2: Q1) {
self.q1 = q1
self.q2 = q2
}
}
class Q1: Codable {
let query: String
let options: [String]
let answer: String
let q1Optional: Bool
enum CodingKeys: String, CodingKey {
case query, options, answer
case q1Optional = "optional"
}
init(question: String, options: [String], answer: String, q1Optional: Bool) {
self.query = query
self.options = options
self.answer = answer
self.q1Optional = q1Optional
}
}
class Sport: Codable {
let q1: Q1
init(q1: Q1) {
self.q1 = q1
}
}
And my JSON data is
{
"qu": {
"music": {
"q1": {
“query”: “What is your name?”,
"options": [
“Sony”,
“Samsung”,
“Apple”,
“MI”
],
"answer": “Apple”,
"optional": true
}
},
“dance”: {
"q1": {
"question": "5 + 1 = ?",
"options": [
“8”,
“9”,
“6”,
“23”
],
"answer": “23”,
"optional": false
},
"q2": {
"question": "12 - 4 = ?",
"options": [
“5”,
“4”,
“9”,
“6”
],
"answer": "4",
"optional": false
}
}
}
}
How to store these data to Coredata and fetching, Showing in tableview..
And, The two categories (music,dance) in json data, I have to show "Music" data in 1st section and "Dance" data in
section tableview.
I am fully struck, how to create this kind json structure in Entity with attributes and fetching them using same model class (Which already created for local json file parsing).
Can anyone suggest me to move on further?
My suggestion is to use one entity.
In Core Data you can filter records very efficiently, so add a type attribute representing music, dance etc. You can even add a computed property to map the type attribute to an enum. The options attribute is declared as flat string. Use another computed property to map the flat string to an array and vice versa.
class Question : NSManagedObject {
#NSManaged var type: String
#NSManaged var question: String
#NSManaged var answer: String
#NSManaged var options: String
#NSManaged var optional: Bool
enum QuestionType : String {
case music, dance
}
var questionType : QuestionType {
get { return QuestionType(rawValue: type)! }
set { type = newValue.rawValue }
}
var questionOptions : [String] {
get { return options.components(separatedBy: ", ") }
set { options = newValue.joined(separator: ", ") }
}
Alternatively use one entity per type and relationships for the questions
class Music : NSManagedObject {
#NSManaged var questions: Set<Question>
...
}
class Question : NSManagedObject {
#NSManaged var question: String
#NSManaged var answer: String
#NSManaged var options: String
#NSManaged var optional: Bool
#NSManaged var type: Music
...
}

swift 3.1 How to sum async fetch properties

I cannot mace a count of a single property in an asynchronous fetch, I'd like to summ all expenseAmount for given async fetch but cannot apply for in pattern
my entity:
extension Expense {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Expense> {
return NSFetchRequest<Expense>(entityName: "Expense");
}
#NSManaged public var expenseAmount: Double
#NSManaged public var expenseDate: NSDate?
#NSManaged public var expenseOwner: String?
#NSManaged public var expenseTag: String?
}
in my view controller I call this func in my viewDidLoad, how could I select and summ the expenseAmount values for all fetched entities?
func makeAsyncFetch() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
managedContext = appDelegate.persistentContainer.viewContext
// MARK: - async fetch request 2
let expenseFetch = NSFetchRequest<Expense>(entityName: kExpenseEntityName)
asyncFetchRequest = NSAsynchronousFetchRequest<Expense>(fetchRequest: expenseFetch) {
[unowned self] (result: NSAsynchronousFetchResult) in
guard let Expenses = result.finalResult else {
return
}
self.asyncExpensesArray = Expenses
self.expenseTableView.reloadData()
// self.testPrintForMyArray(arrayToCheck: self.asyncExpensesArray)
// self.batchUpdate()
}
// MARK: - async fetch request 3
do {
try managedContext.execute(asyncFetchRequest)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
Map the Expenses array to the expenseAmount values and sum them up.
let sum = Expenses.map({$0.expenseAmount}).reduce(0, {$0 + $1})
PS: According to the Swift 3 naming philosophy I recommend to name the Expense properties just amount, date, owner and tag and remove the redundant parts since it's clear that the properties belong to Expense .

Resources