I'm relatively new to iOS Development and decided to implement my own Core Data stack, replacing Apple's default stack.
I've had to make changes in my code (obviously) and have been able to figure it out, however in this instance I cannot. Here is my code:
import UIKit
import CoreData
class AddTableViewController: UITableViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
@IBOutlet weak var nameTextField:UITextField!
@IBOutlet weak var locationTextField:UITextField!
@IBOutlet weak var imageView:UIImageView!
@IBOutlet weak var notesView:UITextView!
var coreDataStack = (UIApplication.sharedApplication().delegate as AppDelegate).coreDataStack
var backpackerSpot:BackpackerSpot!
var managedContext: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// TODO Give user the choice of the Photo Library or the Camera
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row == 0 {
if UIImagePickerController.isSourceTypeAvailable(.Camera) {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.delegate = self
imagePicker.sourceType = .Camera
self.presentViewController(imagePicker, animated: true, completion: nil)
}
}
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
// FIXME image is being displayed in landscape if it is taken in portrait mode by default
func imagePickerController(picker: UIImagePickerController!, didFinishPickingImage image: UIImage!, editingInfo: [NSObject : AnyObject]!) {
imageView.image = image
imageView.contentMode = UIViewContentMode.ScaleAspectFill
imageView.clipsToBounds = true
dismissViewControllerAnimated(true, completion: nil)
}
@IBAction func save() {
//validation
var errorField = ""
// TODO have placeholder text in the NOTES field match up with the placholder text in the NAME and LOCATION fields.
if nameTextField.text == "" {
errorField = "name"
} else if locationTextField.text == "" {
errorField = "location"
} else if notesView.text == "" {
errorField = "notes"
}
if errorField != "" {
let alertController = UIAlertController(title: "Error", message: "You must fill in \(errorField).", preferredStyle: .Alert)
let doneAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(doneAction)
self.presentViewController(alertController, animated: true, completion: nil)
return
}
// If all fields are correctly filled in, extract the field value
// Create Restaurant Object and save to data store
// if let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).coreDataStack.context {
let entityBackpackerSpot = NSEntityDescription.entityForName("BackpackerSpot", inManagedObjectContext: coreDataStack.context)
backpackerSpot?.spotName = nameTextField.text
backpackerSpot?.spotLocation = locationTextField.text
backpackerSpot?.spotImage = UIImagePNGRepresentation(imageView.image)
backpackerSpot?.spotNote = notesView.text
var error: NSError?
if !managedContext.save(&error) {
println("insert error: \(error!.localizedDescription)")
return
}
// Execute the unwind segue and go back to the home screen
performSegueWithIdentifier("unwindToHomeScreen", sender: self)
}
}
The app starts up fine, but when I click on the UIButton attached to the save function, it crashes and I get the following error:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
The line the debugger highlights is:
if !managedContext.save(&error) {
Admittedly, I have to spend some more time working with Optionals in Swift because they seem to often be a source of trouble for me, though usually I am able to figure it out. If anyone could point me in the right direction that would be great. Thank you.
EDIT: Here is my Core Data Stack:
import Foundation
import CoreData
class CoreDataStack {
let model: NSManagedObjectModel
let storeCoordinator: NSPersistentStoreCoordinator
let context: NSManagedObjectContext
let store: NSPersistentStore?
let dbName = "BackpackerSpots"
// these options do the migration for us
let options = [NSInferMappingModelAutomaticallyOption:true,
NSMigratePersistentStoresAutomaticallyOption: true]
init() {
//1 loading model from the file
let bundle = NSBundle.mainBundle()
let modelURL = bundle.URLForResource(dbName, withExtension: "momd")
model = NSManagedObjectModel(contentsOfURL: modelURL!)!
//2 store coordinator created using that model
storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
//3 context
context = NSManagedObjectContext()
context.persistentStoreCoordinator = storeCoordinator
//4. store
let documentsURL = MyDocumentDirectory()
let storeURL = documentsURL.URLByAppendingPathComponent(dbName)
var error:NSError?
store = storeCoordinator.addPersistentStoreWithType(NSSQLiteStoreType,
configuration: nil,
URL: storeURL,
options: nil,
error: &error)
if store == nil {
println("error adding persistent store: \(error)")
abort()
}
}
func saveContext() {
var error: NSError?
if context.hasChanges && !context.save(&error) {
println("Could not save. \(error), \(error?.description)")
}
}
}
EDIT: This is my root view controller:
import UIKit
import CoreData
class BackpackerSpotTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var coreDataStack = (UIApplication.sharedApplication().delegate as AppDelegate).coreDataStack
// var backpackerSpot:BackpackerSpot!
var managedContext: NSManagedObjectContext!
var backpackerSpots:[BackpackerSpot] = []
var fetchResultController:NSFetchedResultsController!
// Let's add some animations!
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
// initial cell state
cell.alpha = 0
// state after animations
UIView.animateWithDuration(1.0, animations: { cell.alpha = 1})
}
override func viewDidLoad() {
super.viewDidLoad()
var fetchRequest = NSFetchRequest(entityName: "BackpackerSpot")
let sortDescriptor = NSSortDescriptor(key: "spotName", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
// if let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).coreDataStack {
let entityBackpackerSpot = NSEntityDescription.entityForName("BackpackerSpot", inManagedObjectContext: coreDataStack.context)
fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
fetchResultController.delegate = self
var error: NSError?
var result = fetchResultController.performFetch(&error)
backpackerSpots = fetchResultController.fetchedObjects as [BackpackerSpot]
if result != true {
println(error?.localizedDescription)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.backpackerSpots.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as CustomTableViewCell
let backpackerSpot = backpackerSpots[indexPath.row]
cell.nameLabel.text = backpackerSpot.spotName
cell.thumbnailImageView.image = UIImage(data: backpackerSpot.spotImage)
cell.locationLabel.text = backpackerSpot.spotLocation
return cell
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
}
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]? {
var deleteAction = UITableViewRowAction(style: UITableViewRowActionStyle.Default, title: "Delete",handler: {
(action:UITableViewRowAction!, indexPath:NSIndexPath!) -> Void in
// if let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext {
let entityBackpackerSpot = NSEntityDescription.entityForName("BackpackerSpot", inManagedObjectContext: self.coreDataStack.context)
let backpackerSpotToDelete = self.fetchResultController.objectAtIndexPath(indexPath) as BackpackerSpot
self.managedContext.deleteObject(backpackerSpotToDelete)
var error: NSError?
if !self.managedContext.save(&error) {
println("Could not save \(error), \(error?.description)")
}
})
deleteAction.backgroundColor = UIColor(red: 169.0/255.0, green: 37.0/255.0, blue:
50.0/255.0, alpha: 1.0)
return [deleteAction]
}
func controllerWillChangeContent(controller: NSFetchedResultsController!) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController!, didChangeObject anObject: AnyObject!, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
case .Update:
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
default:
tableView.reloadData()
}
backpackerSpots = controller.fetchedObjects as [BackpackerSpot]
}
func controllerDidChangeContent(controller: NSFetchedResultsController!) {
tableView.endUpdates()
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "showBackpackerSpotDetails" {
if let row = tableView.indexPathForSelectedRow()?.row {
let destinationController = segue.destinationViewController as DetailViewController
destinationController.backpackerSpot = backpackerSpots[row]
}
}
}
@IBAction func unwindToHomeScreen(segue: UIStoryboardSegue) {
}
}