Creating and assigning a new NSManagedObject to a new NSManagedObject *sometimes* fails - swift

I am creating a new NSManagedObject called "translation". Within the translation I need to create two additional NSManagedObjects called "phrase". Sometimes one of the phrase assignments will throw an error, but when I inspect the values they all look like they were created just fine. What gives???
Creating A Translation Object:
func getOrCreateTranslation(package: Package?, data: NSDictionary) -> Translation {
let translationId = data["id"] as! NSNumber
if let translation = self.getTranslation(translationId) {
return translation
} else {
let context = LocalDataStorage().context
let translation = NSEntityDescription.insertNewObjectForEntityForName("Translation", inManagedObjectContext: context) as! Translation
translation.id = translationId
let fromPhrase = data["from_phrase"]! as! NSDictionary
let toPhrase = data["to_phrase"]! as! NSDictionary
let pm = PhraseManager()
//*******
// *SOMETIMES* ONE OF THESE LINES FAIL WITH BAD_EXC_ACCESS code=1
translation.fromPhrase = pm.getOrCreatePhrase(fromPhrase)
translation.toPhrase = pm.getOrCreatePhrase(toPhrase)
//******
if package != nil {
package!.addTranslationObject(translation)
}
return translation
}
}
Creating A Phrase Object:
func getOrCreatePhrase(data: NSDictionary) -> Phrase {
// check if phrase exists
let phraseId = data["id"] as! NSNumber
if let phrase = self.getPhrase(phraseId) {
return phrase
} else {
let context = localDataStorage.context
let lm = LanguageManager()
let phrase = NSEntityDescription.insertNewObjectForEntityForName("Phrase", inManagedObjectContext: context) as! Phrase
phrase.id = phraseId
phrase.text = data["text"] as! String
phrase.audioUrl = data["audio_url"] as? String
let code = data["language"]!["language_code"] as! String
phrase.language = lm.getLanguageFromCode(code)
return phrase
}
}
Call Made to API:
func getPackageTranslations(package: Package, completion: ([Translation])-> Void) {
let currentLanguage: Language = LanguageManager().getCurrentLanguage()!
let urlString = baseAPIString + "/groups/\(package.id!)/translations/?language_code=\(currentLanguage.code)"
let session = NSURLSession.sharedSession()
let serachUrl = NSURL(string: urlString)
let task = session.dataTaskWithURL(serachUrl!) {
(data, response, error) -> Void in
if error != nil {
print(error?.localizedDescription)
} else {
let jsonData: NSDictionary!
do {
jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as! NSDictionary
} catch _ {
jsonData = NSDictionary()
}
let groupTranslationsData = jsonData["group_translations"] as! [NSDictionary]
var translations = [Translation]()
let context = LocalDataStorage().context
for groupTranslation in groupTranslationsData {
let translationData = groupTranslation["translation"] as! NSDictionary
let translation = TranslationManager().getOrCreateTranslation(package, data: translationData)
if translation.packages?.containsObject(package) == false {
//package.addTranslationObject(translation!)
//translation!.addPackageObject(package)
}
translations.append(translation)
}
do {
try context.save()
} catch {
print("There was a problem saving translation ")
}
dispatch_async(dispatch_get_main_queue(), {
completion(translations)
})
}
}
task.resume()
}
CoreData Context Class:
class LocalDataStorage {
let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context: NSManagedObjectContext!
init() {
context = appDelegate.managedObjectContext
}
}

This issue occurs when you create a NSManagedObjectContext with a concurrency pattern that it should be interacted on and you perform actions on the it on a thread different from the concurrency pattern specified during its initialization.
The completion block of NSURLSession.dataTaskWithURL is run on another thread, so you must dispatch to the type of thread specified in the context creation to perform any operation successfully on it.
If the concurrency type of your context is MainQueueConcurrencyType which is used in most cases, you must perform the context save method on the main queue.
dispatch_async(dispatch_get_main_queue()) {
do {
try context.save()
} catch {
print("There was a problem saving translation ")
}
completion(translations)
}

