Fetch request required after decoding into a NSManagedObject object - ios

I have the following generic function that works: it correctly creates the objects and I know is saved into core data because if do a fetch request right after, I get the object I just created. However, the object itself isn't a valid core data object (x-core data fault). Is there any way around so I don't have to do a fetch request right after a decoding an object? Many thanks.
func decode<T: Decodable>(data: Data?, objectType: T.Type, save: Bool = true, completionHandler: #escaping (T) -> ())
{
guard let d = data else { return }
do
{
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = SingletonDelegate.shared.context
let root = try JSONDecoder(context: privateContext).decode(objectType, from: d)
if save
{
try privateContext.save()
privateContext.parent?.performAndWait
{
do
{
if let p = privateContext.parent
{
try p.save()
}
}catch
{
print(error)
}
}
}
DispatchQueue.main.async
{
completionHandler(root)
}
}catch
{
print(error)
}
}
extension CodingUserInfoKey
{
static let context = CodingUserInfoKey(rawValue: "context")!
}
extension JSONDecoder
{
convenience init(context: NSManagedObjectContext)
{
self.init()
self.userInfo[.context] = context
}
}

A core data fault is a valid Core Data object; it just hasn't been retrieved from the backing store into memory yet.
To reduce memory use, Core Data only fetches the full object when you access one of its properties. This fetch is automatic and effectively transparent to your code.
This means you don't need to do anything special; you can just use the managed object.

Related

extra entities with nil attributes in Core Data

Here is my code. I have extra entities with nil attributes in Core Data. when I delete and run application firstly, I get one saved object with nil attributes fetched from core data.
class RepositoryEntity {
private var context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func fetchRepositories() -> [RepositoryEntity] {
do {
return try context.fetch(RepositoryEntity.fetchRequest())
} catch(let error) {
print("errr: ", error.localizedDescription)
}
return []
}
func saveObject(repo: Repository, onSuccess: () -> Void, onFailure: (_ error: String) -> Void) {
let repoEntity = RepositoryEntity(context: self.context)
repoEntity.fullName = repo.fullName
repoEntity.dateCreated = repo.dateCreated
repoEntity.url = repo.url
repoEntity.language = repo.language
repoEntity.repoDescription = repo.repoDescription
repoEntity.id = repo.id
let ownerEntity = OwnerEntity(context: self.context)
ownerEntity.ownerName = repo.owner.ownerName
ownerEntity.avatarUrl = repo.owner.avatarUrl
repoEntity.addToOwner(ownerEntity)
// Save the data
do {
try context.save()
onSuccess()
} catch(let error) {
onFailure("Something Happend. Try again later.")
print(error.localizedDescription)
}
}
func deleteRepository(repo: Repository, onSuccess: () -> Void, onFailure: (_ error: String) -> Void) {
let repositories = fetchRepositories()
guard let deletableRepo = repositories.first(where: {$0.id == repo.id}) else { return }
self.context.delete(deletableRepo)
do {
try context.save()
onSuccess()
} catch(let error) {
onFailure("Something Happens. try again later.")
print(error.localizedDescription)
}
}
}
when I delete and run application firstly, I get one saved object with nil attributes fetched from core data.
When you write "I get one saved object...": what object? RepositoryEntity? How do you know you have a saved object, by calling fetchRepositories()? I can only assume it's like that (as opposed to having an empty OwnerEntity).
In that case, the problem is that, to call func fetchRepositories() you need to create an instance. So, when you start with zero objects, as soon as you call fetchRepositories() you already have at least one.
Change:
func fetchRepositories() -> [RepositoryEntity]
with:
static func fetchRepositories() -> [RepositoryEntity]
and call it from the type:
RepositoryEntity.fetchRepositories()
The same also for deleteRepository.

Core Data entity name is nil while running a second unit test

I am trying to add some unit tests for my Core Data code. But I always have this issue, the first test always run correctly, but the second one crashes because entity name is nil.
I also get this error:
Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Gym.Exercise' so +entity is unable to disambiguate.
Failed to find a unique match for an NSEntityDescription to a managed object subclass
So my guess is that I am not doing something right in tearDown().
override func setUp() {
super.setUp()
coreDataStack = CoreDataStack(storeType: .inMemory)
context = coreDataStack.context
}
override func tearDown() {
coreDataStack.reset()
context = nil
super.tearDown()
}
Here is my CoreDataStack class:
final class CoreDataStack {
var storeType: StoreType!
public init(storeType: StoreType) {
self.storeType = storeType
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Gym")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unresolved error \(error), \(error.localizedDescription)")
} else {
description.type = self.storeType.type
}
}
return container
}()
public var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
public func reset() {
guard let store = persistentContainer.persistentStoreCoordinator.persistentStores.first else { fatalError("No store found")}
guard let url = store.url else { fatalError("No store URL found")}
try! FileManager.default.removeItem(at: url)
NSPersistentStoreCoordinator.destroyStoreAtURL(url: url)
}
}
And the definition to destroyStoreAtURL:
extension NSPersistentStoreCoordinator {
public static func destroyStoreAtURL(url: URL) {
do {
let psc = self.init(managedObjectModel: NSManagedObjectModel())
try psc.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil)
} catch let e {
print("failed to destroy persistent store at \(url)", e)
}
}
}
I was using this code in the past for unit testing and it works, the difference is that in the past when I setup the NSManagedObject classes in editor I used the following config:
Module - Global namespace
Codegen - Class Definition
Now I use:
Module - Current Product Module
Codegen - Manual/None
Because I want to add my classes manually.
So does anyone know why the behavior is different now ?
edit - my NSManagedObject extension helper (the error occurs inside the first line of fetch() method when trying to retrieve the entity name):
extension Managed where Self: NSManagedObject {
public static var entityName: String {
return entity().name!
}
public static func fetch(in context: NSManagedObjectContext, configurationBlock: (NSFetchRequest<Self>) -> () = { _ in }) -> [Self] {
let request = NSFetchRequest<Self>(entityName: Self.entityName)
configurationBlock(request)
return try! context.fetch(request)
}
public static func count(in context: NSManagedObjectContext, configure: (NSFetchRequest<Self>) -> () = { _ in }) -> Int {
let request = NSFetchRequest<Self>(entityName: entityName)
configure(request)
return try! context.count(for: request)
}
public static func findOrFetch(in context: NSManagedObjectContext, matching predicate: NSPredicate) -> Self? {
guard let object = materializeObject(in: context, matching: predicate) else {
return fetch(in: context) { request in
request.predicate = predicate
request.returnsObjectsAsFaults = false
request.fetchLimit = 1
}.first
}
return object
}
public static func materializeObject(in context: NSManagedObjectContext, matching predicate: NSPredicate) -> Self? {
for object in context.registeredObjects where !object.isFault {
guard let result = object as? Self, predicate.evaluate(with: result) else {
continue
}
return result
}
return nil
}
public static func findOrCreate(in context: NSManagedObjectContext, matching predicate: NSPredicate, configure: (Self) -> ()) -> Self {
guard let object = findOrFetch(in: context, matching: predicate) else {
let newObject: Self = context.insertObject()
configure(newObject)
return newObject
}
return object
}
}
You should try deleting all the found instances of persistentStore instead of deleting the first.
try replacing the reset function with below:
public func reset() {
let stores = persistentContainer.persistentStoreCoordinator.persistentStores
guard !stores.isEmpty else {
fatalError("No store found")
}
stores.forEach { store in
guard let url = store.url else { fatalError("No store URL found")}
try! FileManager.default.removeItem(at: url)
NSPersistentStoreCoordinator.destroyStoreAtURL(url: url)
}
}
And see if you still have the issue.

