0

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
    }
}
pedrouan
  • 12,762
  • 3
  • 58
  • 74
Josh O'Connor
  • 4,694
  • 7
  • 54
  • 98

1 Answers1

2

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()
    }
}
Community
  • 1
  • 1
Josh O'Connor
  • 4,694
  • 7
  • 54
  • 98