Related

How to wait for Swift's URLSession to finish before running again?

Probably a stupid question, but I'm a beginner at this.
The below code is supposed to get book information from Google Books from a keyword search. It then goes through the results and checks if I have a matching ISBN in a Firebase database. It works, but currently can only search 40 books as that's the Google Books API maximum per search.
Fortunately, I can specify where to start the index and get the next 40 books to search as well. Unfortunately, I've been trying for hours to understand how the URLSession works. All the methods I've tried have shown me that the code after the URLSession block doesn't necessarily wait for the session to complete. So if I check if I've found any matches afterward, it might not even be done searching.
I suspect the answer is in completion handling, but my attempts so far have been unsuccessful. Below is my code with a URL setup to take various starting index values.
var startingIndex = 0
//encode keyword(s) to be appended to URL
let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = "https://www.googleapis.com/books/v1/volumes?q=\(query)&&maxResults=40&startIndex=\(startingIndex)"
URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}else{
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String: AnyObject]
if let items = json["items"] as? [[String: AnyObject]] {
//for each result make a book and add title
for item in items {
if let volumeInfo = item["volumeInfo"] as? [String: AnyObject] {
let book = Book()
//default values
book.isbn13 = "isbn13"
book.isbn10 = "isbn10"
book.title = volumeInfo["title"] as? String
//putting all authors into one string
if let temp = volumeInfo["authors"] as? [String] {
var authors = ""
for i in 0..<temp.count {
authors = authors + temp[i]
}
book.author = authors
}
if let imageLinks = volumeInfo["imageLinks"] as? [String: String] {
book.imageURL = imageLinks["thumbnail"]
}
//assign isbns
if let isbns = volumeInfo["industryIdentifiers"] as? [[String: String]] {
for i in 0..<isbns.count {
let firstIsbn = isbns[i]
if firstIsbn["type"] == "ISBN_10" {
book.isbn10 = firstIsbn["identifier"]
}else{
book.isbn13 = firstIsbn["identifier"]
}
}
}
//adding book to an array of books
myDatabase.child("listings").child(book.isbn13!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
myDatabase.child("listings").child(book.isbn10!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
}
}
}
}
SVProgressHUD.dismiss()
}.resume()
Below is my revised code:
func searchForSale(query: String, startingIndex: Int) {
directionsTextLabel.isHidden = true
tableView.isHidden = false
listings.removeAll()
DispatchQueue.main.async { self.tableView.reloadData() }
SVProgressHUD.show(withStatus: "Searching")
//clear previous caches of textbook images
cache.clearMemoryCache()
cache.clearDiskCache()
cache.cleanExpiredDiskCache()
let url = "https://www.googleapis.com/books/v1/volumes?q=\(query)&&maxResults=40&startIndex=\(startingIndex)"
URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}else{
var needToContinueSearch = true
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String: AnyObject]
if json["error"] == nil {
let totalItems = json["totalItems"] as? Int
if totalItems == 0 {
SVProgressHUD.showError(withStatus: "No matches found")
return
}
if let items = json["items"] as? [[String: AnyObject]] {
//for each result make a book and add title
for item in items {
if let volumeInfo = item["volumeInfo"] as? [String: AnyObject] {
let book = Book()
//default values
book.isbn13 = "isbn13"
book.isbn10 = "isbn10"
book.title = volumeInfo["title"] as? String
//putting all authors into one string
if let temp = volumeInfo["authors"] as? [String] {
var authors = ""
for i in 0..<temp.count {
authors = authors + temp[i]
}
book.author = authors
}
if let imageLinks = volumeInfo["imageLinks"] as? [String: String] {
book.imageURL = imageLinks["thumbnail"]
}
//assign isbns
if let isbns = volumeInfo["industryIdentifiers"] as? [[String: String]] {
for i in 0..<isbns.count {
let firstIsbn = isbns[i]
//checks if isbns have invalid characters
let isImproperlyFormatted = firstIsbn["identifier"]!.contains {".$#[]/".contains($0)}
if isImproperlyFormatted == false {
if firstIsbn["type"] == "ISBN_10" {
book.isbn10 = firstIsbn["identifier"]
}else{
book.isbn13 = firstIsbn["identifier"]
}
}
}
}
//adding book to an array of books
myDatabase.child("listings").child(book.isbn13!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
needToContinueSearch = false
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
myDatabase.child("listings").child(book.isbn10!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
needToContinueSearch = false
}
DispatchQueue.main.async { self.tableView.reloadData() }
return
}
if startingIndex < 500 {
if needToContinueSearch {
let nextIndex = startingIndex + 40
self.searchForSale(query: query, startingIndex: nextIndex)
}
}
})
}
}
}
}else{
return
}
}
SVProgressHUD.dismiss()
}.resume()
//hide keyboard
self.searchBar.endEditing(true)
}
In your completion handler if any results have been returned you end with:
DispatchQueue.main.async { self.tableView.reloadData() }
to trigger reloading of your table with the updated information. At this same point is where you could determine of there may be more results and initiate the next asynchronous URL task. In outline your code might be:
let needToContinueSearch : Bool = ...;
DispatchQueue.main.async { self.tableView.reloadData() }
if needToContinueSearch
{ // call routine it initiate next async URL task
}
(If there is any reason to start the task from the main thread the if would be in the block.)
By not initiating the next search until after you've processed the results of the first you avoid having to deal with any issues of a subsequent callback trying to update your data at the same time as a previous one.
However if you find delaying the second search in this way is too slow you can investigate ways to overlap the operations, e.g. you might have the callback just pass the processing of the results to an async task on a serial queue (so that only one set of results is being processed at once) and initiate the next async URL task.
HTH
Declare a bool variable as isLoading and if that function is loading dont trigger urlsession. hope below sample will help you.
var isLoading : Bool = false
func loadMore(with pageCount: Int){
if isLoading { return }
isLoading = true
// call the network
URLSession.shared.dataTask(with: URL(string: "xxxxx")!) { (data, response, error) in
// after updating the data set isloding to false again
// do the api logic here
//
DispatchQueue.main.async {
// self.items = downloadedItems
self.tableView.reloadData()
self.isLoading = false
}
}.resume()
}

