Variable instantiated in prepare for Segue has nil properties in ViewDidLoad - ios

My view controller is loading before Core Location data is available from the model.
I've a Master View controller that modally pushes a new view controller with two NSManagedObject subclasses - records and locations - which are instantiated in a prepareForSegue method.
if segue.identifier == "newRecord"
{
let controller = (segue.destinationViewController as! NewRecordVC)
let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate // instantiate the delegate methods in AppDelegate
let managedContext = appDelegate!.managedObjectContext // create context from delegate methods
let recordEntity = NSEntityDescription.entityForName("RecordData", inManagedObjectContext: managedContext)
let locationEntity = NSEntityDescription.entityForName("Location", inManagedObjectContext: managedContext)
controller.location = Location(entity: locationEntity!, insertIntoManagedObjectContext: managedContext)
controller.record = Record(entity: recordEntity!, insertIntoManagedObjectContext: managedContext)
controller.managedContext = appDelegate?.managedObjectContext
print("segueing")
}
Both managed objects have inits for various values. Records have simple pre-assigned values assigned to properties that show up on the new view. Location properties, however, mainly require location services to assign property values. Printing property values shows that while a location is instantiated in viewDidLoad, the location properties assigned by location services are still nil. Location services are working - the properties print from within the model, BUT this is after viewDidLoad. On load, I need at least the geoPlacemark property, which provides text for the view.
override func viewDidLoad()
{
super.viewDidLoad()
...
if let recordNotNil = record
{
...
iD.text = "ID: \(recordNotNil.iD)"
}
if let locationNotNil = location
{
...
print("bing")
print(locationNotNil.temp)
record!.location = location!
print(location.geoPlacemark)
print(location.timestamp)
if let geoPlacemarkNotNil = location.geoPlacemark
{
print("bong")
locationText(geoPlacemarkNotNil)
}
}
}
Do I have to run the location services from within every view controller, not the model? Or is there a way to get the view to wait for the location delegate methods?

In prepareForSegue() the destinationViewController is already instantiated, and viewDidLoad() was already called.
If you want to set a label you should really do it in viewWillAppear()
override func viewWillAppear() {
super.viewWillAppear()
// Set up your views here
}
The other benefit of viewWillAppear() over viewDidLoad() is that it's called every time the ViewController is about to show up, which can be multiple times during the VCs lifecycle - as opposed to a single call ad the beginning of the VC's lifecycle of viewDidLoad().

The problem here came from using the normal override init method for the NSManagedObject.
class Record: NSManagedObject
{
override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?)
{
super.init(entity: entity, insertIntoManagedObjectContext: context)
let randomBit = String(arc4random_uniform(100))
iD = NSDate().toIDTimeDateString() + "-" + randomBit + "OtR"
believers = 0
deviceTime = NSDate()
doubters = 0
eventTimeStarted = NSDate()
eventTimeEnded = NSDate()
featureImageIndex = 0
coreLocationUsed = 0
photoTaken = 0
tableSection = "My Records"
timeRecorded = deviceTime
validationScore = 0
}
}
As per the apple doc.s, custom NSManagedObjects should be initiated using awakeFromInsert or awakeFromFetch.

Related

How to update UITabBarController from a Helper class?

