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.
Related
I have these classes:
import Foundation
import CoreData
public class Friend: NSManagedObject {
}
and
import Foundation
import CoreData
extension Friend {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Friend> {
return NSFetchRequest<Friend>(entityName: "Friend")
}
#NSManaged public var profileImageName: String?
#NSManaged public var name: String?
#NSManaged public var messages: NSSet?
}
// MARK: Generated accessors for messages
extension Friend {
#objc(addMessagesObject:)
#NSManaged public func addToMessages(_ value: Message)
#objc(removeMessagesObject:)
#NSManaged public func removeFromMessages(_ value: Message)
#objc(addMessages:)
#NSManaged public func addToMessages(_ values: NSSet)
#objc(removeMessages:)
#NSManaged public func removeFromMessages(_ values: NSSet)
}
and
import UIKit
import CoreData
extension FriendsController {
func setupData(){
let context = AppDelegate().context
let mark = Friend(context: context)
mark.name = "mark"
mark.profileImageName = "zuck"
let message1 = Message(context: context)
message1.text = "Hello, my name is mark, nice to meet you"
message1.date = Date()
message1.friend = mark
let steve = Friend(context: context)
steve.name = "steve"
steve.profileImageName = "steve"
let message2 = Message(context: context)
message2.text = "Hello, my name is steve"
message2.date = Date()
message2.friend = steve
messages = [message1, message2]
}
}
and later, I'm trying to access 'message?.friend?.name', but it's always nil...
class MessageCell: BaseCell {
var message: Message? {
didSet {
nameLabel.text = message?.friend?.name
if let profileImageName = message?.friend?.profileImageName {
profileImageView.image = UIImage(named: profileImageName)
}
messageLabel.text = message?.text
if let date = message?.date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "h:m a"
timeLabel.text = dateFormatter.string(from: date)
}
}
}
}
Should I do something else with the context to load these managed objects?
Doesn't make much sense, since I've just create them, just passing as reference to a different class, they should be available.
I think the problem is here:
let context = AppDelegate().context
This creates a new instance of the AppDelegate, it doesn't reference the existing instance. Since that new instance is created in the setupData method, it is deallocated when that method completes. And because you haven't saved, the data isn't persisted before the method completes. So the messages array contains NSManagedObjects which no longer have a reference to a valid context, and their values are therefore nil.
You should probably access the existing AppDelegate instance using:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.context
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.
Is this good or bad code for MVVM pattern with Core Data. For example i have Category+CoreDataClass.swift and CategoryDataProperties.swift; thats my
public class Account: NSManagedObject {
//what should i write here; any clue
}
extension Account {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Category> {
return NSFetchRequest< Category >(entityName: "Category")
}
#NSManaged public var categoryId: Int64
#NSManaged public var categoryTitle: String
}
Model = Category
class CategoryViewModel{
var category:Category() //
var categories: [Category]()
func AddCategory(category) {
//pass category to CoreData save method in CoreDataService.swift
}
}
and ViewModel is used in Single Screen with two textField named categoryId,categoryName(UIView)
class CategoryVC: UITableViewController {
var vm: CategoryViewModel!
}
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 .
I use Xcode 8 CoreData with auto generated Base classes.
When I try
let fetchRequest: NSFetchRequest<Event> = Event.fetchRequest()
fetchRequest variable correctly gets type of NSFetchRequest<Event>
When I try
let fetchRequest = Event.fetchRequest()
Xcode tells that fetchRequest has undefined type, as I understand Swift must determine type automatically by making assignment
Here is the auto generated class extension generated by Xcode
extension Event {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Event> {
return NSFetchRequest<Event>(entityName: "Event");
}
#NSManaged public var ffff: String?
#NSManaged public var timestamp: NSDate?
}
As an example this code works correctly (logic is the same)
struct MyStruct<T> {
let myVar: T
}
class MyClass {
}
extension MyClass {
class func test() -> MyStruct<Int> {
return MyStruct<Int>(myVar: 5)
}
}
let b = MyClass.test()
let b has a correct type of MyStruct<Int>
CoreDate automatically generated
#objc(Event)
public class Event: NSManagedObject {
}
extension Event {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Event> {
return NSFetchRequest<Event>(entityName: "Event");
}
#NSManaged public var timestamp: NSDate?
}
NSManagedObject protocol is in Objective-C and it has class method
+ (NSFetchRequest*)fetchRequest
When i try to use let fetchRequest = Event.fetchRequest() it thinks that i call Objective-C + (NSFetchRequest*)fetchRequest and it has no generics
Why doesn't it use an overridden #nonobjc public class func fetchRequest() -> NSFetchRequest<Event> from an Event class extension?