Swift Core Data Class method?

I'm currently learning Core Data and I have two view controllers that are using the same piece of code to get a users profile. The problem is that it's the same code copy and pasted and I would like to avoid this. I'm using the Managed Class approach to access the data and each controller has the following method:
var profileHolder: Profile!
let profileRequest = Profile.createFetchRequest()
profileRequest.predicate = NSPredicate(format: "id == %d", 1)
profileRequest.fetchLimit = 1
if let profiles = try? context.fetch(profileRequest) {
if profiles.count > 0 {
profileHolder = profiles[0]
}
}
if profileHolder == nil {
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
profileHolder = newProfile
}
profile = profileHolder
Profile is a var inside the controller: var profile: Profile! and I call the above inside viewWillAppear()
I know there's a cleaner approach and I would like to move this logic inside the class but unsure how to.
Thanks
var profileHolder: Profile!
profileHolder here is force unwrapping optional value. And you are fetching from core data and assigning the value in viewWillAppear, which is risky as profileHolder would be nil and can trigger crash if you access it before viewWillAppear.
My suggestion would be:
var profileHolder: Profile
{
if let profiles = try? context.fetch(profileRequest),
profiles.count > 0
{
return profiles[0]
}
else
{
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
return newProfile
}
}()
This will ensure profileHolder is either fetched or created when the view controller is initialised.
However this would not work if
context
is a stored property of viewController, in which case, do:
var profileHolder: Profile?
override func viewDidLoad()
{
if let profiles = try? context.fetch(profileRequest),
profiles.count > 0
{
return profiles[0]
}
else
{
let newProfile = Profile(context: context)
newProfile.id = 1
newProfile.attempts = nil
return newProfile
}
}
Here is the struct I created for a project I did that allows me to access my CoreData functions anywhere. Create a new empty swift file and do something like this.
import CoreData
// MARK: - CoreDataStack
struct CoreDataStack {
// MARK: Properties
private let model: NSManagedObjectModel
internal let coordinator: NSPersistentStoreCoordinator
private let modelURL: URL
internal let dbURL: URL
let context: NSManagedObjectContext
let privateContext: NSManagedObjectContext
// MARK: Initializers
init?(modelName: String) {
// Assumes the model is in the main bundle
guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") else {
print("Unable to find \(modelName)in the main bundle")
return nil
}
self.modelURL = modelURL
// Try to create the model from the URL
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
print("unable to create a model from \(modelURL)")
return nil
}
self.model = model
// Create the store coordinator
coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
// create a context and add connect it to the coordinator
//context.persistentStoreCoordinator = coordinator
privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.persistentStoreCoordinator = coordinator
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = privateContext
// Add a SQLite store located in the documents folder
let fm = FileManager.default
guard let docUrl = fm.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Unable to reach the documents folder")
return nil
}
self.dbURL = docUrl.appendingPathComponent("model.sqlite")
// Options for migration
let options = [NSInferMappingModelAutomaticallyOption: true,NSMigratePersistentStoresAutomaticallyOption: true]
do {
try addStoreCoordinator(NSSQLiteStoreType, configuration: nil, storeURL: dbURL, options: options as [NSObject : AnyObject]?)
} catch {
print("unable to add store at \(dbURL)")
}
}
// MARK: Utils
func addStoreCoordinator(_ storeType: String, configuration: String?, storeURL: URL, options : [NSObject:AnyObject]?) throws {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: dbURL, options: nil)
}
}
// MARK: - CoreDataStack (Removing Data)
internal extension CoreDataStack {
func dropAllData() throws {
// delete all the objects in the db. This won't delete the files, it will
// just leave empty tables.
try coordinator.destroyPersistentStore(at: dbURL, ofType:NSSQLiteStoreType , options: nil)
try addStoreCoordinator(NSSQLiteStoreType, configuration: nil, storeURL: dbURL, options: nil)
}
}
// MARK: - CoreDataStack (Save Data)
extension CoreDataStack {
func saveContext() throws {
/*if context.hasChanges {
try context.save()
}*/
if privateContext.hasChanges {
try privateContext.save()
}
}
func autoSave(_ delayInSeconds : Int) {
if delayInSeconds > 0 {
do {
try saveContext()
print("Autosaving")
} catch {
print("Error while autosaving")
}
let delayInNanoSeconds = UInt64(delayInSeconds) * NSEC_PER_SEC
let time = DispatchTime.now() + Double(Int64(delayInNanoSeconds)) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: time) {
self.autoSave(delayInSeconds)
}
}
}
}
Create a class(CoreDataManager) that can manage core data operations.
import CoreData
class CoreDataManager:NSObject{
/// Application Document directory
lazy var applicationDocumentsDirectory: URL = {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return urls[urls.count-1]
}()
/// Core data manager
static var shared = CoreDataManager()
/// Managed Object Model
lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = Bundle.main.url(forResource: “your DB name”, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
/// Persistent Store Coordinator
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.appendingPathComponent("SingleViewCoreData.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
let options = [ NSInferMappingModelAutomaticallyOption : true,
NSMigratePersistentStoresAutomaticallyOption : true]
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: options)
persistanceStoreKeeper.sharedInstance.persistanceStorePath = url
} catch {
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
abort()
}
return coordinator
}()
/// Managed Object Context
lazy var managedObjectContext: NSManagedObjectContext = {
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
/// Save context
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
}
Add the bellow function in your class.
func fetchProfile(profileId:String,fetchlimit:Int,completion: ((_ fetchedList:["Your model class"]) -> Void)){
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Your entity name")
let predicate:NSPredicate = NSPredicate(format: "id = %#", profileId)
fetchRequest.predicate=predicate
fetchRequest.fetchLimit = fetchlimit
do {
let results =
try CoreDataManager.shared.managedObjectContext.fetch(fetchRequest)
let profileList:["Your model class"] = results as! ["Your model class"]
if(profileList.count == 0){
//Empty fetch list
}
else{
completion(profileList)
}
}
catch{
//error
}
}
replace "Your model class" according to your requirement.
You can call the function "fetchProfile" and you will get the result inside the completion block.