I have a help class like this:
class NotificationHelper {
var managedObjectContext: NSManagedObjectContext!
init(context: NSManagedObjectContext) {
//super.init()
managedObjectContext = context
}
//functions related to Core Data
//update Badge
func updateBadge() {
var count = 1
let currentCount = self.tabBarController?.tabBar.items?[3].badgeValue
if currentCount != nil {
count = Int(currentCount!)! + 1
}
self.tabBarController?.tabBar.items?[3].badgeValue = String(count)
}
}
I'm just not sure how to get a reference to tabBarController so I can update it. I tried making the class inherit from UIViewController, but I think I was going down the wrong path there.
Also, am I correct in passing managedObjectContext like this? So that this class will be able to access Core Data.
Solved.
Instead of trying to inherit from somewhere, I decided to pass the UITabBarController as a parameter when needed:
func updateTabBarBadge(tabBarController: UITabBarController) {
It just means I have to call updateTabBarBadge every time I want to update it, instead of having other functions update it for me.

How do I transfer data from one table view cell to another?

I am trying to create a favorites page. How can I let a user click on an image in one table view and then the data from the table view they clicked on is transferred to another tableview in another page?
#IBAction func favoritesSelected(sender: AnyObject)
{
if toggleState == 1
{
sender.setImage(UIImage(named:"Star Filled-32.png"),forState:UIControlState.Normal)
isFav = true
var appDel:AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext
var newFave = NSEntityDescription.insertNewObjectForEntityForName("Favorites", inManagedObjectContext: context) as NSManagedObject
newFave.setValue("" + nameLabel.text!, forKey: "favorite")
do
{
try context.save()
}
catch _
{
print("error")
}
//print("\(newFave)")
print("Object saved")
toggleState = 2
}
From the code above, you can see what happens when a user clicks on the favorites button. The image changes and it uploads the name to the core data.
I'm trying to get it to go to another table view cell class so that when it gets to the favorites page, the names that were favorited will already be there.
I will show what I have in that class but I'm sure it's wrong.
if (result == 2)
{
var appDel:AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext
var request = NSFetchRequest(entityName: "Favorites")
request.returnsObjectsAsFaults = false
do
{
var results:NSArray = try context.executeFetchRequest(request)
if (results.count <= 0)
{
print("Either all object deleted or error")
}
}
catch _
{
print("error")
}
}
else
{
print("no show")
}
Option 1: NSNotification triggers tableView reload:
Register the UITableView tableView with the notification:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: nil)
func contextDidSave(sender: NSNotification) {
tableView.reloadData()
}
After the user clicks on the star in the first example and the context was saved properly, the contextDidSave callback will be executed and the tableView will load with the latest state in the DB
Option 2: Setup UITableView with NSFetchedResultsController
With this option, once the user clicks on the star and the context saves, iOS will trigger the update to selected cells automatically. See this article for reference:
https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsController_Class/
In my applications with a favourite function I have done the following:
1) On my model objects have a BOOL property called favourite, with a default value of false.
2) In the table view which lists all objects, when a user taps the favourite button, set the favourite property on the corresponding model object to true and save your managed object context.
3) In the table view in which you wish to display the favourited objects, query core data for all of your model objects with a favourite property that is true and display those results. As mentioned in Christopher Harris' answer, this is trivial if you are a using an NSFetchedResultController.

How to update managed object data?

