0
votes

I am having strange race-condition-like issues when fetching/creating my User core data objects. I have two methods, handleGames() and handelUsers(), that are called after making their respective network requests.

A User contains ordinary information like username and id. A game can have many User participants. We created this one-to-many relationship in our core data models. For each core data model, we created extensions to handle fetching and creating the respective core data objects.

import UIKit
import CoreData
import SwiftyJSON

extension User {
    func set(withJson json: JSON) {
        self.id = json["id"].int32Value
        self.username = json["username"].stringValue
        self.email = json["email"].stringValue
        self.money = json["money"].int32 ?? self.money
        self.rankId = json["rank_id"].int32 ?? self.rankId
        self.fullname = json["fullName"].stringValue

        if let pictureUrl = json["picture"].string {
            self.picture = json["picture"].stringValue
        }
        else {
            let pictureJson = json["picture"]
            self.pictureWidth = pictureJson["width"].int16Value
            self.pictureHeight = pictureJson["height"].int16Value
            self.picture = pictureJson["url"].stringValue
        }
    }

    // Peform a fetch request on the main context for entities matching the predicate
    class func fetchOnMainContext(withPredicate predicate: NSPredicate?) -> [User]? {
        do {
            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            let mainContext = appDelegate.persistentContainer.viewContext
            let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
            fetchRequest.predicate = predicate
            let objects = try mainContext.fetch(fetchRequest)
            return objects
        } catch {
            print(error)
        }

        return nil
    }

    // Fetch a single entity that matches the predicate
    class func fetch(id: Int32, context: NSManagedObjectContext) -> User? {
        let predicate = NSPredicate(format: "id = %d", id)
        return fetchAndPerformBlockIfNotFound({return nil}, withPredicate: predicate, inContext: context)
    }

    // Fetch a single entity that matches the predicate or create a new entity if it doesn't exist
    class func fetchOrCreate(withPredicate predicate: NSPredicate?, inContext context: NSManagedObjectContext) -> User? {
        return fetchAndPerformBlockIfNotFound({return User(context: context)}, withPredicate: predicate, inContext: context)
    }

    // Helper method for fetching an entity with a predicate
    private class func fetchAndPerformBlockIfNotFound(_ block: () -> User?, withPredicate predicate: NSPredicate?, inContext context: NSManagedObjectContext) -> User? {
        do {
            let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
            fetchRequest.predicate = predicate
            let objects = try context.fetch(fetchRequest)

            if objects.count == 0 {
                return block()
            } else if objects.count == 1 {
                return objects.first
            } else {
                print("ERROR: fetch request found more than 1 object")
            }
        } catch {
            print(error)
        }

        return nil
    }
}


import UIKit
import CoreData
import SwiftyJSON

extension Game {
    func set(withJson json: JSON) {
        self.id = json["id"].int32Value
        let pickerJson = json["picker"]
        if let pickerId = pickerJson["id"].int32 {
            self.pickerId = pickerId
        }
        self.pickerChoiceId = json["pickerChoice_id"].int32 ?? self.pickerChoiceId
        self.creationDate = NSDate(timeIntervalSince1970: json["creationDate"].doubleValue)
        if let completedTimeInterval = json["completedDate"].double {
            self.completedDate = NSDate(timeIntervalSince1970: completedTimeInterval)
        }
        self.isPublic = json["isPublic"].boolValue
        self.maturityRating = json["rating"].int32Value
        self.beenUpvoted = json["beenUpvoted"].boolValue
        self.beenFlagged = json["beenFlagged"].boolValue
        let topCaptionJson = json["topCaption"]
        if let before = topCaptionJson["fitbBefore"].string,
            let after = topCaptionJson["fitbAfter"].string,
            let userEntry = topCaptionJson["userEntry"].string {
            self.topCaptionBefore = before
            self.topCaptionAfter = after
            self.topCaptionEntry = userEntry
        }
        if let picUrl = topCaptionJson["userPic"].string {
            self.topCaptionUserPicUrl = picUrl;
        }

        let pictureJson =  json["picture"]
        self.pictureUrl = pictureJson["url"].stringValue
        self.pictureWidth = pictureJson["width"].int16Value
        self.pictureHeight = pictureJson["height"].int16Value

        self.numUpvotes = json["numUpvotes"].int32Value
        self.topCaptionUserId = topCaptionJson["userId"].int32 ?? topCaptionUserId
        self.participants = NSSet()
    }
}