How To Delete all Entity Objects in CoreData Swift 3

So I have this code which works fine, but I want a much better one.
func deleteCoreDataObjects() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//where groupData is an Array of an Entity
for i in 0..<self.groupData.count {
context.delete(groupData[i])
}
(UIApplication.shared.delegate as! AppDelegate).saveContext()
}
Currently I'm deleting the objects one by one via for loop.
You can try this:
func deleteAllData(entity: String)
{
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: entity)
fetchRequest.returnsObjectsAsFaults = false
do
{
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
}
} catch let error as NSError {
print("Detele all data in \(entity) error : \(error) \(error.userInfo)")
}
}
Usage:
self.deleteAllData("your_entityName")
Already seen in: https://stackoverflow.com/a/33931528/2894160
Best is delete the persistence storage and then add new one instead of looping each entity (if you want to delete all entities from coredata).
func deletePersistentStoreCoordinator () {
do {
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("YourDatabaseName.sqlite")
try self.persistentStoreCoordinator.destroyPersistentStoreAtURL(url, withType: NSSQLiteStoreType, options: nil)
try self.persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
}
catch{
}
}
Here is the code for deleting records from Core Data :
//Delete user info from local db
func deleteUserInfo() {
let context = appdelegate.managedObjectContext
let coord = appdelegate.persistentStoreCoordinator
let fetchRequest = NSFetchRequest(entityName: "User")
if #available(iOS 9.0, *) {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
//let predicate = NSPredicate(format: "id == %#", key)
//fetchRequest.predicate = predicate
do {
try coord.executeRequest(deleteRequest, withContext: context)
}
catch let error as NSError {
//Error handling
}
catch {}
} else {
// Fallback on earlier versions
do {
let users: NSArray = try appdelegate.managedObjectContext.executeFetchRequest(fetchRequest)
for user in users {
appdelegate.managedObjectContext.delete(user)
}
try appdelegate.managedObjectContext.save()
} catch let error as NSError {
//Error handling
}
catch {}
}
}