I have started my first core data application. I am working with one entity right now called 'Folder'.
The first view controller displays all the Folders in a tableview, which I can add to and it reloads the data. This works fine because It uses the fetch request to populate the table.
override func viewWillAppear(animated: Bool) {
var error: NSError?
let request = NSFetchRequest(entityName: "Folder")
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
self.events = moc?.executeFetchRequest(request, error: &error) as! [Folder]
self.UITable.reloadData()
}
However when segueing to another view controller via the table cell I pass on the selected Folder data to the controller using the index path. e.g.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "showDetails" {
let destinationVC = segue.destinationViewController as! FolderDetailsViewController
let indexPath = UITable.indexPathForSelectedRow()
let selectedFolder = folders[indexPath!.row]
destinationVC.selectedFolder = selectedFolder
}
}
My second view controller uses the data passed from the first table view to display in textfields:
var selectedFolder: Folder!
folderNameLabel.text = selectedFolder?.title
folderDetailsLabel.text = selectedFolder?.details
folderDateLabel.text = displayDate
I then have a modal to edit/save the folder data in a modal appearing from the second controller:
//Edit and save event
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
//Error
var error: NSError?
//Storing Data from fields
SelectedFolder!.title = FolderName.text
SelectedFolder!.details = FolderDetails.text
SelectedFolder!.date = FolderDate.date
context?.save(&error)
self.dismissViewControllerAnimated(true, completion: {});
When dismissing the modulate data is not updated, I have to go back to the first controller to reload the data and segue again.
I think this is because I have no NSFetchRequest (or NSFetchResultsController) to get the most recent changes.
What is the best method to reload the data of the selectedFolder when I make the changes in the modal ?
You can refresh your second view in viewWillAppera() if your modal view is presented in full screen.
override func viewWillAppear(animated: Bool) {
{
folderNameLabel.text = selectedFolder?.title
folderDetailsLabel.text = selectedFolder?.details
folderDateLabel.text = displayDate
}
It seems like you would want to call moc.refreshObject(folder, mergeChanges:true)
See the documentation here.

NSObjectInaccessibleException - Deleting all entity objects - NSFetchedResultsController

I have a function responsible for deleting all the items of an entity:
func removeItems() {
if let managedContext = managedObjectContext {
let fetchRequest = NSFetchRequest()
let entity = NSEntityDescription.entityForName("Ent", inManagedObjectContext: managedContext)
fetchRequest.entity = entity
fetchRequest.includesPropertyValues = false
var error: NSError?
var results = managedContext.executeFetchRequest(fetchRequest, error: &error)
for result in results as [NSManagedObject] {
managedContext.deleteObject(result)
}
if !managedContext.save(&error) {
println("could not save \(error), \(error?.userInfo)")
}
}
}
My application consists of a TabBar with 3 screens:
The first tab presents a list of cities, and when one is selected, a segue is executed and goes to a product listing page, in which the user can "tag" products.
The second tab has a screen that shows the listing of these branded products, and also has a badge showing the amount of products
However, I need to delete all objects of this entity whenever the user selects a different city or when he starts the application after terminated.
For the first case, I delete all the objects in "prepareForSegue" function when the user selects a city, and it works perfectly.
The problem comes when I try to run the second case.
If I try to call the remove function in the "application didFinishLaunchingWithOptions" of the AppDelegate or "viewDidLoad" in the first tab, the bank is corrupted, and I get the following message when I try to enter in the second tab:
Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xd000000000140000 ''
But if I remove the function of "application didFinishLaunchingWithOptions" or "viewDidLoad" the first tab, the application works perfectly.
Looking more closely, the error is occurring in the second tab (the product listing).
I have a variable in which I use to keep up the items in the table (in the second tab):
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest()
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let entity = NSEntityDescription.entityForName("Entidade", inManagedObjectContext: managedContext)
fetchRequest.entity = entity
let sortDescriptor1 = NSSortDescriptor(key: "nome", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1]
let fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: managedContext,
sectionNameKeyPath: "nome",
cacheName: "Entidade")
fetchedResultsController.delegate = self
return fetchedResultsController
}()
And the error is occurring exactly this line of the second tab:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = editButtonItem()
var error: NSError?
if !fetchedResultsController.performFetch(&error) { // <----- HERE
fatalCoreDataError(error)
}
}
Would anyone have any suggestions of what I'm doing wrong?
The problem was entirely in the second tab.
The answer to the problem was to remove the variable:
lazy var fetchedResultsController: NSFetchedResultsController = {
.
.
.
}()
Now the "viewDidLoad" the second tab was as follows (the fetch was removed):
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = editButtonItem()
}
Was added the following variable:
var entities = [Entidade]()
And added the following methods:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
fetchLog()
}
And
func fetchLog() {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "Entidade")
var error: NSError? = nil
if let results = managedContext.executeFetchRequest(request, error: &error) as? [Entidade] {
self.entities = results
} else {
println("Could not fetch \(error), \(error!.userInfo)")
}
}
With these changes, I can finally remove the objects when the application is started by placing the following code in the cities list screen:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
appDelegate.removeItens()
}
Or choose to call the "removeItems()" method in AppDelegate when the application starts or ends.
If anyone needs, I can post the entire source code of the screens.
Updated
I found out what really happened, I have a method in AppDelegate in which is responsible for updating the "badgeValue" tab of the list whenever a user marks a product.
He was as follows (and was called every time a change occurred in managedObjectContext):
func updateUI() {
let tabBarController = window!.rootViewController as UITabBarController
if let tabBarViewControllers = tabBarController.viewControllers {
let navigationController = tabBarViewControllers[3] as UINavigationController
let listViewController = navigationController.viewControllers[0] as ListViewController
listViewController.managedObjectContext = managedObjectContext // <--- Here's the problem
let fetchRequest = NSFetchRequest(entityName: "Entidade")
if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) {
navigationController.tabBarItem.badgeValue = String(fetchResults.count)
}
}
}
I can not set the managedObjectContext to a screen this way, I need to assign it only once in the "application didFinishLaunchingWithOptions", so I got to keep the old code to take advantage of NSFetchedResultsController

change ViewController with SegmentedControl without destroy it

