ios swift: #IBAction handling to avoid segue - ios

I have 2 views, first to enter some data and second to fill in more details based on the data the user entered on the first view.
Therfore I have a function like:
#IBAction func addNewCardSet(sender: AnyObject) { ...
let givenCardSetName = newCardSetName.text
if givenCardSetName != "" {
... save routine ...
}else{
updateLabel("Please fill in a name")
}
I also added a segue to the addNewCardSet Button to do a segue to the second view. What happens now is that if the user doesn't enter a name, I can see the message label saying "Please fill in the name" but one little moment later the segue takes place and send the user to the next view without any saved data...
What can I do to "allow" the segue only, if my save method took place with no errors and it is the time to do the segue?

You can implement shouldPerformSegueWithIdentifier method:
override func shouldPerformSegueWithIdentifier(identifier: String?, sender: AnyObject?) -> Bool {
if let ident = identifier {
if ident == "YOUR IDENTIFIER HERE" {
let givenCardSetName = newCardSetName.text
if givenCardSetName != "" {
return true
}else{
return false
}
}
}
return true
}

Related

How do I stop a segue from going through programmatically in Swift?

Here is the code:
#IBAction func loginTapped(_ sender: Any) {
let email = emailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let password = passwordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
if error != nil {
self.errorLabel.text = error!.localizedDescription
self.errorLabel.alpha = 1
print(error!.localizedDescription)
}
else {
self.performSegue(withIdentifier: "loginSegue", sender: nil)
print("User is signed in with Firebase.")
}
}
}
I have a segue, loginSegue, connected from the login button to the homeViewController. Within in the if error statement I would like to stop the segue from going through because the user has not signed in. The goal here is not allow the user to go forward if they get an error. Is there an "opposite" to the performSegue(withIdentifier: String, sender: Any?) ?
First , there is no "opposite" to performSegue(withIdentifier: String, sender: Any?).
But the issue is not about this. I think you wired the segue from the login button and gave it an identifier. If you wire a segue from a button directly the button is always going to execute that segue. Doing some operations in the button's action does not effect.
You need to wire a segue from FirstVc to SecondVc (not from the button) and then give the segue an identifier. Then, from the button's action you can check if there is no error and call performSegue(withIdentifier: String, sender:) passing your segue's identifier.
I think your button is connected to perform segue in storyboard. So your button has two actions - one from storyboard to perform segue and second in your code. Just remove the connection from storyboard and connect only UIViewControllers not with your button.
You could override the shouldPerformSegue(withIdentifier:,sender:) method and return false if the login fails and you don't want to perform the segue. Here's an example
override func shouldPerformSegue(withIdentifier identifier: String?, sender: Any?) -> Bool {
if let ident = identifier {
if ident == "YourIdentifier" {
if loginSuccess != true {
return false
}
}
}
return true
}

unwind segue cancel upon textbox value equal to nil

I would like to cancel an unwind segue if the textbox values are equal to nil.
below is the current code. I used storyboard to connect the save button (saves values of text into database). I was thinking about using poptorootviewcontroller to ensure the textbook has values before performing segue. is their any way to do this with current code?
code inside of mainView to unwind back to-
#IBAction func unwind(_ segue: UIStoryboardSegue){
print("Back in TableView")
}
button on childView perform unwind segue-
#IBAction func saveAddress(_ sender: UIButton) {
// save values to dictionary save dictionary to firebase under user, uid, addresses
// uialert controller if fields are not completed do not allow segue if fields are not complete
// perform segue to addresstableview
let addy1: String = address1Txt.text!
let addy2: String = address2Txt.text!
let aptNum: String = aptTxt.text!
let city: String = cityTxt.text!
let state: String = stateTxt.text!
let zip: String = zipTxt.text!
// add UIAlert controller for address field == nothing
if addy1.isEmpty && aptNum.isEmpty && city.isEmpty && state.isEmpty && zip.isEmpty
{
//add UIAlert Controller DO NOT PERFORM SEGUE IF TRUE
}
Address.sharedInsance.typedNewAddress = addy1 + "," + addy2 + "," + aptNum + "," + city + "," + state + "," + zip
print("address save print",addy1, addy2, aptNum, city, state, zip)
let key = ref.child("address").childByAutoId().key
let setter = false
let addyDict = ["address line 1":addy1,"address line 2":addy2,"apt Number":aptNum,"city":city,"state":state,"zip":zip,"keyID": key, "setter": setter] as [String : Any]
let userID = Auth.auth().currentUser?.uid
let childUpdates = ["/address/\(key)": addyDict]
ref.child("users").child(userID!).updateChildValues(childUpdates)
}
Given that I don't see you performing the segue in your #IBAction, we'll have to presume that you hooked your button up to both an #IBAction and an unwind segue. You should remove the segue from the button's "Connections Inspector" on the last tab on the panel on the right and instead, create an unwind segue from the view controller, itself, to the exit outlet:
You can then select that segue in the panel on the left, click on the "attributes inspector" for that segue, give it a storyboard identifier:
Then you can programmatically perform the segue, using that storyboard identifier, only if appropriate in your remaining #IBAction, e.g.
#IBAction func didTapDoneButton(_ sender: Any) {
// if the fields are `nil` or have a length of zero characters, show warning
// and just `return` without ever performing unwind segue
guard let firstName = textFieldFirstName.text?.trimmingCharacters(in: .whitespacesAndNewlines),
let lastName = textFieldLastName.text?.trimmingCharacters(in: .whitespacesAndNewlines),
firstName.count > 0, lastName.count > 0 else {
let alert = UIAlertController(title: nil, message: "Please fill in all fields", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
return
}
// do something with firstName and lastName
save(firstName: firstName, lastName: lastName)
// now perform unwind segue
performSegue(withIdentifier: "unwindHome", sender: sender)
}
Having shown you that, I believe the more robust approach is to not let the user tap the "done"/"save" button until the required fields are entered. Bottom line, rather than handling the error situation, design a UI where it is impossible to make such an error.
So, some of the key aspects of that would include:
Dim/disable the "done"/"save" button;
Set the "placeholder text" for the text fields to indicate that the field is required. E.g. a placeholder of "First name (required)" so that the user knows it's required.
Add editingChanged method for the various text fields that enables/disables the "done" button as appropriate:
#IBAction func editingChanged(_ sender: UITextField) {
if let firstName = textFieldFirstName.text?.trimmingCharacters(in: .whitespacesAndNewlines),
let lastName = textFieldLastName.text?.trimmingCharacters(in: .whitespacesAndNewlines),
firstName.count > 0, lastName.count > 0 {
doneButton.isEnabled = true
} else {
doneButton.isEnabled = false
}
}
Obviously hook this up to the "editingChanged" action for all of the relevant text fields.
This yields a "Done" button (the one in the upper right corner of this demo) that is disabled until there is at least some text in all of the text fields:
Bottom line, rather handling the error, prevent it in the first place.
You should override shouldPerformSegue method and return false if the textbook fields are empty.
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
}

