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)
}
}
handleUsers
andhandlerGames
functions. You need to call inside the completion block toperform
after the for-loop. – dan