I have a few controllers. for some reason, I'm using UISegmentedControl instead of tab bar.
each few controllers download data from the servers. my problem is, if I move to next view controller and go back to the previous view controllers, I need to redownload again.
how to change view controller with UISegmentedControl without destroy the previous controller, so I don't need to redownload again. each time I move to different viewcontroller
here's my code
class ContentViewController: UIViewController {
private let homeViewController: HomeViewController!
private let aboutViewController: AboutViewController!
private let liveTVViewController: LiveTVViewController!
private let programsViewController: ProgramsViewController!
private var currentViewController: UIViewController!
var userDeviceType: Int!
override func viewDidLoad() {
super.viewDidLoad()
let viewController = viewControllerForSegmentIndex(0)
self.addChildViewController(viewController)
viewController.view.frame = self.view.bounds
self.view.addSubview(viewController.view)
currentViewController = viewController
NSNotificationCenter.defaultCenter().addObserver(self, selector: "segmentChanged:", name: "SegmentChangedNotification", object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func segmentChanged(notification: NSNotification) {
let userInfo = notification.userInfo as [String: AnyObject]
let selectedIndex = userInfo["selectedIndex"] as Int
let viewController = viewControllerForSegmentIndex(selectedIndex)
self.addChildViewController(viewController)
self.transitionFromViewController(currentViewController,
toViewController: viewController,
duration: 0.0,
options: UIViewAnimationOptions.CurveEaseIn,
animations: { () -> Void in
self.currentViewController.view.removeFromSuperview()
viewController.view.frame = self.view.bounds
self.view.addSubview(viewController.view)
}) { (finished: Bool) -> Void in
viewController.didMoveToParentViewController(self)
self.currentViewController.removeFromParentViewController()
self.currentViewController = viewController
}
}
func viewControllerForSegmentIndex(index: Int) -> UIViewController {
var viewController: UIViewController
if index == 0 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
(viewController as HomeViewController).userDeviceType = userDeviceType
} else if index == 1 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("ProgramsPage") as ProgramsViewController
} else if index == 2 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("LiveTVPage") as LiveTVViewController
} else if index == 3 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("AboutPage") as AboutViewController
} else {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
}
return viewController
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
thank you very much and sorry for my bad English
You should adopt the MVC design pattern. This will allow you to store the data you need downloaded in the Model. Then, when a certain view controller is loaded, you simply ask the model if that data exists. If it does, you'll get it back. Otherwise, you can download it as normal.
To further explain:
The Model-View-Controller (MVC) design pattern assigns objects in an application one of three roles: model, view, or controller. The pattern defines not only the roles objects play in the application, it defines the way objects communicate with each other.
Model objects encapsulate the data specific to an application and define the logic and computation that manipulate and process that data. For example, a model object might represent a character in a game or a contact in an address book. A model object can have to-one and to-many relationships with other model objects, and so sometimes the model layer of an application effectively is one or more object graphs. Much of the data that is part of the persistent state of the application (whether that persistent state is stored in files or databases) should reside in the model objects after the data is loaded into the application. Because model objects represent knowledge and expertise related to a specific problem domain, they can be reused in similar problem domains. Ideally, a model object should have no explicit connection to the view objects that present its data and allow users to edit that data—it should not be concerned with user-interface and presentation issues.
User actions in the view layer that create or modify data are communicated through a controller object and result in the creation or updating of a model object. When a model object changes (for example, new data is received over a network connection), it notifies a controller object, which updates the appropriate view objects
The above quotes are from the link I mentioned in the first paragraph.
You are instantiate new view controllers in viewControllerForSegmentIndex(index: Int) by mistake.
What you should do to avoid instantiate each view controller every time you switch back to it is to modify the properties as:
private let homeViewController: HomeViewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
private let aboutViewController: AboutViewController = self.storyboard!.instantiateViewControllerWithIdentifier("AboutPage") as AboutViewController
private let liveTVViewController: LiveTVViewController = self.storyboard!.instantiateViewControllerWithIdentifier("LiveTVPage") as LiveTVViewController
private let programsViewController: ProgramsViewController = self.storyboard!.instantiateViewControllerWithIdentifier("ProgramsPage") as ProgramsViewController
And change viewControllerForSegmentIndex(index: Int) to:
func viewControllerForSegmentIndex(index: Int) -> UIViewController {
var viewController: UIViewController
switch index {
case 0:
return homeViewController
case 1:
return programsViewController
case 2:
return liveTVViewController
case 3:
return aboutViewController
default:
return homeViewController
}
}

Resources