Below is our handleGames() and handleUsers() methods called after making their respective network requests. Both methods are called asynchronously by our NetworkManager. In handleGames(), we are also calling handleUsers() to set the participants for each game. However, doing this while our NetworkManager is also calling handleUsers() for other tasks simultaneously, we are thrown errors that multiple objects have been fetched, meaning multiple objects have been created prior to the fetch. We tried using performAndWait() but that still didn't work.

import CoreData
import SwiftyJSON

protocol CoreDataContextManager {
    var persistentContainer: NSPersistentContainer { get }
    func saveContext()
}

class CoreDataManager {
    static let shared = CoreDataManager()

    var contextManager: CoreDataContextManager!

    private init() {}

    // Perform changes to objects and then save to CoreData.
    fileprivate func perform(block: (NSManagedObjectContext)->()) {
        if self.contextManager == nil {
            self.contextManager = UIApplication.shared.delegate as! AppDelegate
        }
        let mainContext = self.contextManager.persistentContainer.viewContext

        block(mainContext)

        self.contextManager.saveContext()
    }
}

extension CoreDataManager: NetworkHandler {
    func handleUsers(_ usersJson: JSON, completion: (([User])->())? = nil) {
        var userCollection: [User] = []
        var idSet: Set<Int32> = Set()
        self.perform { context in

            for userJson in usersJson.arrayValue {

                guard userJson["id"].int != nil else {
                    continue
                }
                let predicate = NSPredicate(format: "id = %@", userJson["id"].stringValue)

                if  !idSet.contains(userJson["id"].int32Value), let user = User.fetchOrCreate(withPredicate: predicate, inContext: context) {
                    user.set(withJson: userJson)
                    userCollection.append(user)
                    idSet.insert(userJson["id"].int32Value)
                    //Establish Relations
                    if let rankId = userJson["rank_id"].int32, let rank = Rank.fetch(id: rankId, context: context) {
                        user.rank = rank
                    }
                }
            }
        }
        completion?(userCollection)
    }

func handleGames(_ gamesJson: JSON, completion: (([Game])->())? = nil) {
        var games: [Game] = []
        var idSet: Set<Int32> = Set()
        self.perform { context in

            for gameJson in gamesJson.arrayValue {

                guard gameJson["id"].int != nil else {
                    continue
                }

                let predicate = NSPredicate(format: "id = %@", gameJson["id"].stringValue)
                let gameId = gameJson["id"].int32Value

                // Make sure there are no duplicates
                if !idSet.contains(gameId), let game = Game.fetchOrCreate(withPredicate: predicate, inContext: context) {
                    game.set(withJson: gameJson)
                    games.append(game)

                    // Establish relationships
                    let userPredicate = NSPredicate(format: "id = %@", game.pickerId.description)
                    if let picker = User.fetch(id: game.pickerId, context: context) {
                        game.picker = picker
                    }
                    else if let picker = User.fetchOrCreate(withPredicate: userPredicate, inContext: context) {
                        picker.set(withJson: gameJson["picker"])
                        game.picker = picker
                    }
                    if let pickerChoiceId = gameJson["pickerChoice_id"].int32, let pickerChoice = Caption.fetch(id: pickerChoiceId, context: context) {
                        game.pickerChoice = pickerChoice
                    }
                    idSet.insert(gameId)

                    // add participants to game
                    handleUsers(gameJson["users"]) { users in
                        print("handleUsers for game.id: \(game.id)")
                        for user in users {
                            print("user.id: \(user.id)")
                        }
                        game.participants = NSSet(array: users)

                    }
                }
            }
        }
        completion?(games)
    }
}
1
You're calling your completion blocks in the wrong place in your handleUsers and handlerGames functions. You need to call inside the completion block to perform after the for-loop.dan
Good catch. I moved the completion calls but the issue persists.Jonathan Molina

1 Answers

0
votes

The way I am saving core data did not happen fast enough when other calls were trying to fetch the User core data object. I solved this issue by moving the self.contextManager.saveContext() from the perform() block to within each handleMethod(). I don't think this is the absolute correct way of doing it because there can still be some race conditions involved but it's an appropriate solution for me in the context of a small project. I will post a better answer if I find one later.