What could possibly cause a memory leak in this code?

I've been trying to find out what causes memory leaks, but in Instruments, I am being told that the memory leak happens at seriesDownloadingQueue.addOperation(downloadOperation) and in the class that calls the method getSeries(), which starts the updating process.
I'm completely new to asynchronous programming and memory leaks. I've read some posts on stack overflow about finding and fixing the leaks. I've also read an article from http://www.raywenderlich.com on how to use Instruments. This app is a database preloader. It downloads and processes information. The .sqlite file will be used in a mobile app.
Below is the code which allegedly causes the memory leak. If you need more information, I'll provide it.
import Foundation
import CoreData
class SerieFetcher: NSObject{
dynamic var didGetSeries: Bool = false
static var doneSeries: Int = 0
func getSeries(){
var seriesDownloadingQueue: NSOperationQueue{
let val = NSOperationQueue()
val.maxConcurrentOperationCount = 32
val.name = "Serie Downloading & Processing Queue"
return val
}
var defaultSessionConfiguration:NSURLSessionConfiguration{
let val = NSURLSessionConfiguration.defaultSessionConfiguration()
val.HTTPMaximumConnectionsPerHost = 20
return val
}
let defaultSession: NSURLSession = NSURLSession(configuration: defaultSessionConfiguration,delegate: nil, delegateQueue: seriesDownloadingQueue)
if let countries = fetchCountries(){
for country in countries{
if let url = NSURL(string:(BASE_URL + "series/"+CAT_STAMPS+"producer/\(country.0)")){
let downloadOperation = downloadSeriesOperation(downloadURL: url, countryObjectID: country.1, countryCount: countries.count, defaultSession: defaultSession , completionHandler: { [weak self](didGetSeries) in
if(didGetSeries == true){
self!.didGetSeries = didGetSeries
print("Finished Downloading Series")
}
})
downloadOperation.completionBlock = nil
seriesDownloadingQueue.addOperation(downloadOperation)
}
}
}
}
func fetchCountries() -> [Int: NSManagedObjectID]?{
let fetchRequest = NSFetchRequest(entityName: "Country")
fetchRequest.resultType = .DictionaryResultType
let objectIDDesc = NSExpressionDescription()
objectIDDesc.name = "objectID"
objectIDDesc.expression = NSExpression.expressionForEvaluatedObject()
objectIDDesc.expressionResultType = .ObjectIDAttributeType
fetchRequest.propertiesToFetch = ["countryID",objectIDDesc]
fetchRequest.returnsDistinctResults = true
do{
let results = try managedContext.executeFetchRequest(fetchRequest) as! [NSDictionary]
var countryIDs: [Int: NSManagedObjectID] = [:]
for result in results{
let countryID: Int = result.valueForKey("countryID") as! Int
let objectID: NSManagedObjectID = result.valueForKey("objectID") as! NSManagedObjectID
countryIDs.updateValue(objectID, forKey: countryID)
}
return countryIDs
}catch let error as NSError{
print(error.localizedDescription)
}
return nil
}
}
class downloadSeriesOperation: NSOperation{
let countryObjectID:NSManagedObjectID
let downloadURL:NSURL
let countryCount:Int
let defaultSession:NSURLSession
let completionHandler: (didGetSeries: Bool) -> Void
init(downloadURL:NSURL, countryObjectID: NSManagedObjectID,countryCount: Int, defaultSession:NSURLSession, completionHandler: (didGetSeries:Bool) -> Void){
self.downloadURL = downloadURL
self.countryObjectID = countryObjectID
self.countryCount = countryCount
self.defaultSession = defaultSession
self.completionHandler = completionHandler
}
override func main() {
let dataTask = defaultSession.dataTaskWithURL(downloadURL, completionHandler: { (data, response, error) in
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.persistentStoreCoordinator = managedContext.persistentStoreCoordinator
privateMOC.undoManager = nil
var parsedData: NSArray?
do{
parsedData = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as? NSArray
}catch let error as NSError{
print(error.localizedDescription)
}
for val in parsedData!{
if let serieID = Int(val[0] as! NSString as String){
if let serieName = val[1] as? NSString as? String{
if serieID == 0{
continue
}
let serie = NSEntityDescription.insertNewObjectForEntityForName("Serie", inManagedObjectContext: privateMOC) as! Serie
serie.country = privateMOC.objectWithID(self.countryObjectID) as? Country
serie.serieID = serieID
serie.serieName = serieName
}
}
}
privateMOC.performBlock({
do{
try privateMOC.save()
privateMOC.reset()
}catch let error as NSError{
print(error.localizedDescription)
}
})
SerieFetcher.doneSeries += 1
print(self.countryCount,"/",SerieFetcher.doneSeries)
if(SerieFetcher.doneSeries == self.countryCount){
self.completionHandler(didGetSeries: true)
}
})
dataTask.resume() //Start the Download
}
}