ShouldPerformSegue Fires Twice

I have currently come across a weird problem. I have a storyboard which has viewController A with a button. From that button, I have created a segue in storyboard to viewController B. When the button is clicked, a segue is fired.
The button is a Login button, so I need to validate the login details before the segue is performed.
In ViewController A, when the button is pressed, I have following code:
#IBAction func SignInButtonPressed(_ sender: Any) {
guard let email = username.text , username.text != "" else {
return self.successLogin = false
}
guard let pass = password.text , password.text != "" else {
return self.successLogin = false
}
AuthService.instance.loginUser(email: email, password: pass) { (success) in
if success {
self.successLogin = true
} else {
self.displayAlertView(title: USER_LOGIN_FAILED_TITLE, message: USER_LOGIN_FAILED_MESSAGE)
}
}
}
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if successLogin != true && identifier == "homeSegue" {
self.displayAlertView(title: USER_LOGIN_FAILED_TITLE, message: USER_LOGIN_FAILED_MESSAGE)
return false
} else {
return true
}
}
But with this, the first click shows the alert in shouldPerformSegue, and then a second click allows the login.
Any suggestions would be helpful on how can I fix this.
Thanks
You should perform the segue inside your AuthService success closure callback, you should also remove the unwind segue from the button, the button must only execute the login logic, if the login went well then the segue must be performed but not before
AuthService.instance.loginUser(email: email, password: pass) { (success) in
if success {
self.successLogin = true
self.performSegue(withIdentifier: "homeSegue", sender: self)
} else {
self.displayAlertView(title: USER_LOGIN_FAILED_TITLE, message: USER_LOGIN_FAILED_MESSAGE)
}
}
should work now
You can run segue programmatically.
performSegue(withIdentifier: "homeSegue", sender: self)
If you need to perform custom logic, moreover postpone navigation, it is better to bind button to the action, and trigger segue you need from it.

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 property nil outside unwindToViewController

To pass data between views, I decided to use a "temporary" object that would act as the data model of my views.
var tempMedecine = TempMedecine()
var xValue = 0
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let dmController = segue.destinationViewController as? JRBDosageMainTableViewController {
dmController.tempMedecine = self.tempMedecine
}
}
#IBAction func unwindToViewController(segue: UIStoryboardSegue) {
if let dosageController = segue.sourceViewController as? JRBDosageMainTableViewController {
self.tempMedecine = dosageController.tempMedecine!
self.xValue = 10
let dosageCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 1, inSection: 0))
dosageCell?.detailTextLabel?.text = String(self.tempMedecine.dosageQuantity!) + " " + self.tempMedecine.dosageQuantityType!
}
}
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
if identifier == "saveMedecine" {
print(xValue)
guard tempMedecine.name != nil else {
Common.genericAlertController(self, title: "Error", message: "You need to define a name", preferedStyle: .Alert)
return false
}
guard self.tempMedecine.dosageQuantityType != nil else {
Common.genericAlertController(self, title: "Error", message: "You need to set a quantity", preferedStyle: .Alert)
return false
}
}
else {
return true
}
return false
}
This is some of my code from the "index" viewController where I need to tackle validation.
As you can see all of my viewControllers have a property named tempMedecine. I pass it around and update the data if needed.
The problem is that self.tempMedecine.dosageQuantityType returns nil in the shouldPerformSegueWithIdentifier method but isn't returning nil in the unwindToViewController method.
I figured there could be two instances of my TempMedecine object, but that's not the case. I also thought there might be a problem with the way I pass the tempMedecine variable between my viewControllers but the property tempMedecine.name is effectively transfered, the only difference is that this property is set in the same viewController where I want to implement validation :
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
self.tempMedecine.name = textField.text
return true
}
It's really looking like I'm working with two different scope. As soon as I leave the unwindToViewController method, I would get back to another scope where the tempMedecine variable isn't updated.
But the weird part is when I use a simple variable like xValue. If I update its value in the unwindToViewController method I get the correct value in shouldPerformSegueWithIdentifier
What am I missing? Thanks for your help.
Okay, I fixed it. What happened was that I implemented some code to reset the dataSource which is tempMedecine if the user decided to click on the Back Button in the navigation bar :
if self.isMovingToParentViewController() {
tempMedecine?.dosageQuantity = nil
tempMedecine?.dosageQuantityType = nil
}
The thing is, I never thought the issue could come from this as I can use the tempMedecine data to set the value in my tableView after unwinding to the index viewController but I totally missed the part when object are passed my reference.

Resources