fatal errors with optionals not making sense - swift

I keep getting a fatal error saying how a value was unwrapped and it was nil and I don't understand how. When I instantiate a view controller with specific variables they all show up, but when I perform a segue to the exact VC, the values don't show up.
Take these functions for example...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let displayVC = storyboard?.instantiateViewController(withIdentifier: Constants.Storyboards.TeachStoryboardID) as? SchoolEventDetailsViewController {
displayVC.selectedEventName = events[indexPath.row].eventName
displayVC.selectedEventDate = documentsDate[indexPath.row].eventDate
displayVC.selectedEventCost = documentsCost[indexPath.row].eventCost
displayVC.selectedEventGrade = documentsGrade[indexPath.row].eventGrade
displayVC.selectedEventDocID = documentsID[indexPath.row]?.docID
navigationController?.pushViewController(displayVC, animated: true)
}
}
This combined with this function :
func verifyInstantiation() {
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = dateToLoad
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = costToLoad
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = gradesToLoad
}
if let docIDtoLoad = selectedEventDocID {
docIDUneditableTextF.text = docIDtoLoad
}
if let eventNameToLoad = selectedEventName {
eventNameEditableTextF.text = eventNameToLoad
}
}
Helps load the data perfectly, but when I try to perform a segue from a search controller the data is not there.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
I set the title of the vc to have the event name , and I also recently added a text field to store it as well for experimental purposes (this question).
Now the issue is I want to do a data transfer from an Algolia Search Controller to that VC and I got all the other fields to show up, except for one and that was the document ID. So I created a completion handler function to get the document ID as a string and have it inserted into the vc when the segue is performed, just like how it's there when the vc is instantiated.
Here is the function :
func getTheEventDocID(completion: #escaping ((String?) -> ())) {
documentListener = db.collection(Constants.Firebase.schoolCollectionName).whereField("event_name", isEqualTo: selectedEventName ?? navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.documentsID = querySnapshot!.documents.map { document in
return EventDocID(docID: (document.documentID) as! String)
}
let fixedID = "\(self.documentsID)"
let substrings = fixedID.dropFirst(22).dropLast(3)
let realString = String(substrings)
completion(realString)
}
}
}
I thought either selectedEventName or navigationItem.title would get the job done and provide the value when I used the function in the data transfer function which I will show now :
//MARK: - Data Transfer From Algolia Search to School Event Details
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
}
}
}
}
But it ends up showing nothing when a search result is clicked which is pretty upsetting, I can't understand why they're both empty values when I declared them in the SchoolEventDetailsVC. I tried to force unwrap selectedEventName and it crashes saying there's a nil value and I can't figure out why. There's actually a lot more to the question but I just tried to keep it short so people will actually attempt to read it and help since nobody ever reads the questions I post, so yeah thanks in advance.

I'm a litte confused what the otherVC is, which sets a property of itself in the getTheEventDocID, whilste in the closure you set the properties of self, which is a different controller. But never mind, I hope you know what you are doing.
Since getTheEventDocID runs asynchronously, the view will be loaded and displayed before the data is available. Therefore, viewDidLoad does not see the actual data, but something that soon will be outdated.
So, you need to inform the details view controller that new data is available, and refresh it's user interface. Something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
vc.updateUI()
}
}
}
}
and in the destination view controller:
class SchoolEventDetailsViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI () {
navigationItem.title = selectedEventName
// and so on
}
}

Ok so I decided to attempt a workaround and completely ditched the getTheEventDocID() method because it was just causing me stress. So I decided to ditch Firebase generated document IDS and just use 10 digit generated ids from a function I made. I also figured out how to add that exact same 10 digit id in the Algolia record by just storing the random 10 digit id in a variable and using it in both places. So now instead of using a query call to grab a Firebase generated document ID and have my app crash everytime I click a search result, I basically edited the Struct of the Algolia record and just added an eventDocID property that can be used with hits.hitSource(at: indexPath.row).eventDocID.
And now the same way I added the other fields to the vc by segue data transfer, I can now do the same thing with my document ID because everything is matching :).

Related

Swift, segue information passed won't show