Nested dataTaskWithRequest in Swift tvOS

I'm a C# developer convert to Swift tvOs and just starting to learn. I've made some progress, but not sure how to handle nested calls to json. The sources are from different providers so I can't just combine the query.
How do I wait for the inner request to complete so the TVSeries has the poster_path? Is there a better way to add the show to the collection and then process the poster path loading in another thread so it doesn't delay the UI Experience?
func downloadTVData() {
let url_BTV = NSURL(string: BTV_URL_BASE)!
let request_BTV = NSURLRequest(URL: url_BTV)
let session_BTV = NSURLSession.sharedSession()
//get series data
let task_BTR = session_BTV.dataTaskWithRequest(request_BTV) { (data_BTV, response_BTV, error_BTV) -> Void in
if error_BTV != nil {
print (error_BTV?.description)
} else {
do {
let dict_BTV = try NSJSONSerialization.JSONObjectWithData(data_BTV!, options: .AllowFragments) as? Dictionary<String, AnyObject>
if let results_BTV = dict_BTV!["results"] as? [Dictionary<String, AnyObject>]{
for obj_BTV in results_BTV {
let tvshow = TVSeries(tvDict: obj_BTV)
//for each tv series try to load a poster_path from secondary provider
if let str = obj_BTV["title"] as? String!{
let escapedString = str?.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet())!
if let url = NSURL(string: self.SEARCH_URL_BASE + escapedString!) {
let request = NSURLRequest(URL: url)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
if error != nil {
print (error?.description)
} else {
do {
let dict = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments) as? Dictionary<String, AnyObject>
if let results = dict!["results"] as? [Dictionary<String, AnyObject>] {
//iterate through the poster array
for obj in results {
if let path = obj["poster_path"] as? String {
tvshow.posterPath = path
break
}
}
}
} catch let error as NSError {
print(error.description)
}
}
}
task.resume()
}
}
self.tvSeries.append(tvshow)
}
dispatch_async(dispatch_get_main_queue()){
self.collectionView.reloadData()
}
}
} catch let error as NSError {
print(error.description)
}
}
}
task_BTR.resume()
}
Thanks for your help!
I would recommend breaking things apart into multiple methods, with callbacks to sequence the operations, and utilizing Swift's built-in throws error handling mechanism. Here's an example, not perfect, but might help as a starting point:
class TVSeries
{
let title: String
var posterPath: String?
enum Error: ErrorType {
case MalformedJSON
}
init(tvDict: [String: AnyObject]) throws
{
guard let title = tvDict["title"] as? String else {
throw Error.MalformedJSON
}
self.title = title
}
static func loadAllSeries(completionHandler: [TVSeries]? -> Void)
{
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: BTV_URL_BASE)!) { data, response, error in
guard let data = data else {
print(error)
completionHandler(nil)
return
}
do {
completionHandler(try fromJSONData(data))
}
catch let error {
print(error)
}
}.resume()
}
static func fromJSONData(jsonData: NSData) throws -> [TVSeries]
{
guard let dict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: .AllowFragments) as? [String: AnyObject] else {
throw Error.MalformedJSON
}
guard let results = dict["results"] as? [[String: AnyObject]] else {
throw Error.MalformedJSON
}
return try results.map {
return try TVSeries(tvDict: $0)
}
}
func loadPosterPath(completionHandler: () -> Void)
{
guard let searchPath = title.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet()) else {
completionHandler()
return
}
let url = NSURL(string: SEARCH_URL_BASE)!.URLByAppendingPathComponent(searchPath)
NSURLSession.sharedSession().dataTaskWithURL(url) { [weak self] data, response, error in
defer { completionHandler() }
guard let strongSelf = self else { return }
guard let data = data else {
print(error)
return
}
do {
strongSelf.posterPath = try TVSeries.posterPathFromJSONData(data)
}
catch let error {
print(error)
}
}.resume()
}
static func posterPathFromJSONData(jsonData: NSData) throws -> String?
{
guard let dict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: .AllowFragments) as? [String: AnyObject] else {
throw Error.MalformedJSON
}
guard let results = dict["results"] as? [[String: AnyObject]] else {
throw Error.MalformedJSON
}
for result in results {
if let path = result["poster_path"] as? String {
return path
}
}
return nil
}
}
It might also be worth your time to look into something like RxSwift or Alamofire, which help you with these kinds of data-conversion / sequencing operations.