Globally save and access received data

I'm trying to get some data from the server and use it globally in the app..
I mean for example, I'm using following code to get data from service:
struct Service : Decodable{
let id: Int
let name, description: String
let createdAt: String?
let updatedAt: String?
}
func makeGetCall() {
let todoEndpoint: String = "http://web.src01.view.beta.is.sa/public/api/services"
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET on /public/api/services")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let todos = try decoder.decode([Service].self, from: responseData)
for todo in todos{
print(todo.name)
}
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
This code is located and called in HomeViewController and i'm getting data which i want.
But i want to access and use this data in another viewcontroller and in whole app...
How i can do it? How can i make the received data from the function is saved globally and how to use it in another viewcontroller?
Can someone tell me how i can do this?
For such cases we usually use static data. They may be served as singleton or just a static property. In your case a static property for cached data may be nice. We can put static properties in extension so adding following may be nice:
// MARK: - Fetching Data
extension Service {
private static var cachedServices: [Service]?
static func fetchServices(_ completion: (_ services: [Service]) -> Void?) {
if let cachedServices = cachedServices {
completion(cachedServices)
} else {
makeGetCall { services in
let newServices = services ?? []
self.cachedServices = newServices
completion(newServices)
}
}
}
}
Now the usage from everywhere is calling
Service.fetchServices { services in
}
and this call may be asynchronous or not, depending if data is already loaded.
If you need to access them synchronous and you are sure data is already loaded then simply add another method in extension:
static func getCachedData() -> [Service] {
return cachedServices ?? []
}
This method will return instantly but array will be empty if no data was received yet. But anywhere you can call Service.getCachedData()
This cache is now only preserved until your app terminates. If you want to preserve them longer then all you need to do is add the logic to save and load data into file or user defaults. The logic for that would be something like:
private static var cachedServices: [Service]? {
didSet {
self.saveServicesToFile(cachedServices)
}
}
static func fetchServices(_ completion: (_ services: [Service]) -> Void?)
{
if let cachedServices = cachedServices {
completion(cachedServices)
} else if let saved = self.loadFromFile() {
self.cachedServices = saved
completion(saved)
}else {
makeGetCall { services in
let newServices = services ?? []
self.cachedServices = newServices
completion(newServices)
}
}
}

Saving in background causes response time to be delayed (iOS)

I have a table, which uses a NSFetchedResultsController to populate it's data. When I refresh my table, I need to update all 50+ items, so I do the following: I make a call to the server which returns JSON data, store the "media" object into an array, loop through this array and individually store each object to core data (in background thread), then reload the table. This works fine. However there is a major issue.
Sometimes the step of saving to the database takes 7+ seconds, due to looping through large arrays and individually storing each object to core data. And while this step is executing, when I fetch other data from the server, the response time is delayed tremendously. I wont be able to fetch new data until the save process is complete. I'm quite confused because this is supposed to be done in the background thread and not block other server calls.
Why does saving data to core data in bg causing my response time to be delayed? Is there a better approach to storing large arrays to core data without disrupting any responses?
//Refreshing User Table method
class func refreshUserProfileTable(callback: (error: NSError?) -> Void) {
//getProfile fetches data from server
ProfileWSFacade.getProfile(RequestManager.userID()!) {
(profile, isLastPage, error) -> () in
DataBaseManager.sharedInstance.saveInBackground({ (backgroundContext) in
let mediaList = profile?["media"] as? Array<JSONDictionary>
if let mediaList = mediaList {
//Response time is delayed when this loop is executing
for media in mediaList {
DataBaseManager.sharedInstance.storeObjectOfClass(Media.self, dict: media, context: backgroundContext)
}
}
}, completion: {
callback(error: error)
})
}
}
//MARK: Core data methods:
//Save in background method in Database manager
func saveInBackground(
block: (backgroundContext: NSManagedObjectContext) -> Void,
completion: (Void->Void)? = nil)
{
let mainThreadCompletion = {
if let completion = completion {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
completion()
})
}
}
backgroundContext.performBlock { () -> Void in
guard RequestManager.userID() != nil else {
mainThreadCompletion()
return
}
block(backgroundContext: self.backgroundContext)
if RequestManager.userID() != nil {
_ = try? self.backgroundContext.save()
DataBaseManager.sharedInstance.save()
}
mainThreadCompletion()
}
}
//Stores class object
func storeObjectOfClass<T: NSManagedObject where T: Mappable>(
entityClass:T.Type,
dict: JSONDictionary,
context: NSManagedObjectContext? = nil) -> T
{
let context = context ?? mainManagedObjectContext
let predicate = NSPredicate(format: "%K LIKE %#", entityClass.primaryKey(), entityClass.primaryKeyFromDict(dict))
let requestedObject = DataBaseManager.createOrUpdateFirstEntity(
entityType: T.self,
predicate: predicate,
context: context) { (entity) -> () in
entity.populateFromDictionary(dict)
}
return requestedObject
}
//Creates or updates core data entity
class func createOrUpdateFirstEntity<T: NSManagedObject>(
entityType entityType: T.Type,
predicate: NSPredicate,
context: NSManagedObjectContext,
entityUpdateBlock:(entity: T) -> ()) -> T
{
guard DataBaseManager.sharedInstance.doPersistentStoreAvailible() else { return T() }
let desc = NSEntityDescription.entityForName(String(entityType), inManagedObjectContext: context)!
let existingEntityRequest = NSFetchRequest()
existingEntityRequest.entity = desc
existingEntityRequest.predicate = predicate
let requestedObject = try? context.executeFetchRequest(existingEntityRequest).first
if let requestedObject = requestedObject as? T {
entityUpdateBlock(entity: requestedObject)
return requestedObject
} else {
let newObject = T(entity: desc, insertIntoManagedObjectContext: context)
entityUpdateBlock(entity: newObject)
return newObject
}
}
I found out that .performBlock follows the FIFO rule, first in, first out. Meaning the blocks will be executed in the order in which they were put into the internal queue: SO Link. Because of that, the next rest call would wait until the first block has completed before it saved, and did its callback. The actual response time wasnt slow, it was just the saving time because of FIFO.
The solution was to use a different NSManagedContext for profile loading, rather than using the one that was being used for all background calls.
let profileContext: NSManagedObjectContext
//Instead of calling saveInBackground, we save to saveInProfileContext, which wont block other rest calls.
func saveInProfileContext(
block: (profileContext: NSManagedObjectContext) -> Void,
completion: (Void->Void)? = nil)
{
let mainThreadCompletion = {
if let completion = completion {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
completion()
})
}
}
profileContext.performBlock { () -> Void in
guard RequestManager.userID() != nil else {
mainThreadCompletion()
return
}
block(profileContext: self.profileContext)
if RequestManager.userID() != nil {
_ = try? self.profileContext.save()
DataBaseManager.sharedInstance.save()
}
mainThreadCompletion()
}
}