I'm trying to get some information to pass to another view controller. I did the segue and nothing is showing. I'm using an external class to organize the information. But I'm not sure why it's not working.
first view controller:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toLocationVC" {
let lVC = segue.destinationViewController as! LocationViewController
lVC.locationImage?.image = locations[locationSelection].image;
lVC.nameLabel?.text = locations[locationSelection].name;
lVC.descriptionTextView?.text = locations[locationSelection].desc;
}
second view:
var selectedLocation : Location?;
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
locationImage.image = selectedLocation!.image
nameLabel.text = selectedLocation!.name
descriptionTextView.text = selectedLocation!.desc
}
and this is the class Location:
class Location {
var image : UIImage
var name : String
private var description : String
var desc : String {
return description + "\n\n\nThis Description and Images Provided by http://www.travel.usnews.com"
}
init(name : String, image : UIImage, description: String) {
self.name = name;
self.image = image;
self.description = description;
}
}
I've tried changing some of the code around, but nothing seems to work.
Obviously you override the information you just set prepareForSegue() in your second view controller's viewDidLoad() method.
Just remove the following code from your viewDidLoad() and it should be working (if this is actually the correct segue and all data are set):
locationImage.image = selectedLocation!.image
nameLabel.text = selectedLocation!.name
descriptionTextView.text = selectedLocation!.desc
(And I got the feeling that the selectedLocation is nil (= not set) in the viewDidLoad().)
I would rewrite the segue as follows:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toLocationVC" {
if let lVC = segue.destinationViewController as? LocationViewController {
lVC.selectedLocation = locations[locationSelection]
}
}
Then set a breakpoint inside the if let lVC = segue...{ block to see if it's ever executed. Step through the code and use the po {variable name here} command in the debugger to look into each variable.
You shouldn't be setting anything in the viewDidLoad() function any longer like Mischa suggested. So delete those assignments.
If this answer doesn't help, I think you'll need to update your code listings to include more information. We can't see exactly where a lot of these variables are declared, or if the second view is the correct class.

How to update user interface on Core Data?

I have an app with UITableView, Core Data and NSFetchedResultsController as well. I have passed data to the DetailViewController. And I can delete them from the DetailViewController! In the Apple's iOS Notes app, you can see such as functions as I wanted! When you delete a notes from the DetailViewController ( for example ), object deleted and Notes app automaticlly shows the next or previos notes! I want to create such as function. How update user interface after deleted current object? Here's my codes! Thanks `
import UIKit
import CoreData
class DetailViewController: UIViewController {
#IBOutlet weak var containerLabel: UILabel!
var retrieveData:NSManagedObject!
var managedObjectContext:NSManagedObjectContext!
var manager:Manager!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.containerLabel.userInteractionEnabled = false
self.containerLabel.textColor = UIColor.redColor()
self.containerLabel.alpha = 0
UIView.animateWithDuration(2.5) { () -> Void in
self.containerLabel.alpha = 1
}
if let demo = self.retrieveData.valueForKey("titleField") as? String {
self.containerLabel.text = demo
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func backToMain(sender: AnyObject) {
// Back to the MainTableViewController
self.dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func trashButton(sender: AnyObject) {
self.managedObjectContext.deleteObject(retrieveData)
do {
try self.managedObjectContext.save()
} catch {
}
self.dismissViewControllerAnimated(true, completion: nil)
}
`
If I have 5 items on the list like so:
When I select fourth item from the list ( for example ). And detailVC shows me selected item like this:
And I want to delete them. When I delete "Four" and then my containerLabel.text shows previous objects from the list. They're after "Four" is deleted, "Three","Two" and "One" as well. After "One" is deleted my containerLabel.text shows strings
But I have left single object called as "Five"
My problem is "Five"! I can't delete it. Example: In iOS Notes App, if you have five objects on the list like my demo app. When you select fourth object from the list ( for example ). And begin deleting them, after "Four" is delete iOS Notes App shows "Five". And "Five" ( last object on the list ) is deleted and then iOS Notes App shows "Three", "Two" and "One". Maybe problem line is here:
if index != 0 {
self.retrieveData = fetchedObject[index! - 1]
} else {
self.retrieveData == fetchedObject[0]
}
Let's take the easy (but not so elegant) route here. You'll have to pass over all the fetched objects to the detail VC like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "yourSegueIdentifier"{
if let destinationVC = segue.destinationViewController as? DetailViewController{
destinationVC.managedObjectContext = yourContext
destinationVC.retrieveData = yourManagedObject
destinationVC.arrayOfFetchedObjects = yourFetchedResultsController.fetchedObjects
//pass over other data...
}
}
}
Then, in your detailVC, write a method that will be executed when you press the delete button. Something like this:
#IBAction func trashButton(sender: AnyObject) {
//make sure you have an array with YourObjects
guard let fetchedObjects = arrayOfFetchedObjects as? [YourObjectType] else {return}
//get index of the shown object in the array of fetched objects
let indexOfObject = fetchedObjects.indexOf(retrieveData)
//delete the object from the context
self.managedObjectContext.deleteObject(retrieveData)
do {
try self.managedObjectContext.save()
//delete the object from the fetchedObjects array
fetchedObjects.removeAtIndex(indexOfObject)
} catch {
}
//get the object that should be shown after the delete
if indexOfObject != 0{
//we want the object that represents the 'older' note
retrieveData = fetchedObjects[indexOfObject - 1]
updateUserInterface(true)
}
else{
//the index was 0, so the deleted object was the oldest. The object that is the oldest after the delete now takes index 0, so just use this index. Also check for an empty array.
if fetchedObjects.isEmpty{
updateUserInterface(false)
}
else{
retrieveData = fetchedObjects[0]
updateUserInterface(true)
}
}
}
func updateUserInterface(note: Bool){
switch note{
case true:
//update the user interface
if let demo = retrieveData.valueForKey("titleField") as? String {
self.containerLabel.text = demo
}
case false:
self.containerLabel.text = "no more notes"
}
}
You either need to pass the details view controller
A list of all managed objects and an index for where in the list to start
A current managed object and a callback to get the next object
In order for it to have enough information to do what you want. The callback approach is nicest and is a simple form of delegate, where your master view controller is the delegate supplying the extra data.

