performSegue(withIdentifier) not being called - ios

For some reason my segue is not being performed. Here is my storyboard setup.
This code is being called in viewDidAppear of my root view controller. In the XCode debugger it says that the fetch request returned 0 results before failing on the last line due to unexpectedly finding nil. If it returned 0 results, why wouldn't my segue be performed?
var fetchResult: [User] = []
do {
// Look for User entities in Core Data
fetchResult = try context.fetch(User.fetchRequest()) as! [User]
} catch {
fatalError("Fetch error")
}
if fetchResult.count == 0 {
// New user, calculate TDEE
performSegue(withIdentifier: "toTDEE", sender: self)
}
if fetchResult.count > 1 {
// ERROR: too many users
fatalError("fetch count > 1")
}
let user = fetchResult.first! as User

The call to performSegue(withIdentifier:,sender:) doesn't block the control flow. It's an asynchronous call that will make sure that UIKit eventually presents the new view controller. The code in your method will continue to execute, however.
As a result, it will encounter the last line let user = fetchResult.first! as User immediately afterwards. This will crash as fetchResult.first results is an optional and you force-unwrap it even though it is nil (the fetchResult is empty, after all). And this crash will happen even before UIKit has even started to present the new view controller.
As a general rule of thumb you should always use optional binding instead of force-unwrapping optionals.
There's excellent reading about this topic in this answer: https://stackoverflow.com/a/32170457/10165733. I recommend you have a look at it.

here is your updated RootViewController code:
//Changed back to this method
override func viewDidAppear(_ animated: Bool) {
setup()
}
func setup() {
var fetchResult: [User] = []
do {
// Look for User entities in Core Data
fetchResult = try context.fetch(User.fetchRequest()) as! [User]
} catch {
fatalError("Fetch error")
}
//Modified this with single if conditions
if fetchResult.count == 0 {
// New user, calculate TDEE
performSegue(withIdentifier: "toTDEE", sender: self)
} else if fetchResult.count > 1 {
// ERROR: Too many users
fatalError("fetch count > 1")
} else {
//if fetchResult.count == 1
if let user = fetchResult.first { // New Edit Here
if !user.didFinishSetup {
// Didn't finish setup, re-calculate TDEE
performSegue(withIdentifier: "toTDEE", sender: self)
}
}
}
}
And HERE is your updated project for more info.

Related

home page jump over login feature in Swift

I am pretty new to the swift, I am trying to implement the feature that when the user logged in, they will get directly to the home page rather than the login page every time they reopen the app.
I took the reference to the tutorial: https://www.youtube.com/watch?v=gjYAIXjpIS8&t=146s. and I implemented the is logged in boolean checking as he did, but I somehow encounter the trouble reopen the homepage while logged in. I have an error message:[Presentation] Attempt to present <UITabBarController: 0x7fa68102ea00> on <IFTTT.ViewController: 0x7fa67fe0c150> (from <IFTTT.ViewController: 0x7fa67fe0c150>) whose view is not in the window hierarchy.
This is how my login page controller class:(which is the entry point when opening the app) I tried present as the tutorial and performsegue and both shows up the same error message above
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if isLoggedIn() {
performSegue(withIdentifier: "logInJump", sender: nil)
}
}
fileprivate func isLoggedIn() -> Bool {
print("logged in status: \(UserDefaults.standard.bool(forKey: "isLoggedIn"))")
return UserDefaults.standard.bool(forKey: "isLoggedIn")
}
#IBAction func signInButton(_ sender: Any) {
print("sign in tapped")
if let appURL = URL(string: "http://vocation.cs.umd.edu/flask/register") {
UIApplication.shared.open(appURL) { success in
if success {
print("The URL was delivered successfully.")
} else {
print("The URL failed to open.")
}
}
} else {
print("Invalid URL specified.")
}
}
}
// button broder width
#IBDesignable extension UIButton {
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
}
get {
return layer.cornerRadius
}
}
#IBInspectable var borderColor: UIColor? {
set {
guard let uiColor = newValue else { return }
layer.borderColor = uiColor.cgColor
}
get {
guard let color = layer.borderColor else { return nil }
return UIColor(cgColor: color)
}
}
}
and this is my messy story board and segues :
story board
I tried adding a navigation controller that the app entry point gets in there and it performs the isLoggedIn the same as the view controller class did, but it also has the same error.
Can someone walk me through how to fix it or any other better techniques? I felt like I am blind since I just get into the study of swift. Thank you!
You need to thread your request to perform the segue. Because your calling performSegue in the viewDidLoad() what happens is that your call is being called before everything is loaded, so you need to introduce some lag.
Threads are sometimes called lightweight processes because they have
their own stack but can access shared data. Because threads share the
same address space as the process and other threads within the
process, the operational cost of communication between the threads is
low, which is an advantage
An asynchronous function will await the execution of a promise, and an
asynchronous function will always return a promise. The promise
returned by an asynchronous function will resolve with whatever value
is returned by the function
Long story short, you need to wait for everything to be loaded into memory and if you're calling functions from the main stack/thread e.g. viewDidLoad() then there is a good chance that it hasn't been loaded into memory yet. Meaning, logInJump segue doesn't exist at that point in that view controller, thus your error.
The other possibility is you don't have the right view/segue ID but that should've thrown a different error.
Also, change the sender from nil to self. Actually this isn't necessary but I've always used self over nil
// change to desired number of seconds should be higher then 0
let when = DispatchTime.now() + 0.2
if isLoggedIn() {
DispatchQueue.main.asyncAfter(deadline: when) {
self.dismiss(animated: true, completion: nil)
self.performSegue(withIdentifier: "logInJump", sender: nil)
}
}

