0

I am new to Core Data and I have encountered a problem. This app is supposed to store images from the photo library to core data and display those in a collection view.

However, the problem is the pictures display when the app is newly installed on the simulator and you just add pictures. But when you close the app and open again it crashes and shows an error in the console: Thread 1 EXC_BAD_ACCESS

// Loading Setup

@IBOutlet var collection: UICollectionView!
var images = [NSManagedObject]()

override func viewDidAppear(animated: Bool) {
    let managedContext = AppDelegate().managedObjectContext

    let fetchRequest = NSFetchRequest(entityName: "FullRes")

    do {
        let results =
        try managedContext.executeFetchRequest(fetchRequest)
        images = results as! [NSManagedObject]
    } catch let error as NSError {
        print("Could not fetch \(error), \(error.userInfo)")
    }
    collection.reloadData()

}
override func viewDidLoad() {
    super.viewDidLoad()
    imagePicker.delegate = self

    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = false

    // Register cell classes
    self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)

    // Do any additional setup after loading the view.
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

/*
// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

// MARK: UICollectionViewDataSource

override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}


override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of items
    return images.count
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! MainCollectionViewCell
    let selectedObject = images[indexPath.row]
    print(selectedObject)
    let image: UIImage = UIImage(data: selectedObject.valueForKey("imageData") as! NSData!)!
    cell.imageView.image = image

    // Configure the cell

    return cell
}

// MARK: UICollectionViewDelegate

/*
// Uncomment this method to specify if the specified item should be highlighted during tracking
override func collectionView(collectionView: UICollectionView, shouldHighlightItemAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}
*/

/*
// Uncomment this method to specify if the specified item should be selected
override func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}
*/

/*
// Uncomment these methods to specify if an action menu should be displayed for the specified item, and react to actions performed on the item
override func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool {
    return false
}

override func collectionView(collectionView: UICollectionView, canPerformAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) -> Bool {
    return false
}

override func collectionView(collectionView: UICollectionView, performAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) {

}
*/

// Camera Setup

// the image picker
let imagePicker = UIImagePickerController()
// a queue to save the image without freezing the App UI
let saveQueue = dispatch_queue_create("saveQueue", DISPATCH_QUEUE_CONCURRENT)
// the Managed Object Context where we will save our image to.
let managedContext = AppDelegate().managedObjectContext

@IBAction func imageDidPress(sender: AnyObject) {
    imagePicker.sourceType = UIImagePickerControllerSourceType.Camera
    presentViewController(imagePicker, animated: true, completion: nil)

}


@IBAction func addDidPress(sender: AnyObject) {
    imagePicker.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
    presentViewController(imagePicker, animated: true, completion: nil)

}

func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) {
    prepareImageForSaving(image)
    self.dismissViewControllerAnimated(true, completion: nil)

}

/*

func deletePhotoFromLibrary(info: [String : AnyObject]) {
        print("wasrun")
        let imageUrl = info[UIImagePickerControllerReferenceURL] as! NSURL
        let imageUrls = [imageUrl]
        //Delete asset
        PHPhotoLibrary.sharedPhotoLibrary().performChanges( {
            let imageAssetToDelete = PHAsset.fetchAssetsWithALAssetURLs(imageUrls, options: nil)
            PHAssetChangeRequest.deleteAssets(imageAssetToDelete)
            },
            completionHandler: { success, error in
                NSLog("Finished deleting asset. %@", (success ? "Success" : error!))
        })
    }

}
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
}
*/


func prepareImageForSaving(image:UIImage) {
    // use date as unique id
    let date : Double = NSDate().timeIntervalSince1970


    // dispatch with gcd.
    dispatch_async(saveQueue) {

        // create NSData from UIImage
        guard let imageData = UIImageJPEGRepresentation(image, 1) else {
            // handle failed conversion
            print("jpg error")
            return
        }

        // scale image
        let thumbnail = self.scale(image: image, toSize: self.view.frame.size)

        guard let thumbnailData  = UIImageJPEGRepresentation(thumbnail, 0.7) else {
            // handle failed conversion
            print("jpg error")
            return
        }

        // send to save function
        self.saveImage(imageData, thumbnailData: thumbnailData, date: date)

    }
}