Query for data to add data to prepareForSegue function

I am attempting to query for data from a Parse table to add it to a prepareForSegue function. But once I go into the newViewController the label is blank. Here's my line of code.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "marathonDetail"){
var upcoming: marathonDetailViewController = segue.destinationViewController as! marathonDetailViewController
let indexPath = self.marathonsTableView.indexPathForSelectedRow!
let currentCell = marathonsTableView.cellForRowAtIndexPath(indexPath) as! marathonTableViewCell
let marathonEvents = currentCell.marathonName.text
upcoming.nameMarathon = marathonEvents
self.marathonsTableView.deselectRowAtIndexPath(indexPath, animated: true)
var query = PFQuery(className: "marathons")
query.whereKey("marathonName", equalTo: marathonEvents!)
query.findObjectsInBackgroundWithBlock{
(marathonPickeds: [PFObject]?, error: NSError?) -> Void in
if (error == nil){
if let marathonPicked = marathonPickeds as? [PFObject]?{
for marathonPicked in marathonPickeds!{
var selectedDescription = marathonPicked.description
upcoming.marathonDescription = selectedDescription
print(selectedDescription)
}
}
}else {
print(error)
}
}
}
}
The marathonsEvents= currentCell.marathonName.text works well but the marathonDescription is blank.
Any advice? I am using Parse as my backend XCODE 7, and swift
You're performing a network call on a background thread so by the time its finished you've already completed the segue. What you probably want to do is:
Pull that query logic out into a separate class, get it out of your view controllers.
In this instance perform the request in the view controller that is being pushed to. You can start it in viewWillAppear and refresh your view when its finished. It looks like it has all the information it needs to perform the request using just the marathonEvents.

Making a prepareForSegue wait till after a Realm database write is completed