swift conditionally call unwind segue using shouldPerformSegue

I have two view controllers: Step3VC (we'll call this 'A') and Step3AddJobVC (we'll call this 'B'). I'm trying to validate some data on 'B' before performing an unwind segue back to 'A'.
'B' takes some user input, and I want to verify that the user input is not duplicate. The user is making a list of chores, and so duplicate names won't work. When the user taps 'save', the unwind segue performs, and the data is added to an array.
Here's the problem: the array is on 'A', but the validation needs to happen on 'B' before 'A' gets called. How do I do that?
What I've tried:
I've tried using shouldPerformSegue in 'B', but the array comes back blank []. So that's no good. Here's the code from 'B':
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
print("identifier is: ", (identifier))
print("sender is: ", (sender)!)
let newVC = Step3VC()
print(newVC.dailyJobs)
return false
}
So then I tried putting the validation into 'A' during the unwind segue...
#IBAction func unwindToStep3VC(sender: UIStoryboardSegue) {
let sourceVC = sender.source as! Step3AddJobVC
let updatedJob = sourceVC.job
// check for duplicate names
for name in dailyJobs {
print(name.name)
if name.name.lowercased() == (sourceVC.jobTextField.text?.lowercased()) { // check to see if lowercased text matches
print("error")
// call alert function from sourceVC
sourceVC.duplicateNameCheck()
return
}
}
if let selectedIndexPathSection = jobsTableView.indexPathForSelectedRow?.section { // if tableview cell was selected to begin with
// Update existing job
if selectedIndexPathSection == 0 {
let selectedIndexPathRow = jobsTableView.indexPathForSelectedRow
dailyJobs[(selectedIndexPathRow?.row)!] = updatedJob!
jobsTableView.reloadData()
} else if selectedIndexPathSection == 1 {
let selectedIndexPathRow = jobsTableView.indexPathForSelectedRow
weeklyJobs[(selectedIndexPathRow?.row)!] = updatedJob!
jobsTableView.reloadData()
}
} else {
// Add a new daily job in the daily jobs array
let newIndexPath = IndexPath(row: dailyJobs.count, section: 0)
dailyJobs.append(updatedJob!)
jobsTableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
...but it gave the error:
popToViewController:transition: called on <ToDo_App.SetupNavController 0x7fcfd4072c00> while an existing transition or presentation is occurring; the navigation stack will not be updated.
If I pull out the 'if' validation code, the unwind segue works properly. The data is transferred and does the right thing. The problem is that if the user enters duplicate entries, I can't figure out how to stop them.
This is my code for checking if user input is duplicate:
// check for duplicate names
for name in dailyJobs {
print(name.name)
if name.name.lowercased() == (sourceVC.jobTextField.text?.lowercased()) { // check to see if lowercased text matches
print("error")
// call alert function from sourceVC
sourceVC.duplicateNameCheck()
return
}
}
What am I missing? Is there a better way to do this? How do I call the variables from 'A' while I'm in 'B' to perform my validation BEFORE the unwind segue is called / performed?
You are trying to validate the things in shouldPerformSegue which is the right place, the thing which you are doing wrong is recreating new object of Step3VC and trying to access dailyJobs which is never set with value.
let newVC = Step3VC()
print(newVC.dailyJobs)
What you have do is pass dailyJobs form VC A to VC B while presenting VC B and then check if the data is duplicate or not in shouldPerformSegue.
Your code have to look like:
class VCA: UIViewController {
var dailyJobs = getDailyJobsFromServer()
#IBAction segueToVCB(sender: UIButton) {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vcB = sb.instantiateViewController(withIdentifier: "VCB") as! VCB
vcB.dailyJobs = dailyJobs
self.present(vcB, animated: true, completion: nil)
}
}
class VCB: UIViewController {
var dailyJobs: //DataType
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
//Here you do comparision with dailyJobs
if dailyJobs == userInput {
}
return false
}
}