func saveImage(imageData:NSData, thumbnailData:NSData, date: Double) {

    dispatch_barrier_async(saveQueue) {
        // create new objects in moc
        guard let fullRes = NSEntityDescription.insertNewObjectForEntityForName("FullRes", inManagedObjectContext: self.managedContext) as? FullRes, let thumbnail = NSEntityDescription.insertNewObjectForEntityForName("Thumbnail", inManagedObjectContext: self.managedContext) as? Thumbnail else {
            // handle failed new object in moc
            print("moc error")
            return
        }

        //set image data of fullres
        fullRes.imageData = imageData

        //set image data of thumbnail
        thumbnail.imageData = thumbnailData
        thumbnail.id = date as NSNumber
        //set relationship between the two objects
        thumbnail.fullRes = fullRes

        // save the new objects
        do {
            try self.managedContext.save()
        } catch {
            // implement error handling here
            fatalError("Failure to save context: \(error)")
        }

        // clear the moc
        self.managedContext.refreshAllObjects()
    }


}

func scale(image image:UIImage, toSize newSize:CGSize) -> UIImage {

    // make sure the new size has the correct aspect ratio
    let aspectFill = resizeFill(image.size, toSize: newSize)

    UIGraphicsBeginImageContextWithOptions(aspectFill, false, 0.0);
    image.drawInRect(CGRectMake(0, 0, aspectFill.width, aspectFill.height))
    let newImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return newImage
}

func resizeFill(fromSize: CGSize, toSize: CGSize) -> CGSize {

    let aspectOne = fromSize.height / fromSize.width
    let aspectTwo = toSize.height / toSize.width

    let scale : CGFloat

    if aspectOne < aspectTwo {
        scale = fromSize.height / toSize.height
    } else {
        scale = fromSize.width / toSize.width
    }

    let newHeight = fromSize.height / scale
    let newWidth = fromSize.width / scale
    return CGSize(width: newWidth, height: newHeight)

}

Update: Based on the comments made below, I have made changes to the code.

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! MainCollectionViewCell
    if let imagePath = images[indexPath.row].imageData {
        cell.imageView.image = UIImage(data: imagePath)
    }
    // Configure the cell

    return cell
}

But now, I get another error: 2015-12-26 20:10:24.464 Collect[480:69113] -[Thumbnail imageData]: unrecognized selector sent to instance 0x13fe9ba00.