CoreData Concurrency issue

I am having issue while using private managedObjectContextfor saving data in background. I am new to CoreData. I am using Parent-Child approach for NSManagedObjectContext but facing several issues.
Errors arise when I tap reload button multiple times
Errors:
'NSGenericException', reason: Collection <__NSCFSet: 0x16e47100> was mutated while being enumerated
Some times : crash here try managedObjectContext.save()
Sometimes Key value coding Compliant error
My ViewController class
class ViewController: UIViewController {
var jsonObj:NSDictionary?
var values = [AnyObject]()
#IBOutlet weak var tableView:UITableView!
override func viewDidLoad() {
super.viewDidLoad()
getData()
saveInBD()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.saved(_:)), name: "kContextSavedNotification", object: nil)
}
//Loding json data from a json file
func getData(){
if let path = NSBundle.mainBundle().pathForResource("countries", ofType: "json") {
do {
let data = try NSData(contentsOfURL: NSURL(fileURLWithPath: path), options: NSDataReadingOptions.DataReadingMappedIfSafe)
do {
jsonObj = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
} catch {
jsonObj = nil;
}
} catch let error as NSError {
print(error.localizedDescription)
}
} else {
print("Invalid filename/path.")
}
}
**Notification reciever**
func saved(not:NSNotification){
dispatch_async(dispatch_get_main_queue()) {
if let data = DatabaseManager.sharedInstance.getAllNews(){
self.values = data
print(data.count)
self.tableView.reloadData()
}
}
}
func saveInBD(){
if jsonObj != nil {
guard let nameArray = jsonObj?["data#"] as? NSArray else{return}
DatabaseManager.sharedInstance.addNewsInBackGround(nameArray)
}
}
//UIButton for re-saving data again
#IBAction func reloadAxn(sender: UIButton) {
saveInBD()
}
}
**Database Manager Class**
public class DatabaseManager{
static let sharedInstance = DatabaseManager()
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
private init() {
}
func addNewsInBackGround(arr:NSArray) {
let jsonArray = arr
let moc = managedObjectContext
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc
privateMOC.performBlock {
for jsonObject in jsonArray {
let entity = NSEntityDescription.entityForName("Country",
inManagedObjectContext:privateMOC)
let managedObject = NSManagedObject(entity: entity!,
insertIntoManagedObjectContext: privateMOC) as! Country
managedObject.name = jsonObject.objectForKey("name")as? String
}
do {
try privateMOC.save()
self.saveMainContext()
NSNotificationCenter.defaultCenter().postNotificationName("kContextSavedNotification", object: nil)
} catch {
fatalError("Failure to save context: \(error)")
}
}
}
func getAllNews()->([AnyObject]?){
let fetchRequest = NSFetchRequest(entityName: "Country")
fetchRequest.resultType = NSFetchRequestResultType.DictionaryResultType
do {
let results =
try managedObjectContext.executeFetchRequest(fetchRequest)
results as? [NSDictionary]
if results.count > 0
{
return results
}else
{
return nil
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
return nil
}
}
func saveMainContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
print("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
You can write in background and read in the main thread (using different MOCs like you do). And actually you're almost doing it right.
The app crashes on the try managedObjectContext.save() line, because saveMainContext is called from within the private MOC's performBlock. The easiest way to fix it is to wrap the save operation into another performBlock:
func saveMainContext () {
managedObjectContext.performBlock {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
print("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
Other two errors are a little more tricky. Please, provide more info. What object is not key-value compliant for what key? It's most likely a JSON parsing issue.
The first error ("mutated while being enumerated") is actually a nasty one. The description is pretty straight forward: a collection was mutated by one thread while it was enumerated on the other. Where does it occur?
One possible reason (most likely one, I would say) is that it is indeed a Core Data multithreading issue. Despite the fact that you can use several threads, you can only use core data objects within the thread they were obtained on. If you pass them to another thread, you'll likely run into an error like this.
Look through your code and try to find a place where such situation might occur (for instance, do you access self.values from other classes?). Unfortunately, I wasn't able to find such place in several minutes. If you said upon which collection enumeration this error occurs, it would help).
UPDATE:
P.S. I just thought that the error might be related to the saveMainContext function. It is performed right before a call to saved. saveMainContext is performed on the background thread (in the original code, I mean), and saved is performed on the main thread. So after fixing saveMainContext, the error might go away (I'm not 100% sure, though).
You are violating thread confinement.
You cannot write to CoreData in Background, and read in MainThread.
All operation on CoreData must be done in the same thread

Resources