Swift 3 pass data through completion with userID

I'm trying to run a check on Firebase to see if a user exists, then I need to check for specific vales before continuing. I currently have this:
func myFirebaseNetworkDataRequest(finished: () -> Void) {
if let user = FIRAuth.auth()?.currentUser {
self.userUUID = (user.uid)
getUser(userUUID: self.userUUID)
finished()
}
}
In my view did load:
myFirebaseNetworkDataRequest {
// perform further operations here after data is fetched
if AppState.sharedInstance.user == true {
//present 1st view controller
} else {
//present 2nd view controller
}
In my "getUser" function:
func getUser(userUUID: String) {
let userFacebookRef = FIRDatabase.database().reference(withPath: "users").child(userUUID)
//The rest of the Firebase function.
AppState.sharedInstance.user == results.active
//active is = to true
What currently happens is that if presents the 2nd view controller because firebase hasent finished yet. I realize I need a block because firebase is already asnyc but how do I send userUUID through the closure/block?
You can have your closure parameter have an parameter of it's own. Something like,
func myFirebaseNetworkDataRequest(finished: (_ isAuthenticated: Bool) -> Void)
Then in your viewDidLoad, you would make your request...
open override func viewDidLoad() {
self.myFirebaseNetworkDataRequest( (isAuthenticated) -> {
if isAuthenticated {
// Present VC1
} else {
// Present VC 2
}
}
}

UITableView not updating when switching between tabs

Preface: I've tried adding tableView.reloadData() to viewWillAppear (...and viewDidLoad, viewDidAppear, etc.) of the UITableViewController that's not updating. I threw in setNeedsDisplay for S's & G's, too.
I have a UITabBarController with 3 tabs on it. Each tab is a TableViewController is backed by Core Data and is populated with NSManagedObjects from one NSManagedObjectContext.
In TableViewController1 I make changes to the cells, the tableView reloads properly and reflects the changes. If I click the tab for TableViewController2, the changes made on TVC1 aren't reflected.
The changes made on TVC1 are persisting between launches, as I see them on TVC2 when I close the app and relaunch it.
What am I missing? Any ideas would be greatly appreciated.
Update
Here's the code in question:
func markFavorite(sender: AnyObject) {
// Store the sender in case you need it later. Might not need this.
clickedFavoriteButton = sender as! UIButton
if resultsSearchController.active {
let indexPath = sender.tag
let selectedSound = self.filteredSounds[indexPath]
print("markFavorite's sender tag is \(indexPath)")
if selectedSound.favorite == 1 {
selectedSound.favorite = 0
} else {
selectedSound.favorite = 1
}
saveManagedObjectContext()
} else {
let indexPath = sender.tag
let selectedSound = self.unfilteredSounds[indexPath]
print("markFavorite's sender tag is \(indexPath)")
if selectedSound.favorite == 1 {
selectedSound.favorite = 0
} else {
selectedSound.favorite = 1
}
saveManagedObjectContext()
}
}
func saveManagedObjectContext() {
if managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
} catch {
// catch error here
}
}
self.tableView.reloadData()
}
You should always use a NSFetchedResultsController to display Core Data items in a table view. It comes with delegate methods that update your table as the underlying data changes (even before saving).
To get started, examine the Xcode template (Master-Detail) implementation. Once you get the hang of it you will love it. Everything works pretty much out of the box.
You may have to trigger context.save() manually because core-data isn't saving the data right away.
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
runOnMain() {
do {
try! self.context.save()
} catch let error as NSError {
//Handle any upcoming errors here.
}
}
Its important to run the method on the main thread otherwise you will get an error.
this method should do the job:
func runOnMain(block: dispatch_block_t) {
if NSThread.isMainThread() {
block()
}else{
dispatch_sync(dispatch_get_main_queue(), block)
}
}
Please let me know if this approach worked for you.
You should not try to reload data at any point in view controller lifecycle. Instead create delegates for each tab bar controller, set them properly and call delegate methods only when something really change in your data source. If you are not familiar with delegation you can learn more about it here

Firebase removing observers

I have a problem removing a Firebase observer in my code. Here's a breakdown of the structure:
var ref = Firebase(url:"https://MY-APP.firebaseio.com/")
var handle = UInt?
override func viewDidLoad() {
handle = ref.observeEventType(.ChildChanged, withBlock: {
snapshot in
//Do something with the data
}
}
override func viewWillDisappear(animated: Bool) {
if handle != nil {
println("Removed the handle")
ref.removeObserverWithHandle(handle!)
}
}
Now when I leave the viewcontroller, I see that "Removed the handle" is printed, but when I return to the viewcontroller, my observer is called twice for each event. When I leave and return again, it's called three times. Etc. Why is the observer not being removed?
I do also call ref.setValue("some value") later in the code, could this have anything to do with it?
Thought I was having this bug but in reality I was trying to remove observers on the wrong reference.
ORIGINAL CODE:
let ref: FIRDatabaseReference = FIRDatabase.database().reference()
var childAddedHandles: [String:FIRDatabaseHandle] = [:]
func observeFeedbackForUser(userId: String) {
if childAddedHandles[userId] == nil { // Check if observer already exists
// NOTE: - Error is caused because I add .child(userId) to my reference and
// do not when I call to remove the observer.
childAddedHandles[userId] = ref.child(userId).observeEventType(.ChildAdded) {
[weak self] (snapshot: FIRDataSnapshot) in
if let post = snapshot.value as? [String:AnyObject],
let likes = post["likes"] as? Int where likes > 0 {
self?.receivedFeedback(snapshot.key, forUserId: userId)
}
}
}
}
func stopObservingUser(userId: String) {
// THIS DOES NOT WORK
guard let cah = childAddedHandles.removeValueForKey(userId) else {
print("Not observing user")
return
}
// Error! I did not add .child(userId) to my reference
ref.removeObserverWithHandle(cah)
}
FIXED CODE:
func stopObservingUser(userId: String) {
// THIS WORKS
guard let cah = childAddedHandles.removeValueForKey(userId) else {
print("Not observing user")
return
}
// Add .child(userId) here
ref.child(userId).removeObserverWithHandle(cah)
}
Given it's April 2015 and the bug is still around I'd propose a workaround for the issue:
keep a reference of the handles (let's say in a dictionary and before initiating a new observer for the same event type check if the observer is already there.
Having the handles around has very low footprint (based on some official comments :) ) so it will not hurt that much.
Observers must be removed on the same reference path they were put upon. And for the same number of times they were issued, or use ref.removeAllObservers() for each path.
Here's a trick I use, to keep it tidy:
var fbObserverRefs = [FIRDatabaseReference]() // keep track of where observers defined.
...then, put observers in viewDidLoad():
fbObserverRefs.append(ref.child("user/\(uid)"))
fbObserverRefs.last!.observe(.value, with: { snap in
// do the work...
})
...then, in viewWillDisappear(), take care of removing any issued observers:
// Only true when popped from the Nav Controller stack, ignoring pushes of
// controllers on top.
if isBeingDismissed || isMovingFromParentViewController {
fbObserverRefs.forEach({ $0.removeAllObservers() })
}

Resources