The code class AppDelegate: UIResponder, UIApplicationDelegate { on AppDelegate.swift is highlighted.

It is an interesting thing that the app works until a certain point. For instance, when I uninstall the app from the phone I am running it in and then rerun it again, it works for a while until it crashes.

But still, it does not work perfectly like I hope it would be. Sometimes, one picture will appear only if another picture is added.

Update 2: I made some adjustments based on your comments below:

override func viewDidAppear(animated: Bool) {
    authenticateUser()

    let managedContext = AppDelegate().managedObjectContext

    let fetchRequest = NSFetchRequest(entityName: "Thumbnail")

    managedContext.performBlockAndWait { () -> Void in
        do {
            let results =
            try managedContext.executeFetchRequest(fetchRequest)
            self.images = results as! [Thumbnail]
        } catch let error as NSError {
            print("Could not fetch \(error), \(error.userInfo)")
        }
    }

    collection.reloadData()

}

It works fine whenever I delete the app and rerun it. However, when the app is closed, it no longer has the capability to restore the images.

I still get -[Thumbnail imageData]: unrecognized selector sent to instance 0x13762d650 while running it. The highlighted code is class AppDelegate: UIResponder, UIApplicationDelegate { on AppDelegate.swift

Chechu
  • 15
  • 5
  • Which part do you get the error message? – Jason Nam Dec 26 '15 at 03:55
  • 1
    why are you not using the NSManagedObject subclasses when you fetch? Just do a downcast with `if let imageData = results as? [FullRes]` Also look up optional binding. You are using a lot of `!` when you should be using `if let` [source of most of the code in the question](http://stackoverflow.com/a/27996685/4263818) – R Menke Dec 26 '15 at 04:14
  • @JasonNam let image: UIImage = UIImage(data: selectedObject.valueForKey("imageData") as! NSData!)! – Chechu Dec 26 '15 at 04:47
  • @RMenke Could you please expound on that? – Chechu Dec 26 '15 at 04:56
  • you found a `nil` somewhere and because you use three force unwraps in one line, we can not help you. Google optional binding. – R Menke Dec 26 '15 at 05:26
  • @RMenke I tried this but it still produces the same error: – Chechu Dec 26 '15 at 05:57
  • `override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! MainCollectionViewCell let selectedObject = images[indexPath.row] print(selectedObject) if let image = selectedObject.valueForKey("imageData") { cell.imageView.image = UIImage(data: image as! NSData) } // Configure the cell return cell }` – Chechu Dec 26 '15 at 05:57
  • @RMenke I tried the approach you suggested `cell.imageView.image = UIImage(data: images[indexPath.row].imageData!)` – Chechu Dec 26 '15 at 08:03
  • The same error persists – Chechu Dec 26 '15 at 08:03
  • As long as you are using `!` characters in your code it will crash. Since optionals are so basic and important, but als well covered in tutorials there is no point explaining them here. Googe it, fix how you handle optionals. Update the question, then you will get an answer. – R Menke Dec 26 '15 at 11:15
  • 1
    I suspect the underlying problem, which is causing the nil values, is one of threading/concurrency. Take a look at the [Concurrency section](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/CoreDataFramework/Classes/NSManagedObjectContext_Class/) of the `NSManagedObjectContext` class reference. – pbasdf Dec 27 '15 at 14:05
  • @pbasdf Please check my update above. Thank you. – Chechu Dec 29 '15 at 03:08
  • @pbasdf thx that was indeed the problem, I updated the answer from which this code originates. – R Menke Dec 29 '15 at 12:24
  • @user139863 did my answer help you? – R Menke Dec 30 '15 at 19:26
  • @RMenke Thank you for the update! – Chechu Jan 01 '16 at 00:50

1 Answers1

0

You are right, there is a mistake in my answer on the other question. CoreData needs all operation in one NSManagedObjectContext to happen on the same thread as the one on which it was created. I updated the answer.

Here is the relevant code :

Create two separate queue's in the UIViewController

let convertQueue = dispatch_queue_create("convertQueue", DISPATCH_QUEUE_CONCURRENT) // this one is for image stuff
let saveQueue = dispatch_queue_create("saveQueue", DISPATCH_QUEUE_CONCURRENT) // this one is for CoreData

Set value of moc on the correct thread

extension ViewController {

    // call this in viewDidLoad
    func coreDataSetup() {

        dispatch_sync(saveQueue) {
            self.managedContext = AppDelegate().managedObjectContext
        }
    }
}

Save images with :

extension ViewController {

    func saveImage(imageData:NSData, thumbnailData:NSData, date: Double) {

        dispatch_barrier_sync(saveQueue) {
            // create new objects in moc
            guard let moc = self.managedContext else {
                return
            }

            guard let fullRes = NSEntityDescription.insertNewObjectForEntityForName("FullRes", inManagedObjectContext: moc) as? FullRes, let thumbnail = NSEntityDescription.insertNewObjectForEntityForName("Thumbnail", inManagedObjectContext: moc) as? Thumbnail else {
                // handle failed new object in moc
                print("moc error")
                return
            }

            //set image data of fullres
            fullRes.imageData = imageData

            //set image data of thumbnail
            thumbnail.imageData = thumbnailData
            thumbnail.id = date as NSNumber
            thumbnail.fullRes = fullRes

            // save the new objects
            do {
                try moc.save()
            } catch {
                fatalError("Failure to save context: \(error)")
            }

            // clear the moc
            moc.refreshAllObjects()
        }
    }
}
R Menke
  • 8,183
  • 4
  • 35
  • 63
  • Thank you. But `guard let moc = self.managedContext else {` produces an error `Initializer for conditional binding must have Optional type, not 'NSManagedObjectContext'`. Also, where do I call `CoreDataSetup()` and when do I use the `convertQueue`? – Chechu Dec 31 '15 at 07:19
  • @user139863 Check the linked question from my comment. It has been updated – R Menke Dec 31 '15 at 09:18