In my program when a button is pressed I am adding information to a database, including creating invoice number then calling a segue to a new view controller. When the new view controller is called I'd like to pass along that invoice number. Everything works fine, I can pass along sample data no problem. However, it appears that "override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject!) {}" is being called before my button (upon initialization of the view controller?), so I am passing along a blank value. How can I make my prepareForSegue wait till after my button is pressed? Here is the code I currently have.
#IBAction func createInvoice(sender: AnyObject) {
let realm = Realm()
let invoicepull = Invoice()
let invoicecount = realm.objects(Invoice)
let invoicenraw = invoicecount.count
let a = 100
let invoicenumber = a + invoicenraw
var invoicefile = Invoice()
invoicefile.inumber = invoicenumber
invoicefile.cnumber = clientcombo.stringValue
invoicefile.cost = owed.doubleValue
invoicefile.paid = paid.doubleValue
invoicefile.sevicecode = service.stringValue
invoicefile.dateofservice = NSDate()
// Save your object
realm.beginWrite()
realm.add(invoicefile)
realm.commitWrite()
//Sent notification
performSegueWithIdentifier("cinvoiceseuge", sender: nil)
println("Inside Action")
println(invoicenumber)
dismissViewController(self)
}
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "cinvoiceseuge") {
//Checking identifier is crucial as there might be multiple
// segues attached to same view
var detailVC = segue.destinationController as! invociegenerator;
detailVC.toPass = invoicenumber
println("Inside Sugue")
println(invoicenumber)
}
}
Update: I belive this is an issue with the Realm database causing it to behave unexpectedly. If I remove all realm code, the program works as expected and I can pass a static dummy value.
invoicenumber in createInvoice() is a local variable and invoicenumber in prepareForSegue() seems to be an instance variable. is it what you expected?

Realm causing program to behave unexpectedly. (OS X and Swift)

So I am creating a fairly simple program using Realm as my database. I am fairly new to programing in Swift (or any OS X or iOS environment.) In my program when a button is pressed IBAction func createInvoice I want a few things to happen, I want to count the previous rows in the database and create an invoice number, I want to write new data to the database and I want to call a new view and view controller and pass along the invoice number. My code works except for one thing when using Realm the new view controller is called (override func prepareForSegue) before the invoice number is created so a 0 value is passed along to the new view controller.
If I create a dummy invoice number value such as let invoicenumber = 42 everything works perfectly. It seems that Realm is causing things to happen 'out of order' How can I make the veiwcontroller wait for a value before loading?
#IBAction func createInvoice(sender: AnyObject) {
let realm = Realm()
let invoicepull = Invoice()
let invoicecount = realm.objects(Invoice)
let invoicenraw = invoicecount.count
let a = 100
let invoicenumber = a + invoicenraw
var invoicefile = Invoice()
invoicefile.inumber = invoicenumber
invoicefile.cnumber = clientcombo.stringValue
invoicefile.cost = owed.doubleValue
invoicefile.paid = paid.doubleValue
invoicefile.sevicecode = service.stringValue
invoicefile.dateofservice = NSDate()
// Save your object
realm.beginWrite()
realm.add(invoicefile)
realm.commitWrite()
//Sent notification
performSegueWithIdentifier("cinvoiceseuge", sender: nil)
println("Inside Action")
println(invoicenumber)
dismissViewController(self)
}
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "cinvoiceseuge") {
//Checking identifier is crucial as there might be multiple
// segues attached to same view
var detailVC = segue.destinationController as! invociegenerator;
detailVC.toPass = invoicenumber
println("Inside Sugue")
println(invoicenumber)
}
}
If createInvoice is happening on a different thread than prepareForSegue, you'll have to refresh the realm (Realm().refresh()) before accessing your invoicenumber variable (which I assume is of type RealmSwift.Object).
I have solved this issue, thanks to the help of #Shmidt by using Realm's built in notification center. To use the notifications you can use this basic structure.
var notificationToken: NotificationToken?
deinit{
let realm = Realm()
if let notificationToken = notificationToken{
realm.removeNotification(notificationToken)
}
}
override func viewDidLoad() {
super.viewDidLoad()
let realm = Realm()
notificationToken = realm.addNotificationBlock { note, realm in
println("The realm is complete")
}
...
}
One small other error in my code was let invoicenumber = a + invoicenraw I needed to drop the let as it is a variable and not a constant.