Prevent View Disappearing in splitview When User Edits Pending Swift 3 - ios

I have a universal master-detail application where I present the master
and detail views side by side on an iPad in both orientations. When a
user makes changes in the detail view on an iPhone I can easily detect
changes and present an alert asking if they want to save the changes or
lose them (CoreData). On an iPad, there is no prohibition against simply clicking
in the master list thereby losing the edits.
I have placed a function in the DetailViewController viewWillDisappear
(for iPad) that raises an alert, but the compiler tells me that it does
not like the presentation of a view on a non-connected view.
Console message: Presenting view controllers on detached view
controllers is discouraged <>.
Is there a more appropriate way to handle this? Swift 3, iOS 10, Xcode 8.2.1
var hasChanged //set to true whenever edits are made
override func viewWillDisappear(_ animated: Bool) {
if UIDevice.current.model == "iPad" {
if hasChanged {
print("hasChanged (should be true) is: \(hasChanged)")
cancelUnsavedEdits()
}//if hasChanged
}//if ipad
}//viewWillDisappear
func cancelUnsavedEdits() {
if hasChanged {
let ac = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Delete Edits", style: .default, handler: { (action : UIAlertAction!) -> Void in
self.codeDismissTheKeyboard()
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
let editRecordButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.edit, target: self, action: #selector(DetailViewController.editThisRecord))
self.navigationItem.rightBarButtonItem = editRecordButton
self.navigationItem.leftBarButtonItem = nil
self.navigationItem.hidesBackButton = false
//need to remove the edits - refresh the original page
self.configureView()
}))//addAction block
ac.addAction(UIAlertAction(title: "Save Edits", style: .default, handler: { (action : UIAlertAction!) -> Void in
self.codeDismissTheKeyboard()
self.saveTheEditedRecord()
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
}))//addAction block
//ac.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
//try this - for -ipad add code in handler to reopen the fields for editing if the cancel of the cancel is chosen
ac.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (whatever) in
self.makeEntryFieldsEnabledYES()
let cancelItemButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(DetailViewController.cancelUnsavedEdits))
self.navigationItem.leftBarButtonItem = cancelItemButton
}))
//try above
self.present(ac, animated: true, completion: nil)
} else {
self.codeDismissTheKeyboard()
//for ipad
self.navigationItem.rightBarButtonItem = nil
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
}//if hasChanged
//for ipad
navigationItem.leftBarButtonItem = nil
}//cancelUnsavedEdits

I think the problem you try to present a new viewController on a viewController which is going to be removed from the view hierarchy.
Maybe a dirty fix can be to replace:
self.present(ac, animated: true, completion: nil)
to:
self.view.window.rootViewController?.present(ac, animated: true, completion: nil);

Related

Show UIAlert before unwinding segue

I'm trying to fire an alert that asks if you want to save or delete a draft after pressing cancel. I'm quite close, but I can't seem to get it right.
I'm unwinding from 'ReplyMailViewController'(ViewController A) to 'MailContentViewController'(ViewController B).
I added the following code in ViewController A to show the alert and 'hold' the segue perform:
override func shouldPerformSegue(withIdentifier identifier: String?, sender: Any?) -> Bool {
if let ident = identifier {
if ident == "cancelDraft" {
let saveDraftActionHandler = { (action:UIAlertAction!) -> Void in
NSLog("EXIT")
}
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let deleteDraftAction = UIAlertAction(title: "Delete Draft", style: .destructive, handler: nil)
alertController.addAction(deleteDraftAction)
let saveDraftAction = UIAlertAction(title: "Save Draft", style: .default, handler: saveDraftActionHandler)
alertController.addAction(saveDraftAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
return false
}
}
return true
}
The segue holds with this code, but the issue is that I can't figure out how to continue the unwind segue after pressing 'Save Draft' for example.
I also have an unwind function in View Controller B, but I can't seem to figure out how I can use this one for this task:
#IBAction func cancelToMailContentViewController(_ segue: UIStoryboardSegue) {
}
Instead of perform the segue directly you need to show your UIAlertViewController first and according to the user response execute your segue or not
#IBAction func showAlertViewController(){
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let replyAction = UIAlertAction(title: "Delete Draft", style: .destructive, handler: nil)
let replyAllAction = UIAlertAction(title: "Save Draft", style: .default) { (action) in
//Do whatever you need here
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (action) in
self.performSegue(withIdentifier: "cancelDraft", sender: action) //executing the segue on cancel
}
alertController.addAction(replyAllAction)
alertController.addAction(replyAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
After this you only need to change the unwind segue action to execute this method, and your segue will be executed if you press cancel in the UIAlertViewController via self.performSegue(withIdentifier: #<SegueIdentifier>, sender: #<sender>)
First, make the alert with two options:
class ViewController: UIViewController {
#IBAction func showAlertButtonTapped(_ sender: UIButton) {
// create the alert
let alert = UIAlertController(title: "UIAlertController", message: "Save this work?", preferredStyle: UIAlertControllerStyle.alert)
// add the actions (buttons)
alert.addAction(UIAlertAction(title: "Hell Yeah", style: UIAlertActionStyle.default, handler: nil))
alert.addAction(UIAlertAction(title: "Hell No", style: UIAlertActionStyle.cancel, handler: nil))
// show the alert
self.present(alert, animated: true, completion: nil)
}
After this, you have to make the segue and then name it (also connect it by control dragging from the view controller yellow icon to the other view controller):
After that put this your code to execute the segue:
self.performSegue(withIdentifier: ":)", sender: self)
After that you are going to execute the segue when the user responds to the alert:
if buttonTitle == "Hell Yeah" {
elf.performSegue(withIdentifier: ":)", sender: self)
}
so, in the end, your code should look like this:
class ViewController: UIViewController {
#IBAction func showAlertButtonTapped(_ sender: UIButton) {
// create the alert
let alert = UIAlertController(title: "UIAlertController", message: "Save this work?", preferredStyle: UIAlertControllerStyle.alert)
// add the actions (buttons)
alert.addAction(UIAlertAction(title: "Hell Yeah", style: UIAlertActionStyle.default, handler: nil))
alert.addAction(UIAlertAction(title: "Hell No", style: UIAlertActionStyle.cancel, handler: nil))
// show the alert
self.present(alert, animated: true, completion: nil)
if buttonTitle == "Hell Yeah" {
self.performSegue(withIdentifier: ":)", sender: self)
}
}
}

Controller being pushed twice on iPad screen

I am trying to segue to another controller by clicking on one of presented options on actionsheet. It works just fine on iPhone screens and it's being pushed to appropriate scenes, however issue occurs on iPad. I have been searching a lot for similar issue, but with no success.
#IBAction func actionSheet(_ sender: UIButton) {
let alert = UIAlertController(title: "Please select one of the options", message: nil, preferredStyle: .actionSheet)
let cancelActionButton = UIAlertAction(title: "Cancel", style: .cancel) { action -> Void in }
let recipeActionButton = UIAlertAction(title: "Get The Recipe", style: .default) { action in self.performSegue(withIdentifier: "GetRecipeID", sender: self) }
let facebookActionButton = UIAlertAction(title: "Login with Facebook", style: .default) { action in self.handleCustomFacebookLogin() }
//actions
alert.addAction(cancelActionButton)
alert.addAction(recipeActionButton)
alert.addAction(facebookActionButton)
// support ipad
if let popoverController = alert.popoverPresentationController {
popoverController.sourceView = sender
popoverController.sourceRect = sender.bounds
}
self.present(alert, animated: true, completion: nil)
}
This approach is also not working:
let viewController = UIStoryboard(name: "Detail", bundle: nil).instantiateViewController(withIdentifier: "DetailsVC") as! DetailsViewController
let recipeActionButton = UIAlertAction(title: "Get The Recipe", style: .default, handler: { action in
self.navigationController?.pushViewController(viewcontroller, animated: true)})
I am getting this warning in console when pushing from iPhone:
pushViewController:animated: called on <UINavigationController
0x7fd96a81f800> while an existing transition or presentation is
occurring; the navigation stack will not be updated.
This is not showing when I trigger action from iPad, but new controller is stacked on top.FirstController, SecondController
after clicking on getRecipe/Login button from previous screen.
You could try to perform the segue from another operation
OperationQueue.main.addOperation {
self.performSegue(withIdentifier: "GetRecipeID", sender: nil)
}

Allow User Input Before App Enters Background

I have a CoreData app with a fairly long list of data fields. When a user edits the fields but attempts to exit the DetailViewController without saving the edits, I put up an alert asking if they really want to discard the changes. This works fine, but if the user taps the home key, the edits are lost. I've tried to present an alert before the app enters the background but have been unable to delay entry into background to allow for user input. Is it possible to delay app entry into the background while waiting for user input?
Here's what I tried:
func applicationWillResignActive(_ notification : Notification) {
//this does not work - alert is too late
cancelUnsavedEdits()
//try above
}//applicationWillResignActive
The canelUnsavedEdits method is fairly straight forward:
func cancelUnsavedEdits() {
if hasChanged {
let ac = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Delete Edits", style: .default, handler: { (action : UIAlertAction!) -> Void in
self.codeDismissTheKeyboard()
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
let editRecordButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.edit, target: self, action: #selector(DetailViewController.editThisRecord))
self.navigationItem.rightBarButtonItem = editRecordButton
self.navigationItem.leftBarButtonItem = nil
self.navigationItem.hidesBackButton = false
//need to remove the edits - refresh the original page
self.configureView()
}))//addAction block
ac.addAction(UIAlertAction(title: "Save Edits", style: .default, handler: { (action : UIAlertAction!) -> Void in
self.codeDismissTheKeyboard()
self.saveTheEditedRecord()
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
}))//addAction block
//for -ipad add code in handler to reopen the fields for editing if the cancel of the cancel is chosed
ac.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (whatever) in
//print("makeEntryFieldsEnabledYES for ipad")
self.makeEntryFieldsEnabledYES()
}))
//above for ipad
self.present(ac, animated: true, completion: nil)
} else {
self.codeDismissTheKeyboard()
//add this for ipad
self.navigationItem.rightBarButtonItem = nil
//add above for ipad
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
}//if hasChanged
//this for ipad
navigationItem.leftBarButtonItem = nil
//above for iPad
}//cancelUnsavedEdits
Any guidance on a strategy to accomplish this idea would be appreciated. iOS 10, Xcode 8.1
No. Not possible.
iOS does give your app some time to clean up or save data, but not enough time for user interaction. The reasoning is that the user DID interact and wants to exit your app. Maybe save the data the user entered and present it when they return, but do not try to prevent the user from exiting.

Displaying a UIAlert before dismissing the view

My view is being dismissed through the previous button on the navigation bar. I think the correct term for that is that the view is being popped off the view stack. Now before actually dismissing the view I want to display a UIAlert asking the user to setup his/her address.
I've tried this but the UIAlert is not being show:
override func viewWillDisappear(animated: Bool) {
if let currentUser = ApiManager.sharedInstance.currentUser {
if !currentUser.hasAddress {
let alert = UIAlertController(title: "Missing address", message: "We see you're stilling missing an address, would you like set it now?", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "yes".localized, style: .Default, handler: { (alertAction) in
let newViewController = LocationViewController()
newViewController.delegate = self
self.navigationController?.pushViewController(newViewController, animated: true)
}))
alert.addAction(UIAlertAction(title: "no".localized, style: .Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
}
}
In my console the following gets printed:
2016-10-28 16:58:47.380 gaifong[17096:14515422] Warning: Attempt to present <UIAlertController: 0x7fd7037e4cb0> on <gaifong.ProfileViewController: 0x7fd7048cc000> whose view is not in the window hierarchy!
You should add leftBarButtonItem on navigationItem and make an action to leftBarButtonItem where you can handle this.
in viewDidLoad
self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: image, style: UIBarButtonItemStyle.Plain, target: self, action: #selector(self.goBack))
// Then handle the button selection
func goBack() {
if let currentUser = ApiManager.sharedInstance.currentUser {
if !currentUser.hasAddress {
let alert = UIAlertController(title: "Missing address", message: "We see you're stilling missing an address, would you like set it now?", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "yes".localized, style: .Default, handler: { (alertAction) in
// here you can pop to main controller
self.navigationController?.popViewControllerAnimated(true)
}))
alert.addAction(UIAlertAction(title: "no".localized, style: .Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
}
}

NSNotification: Attempt to present UIAlertController on ViewController whose view is not in the window hierarchy

I'm trying to show a UIAlertController in my ViewController in a function that's been called via an NSNotification. However I'm getting the error:
Attempt to present <UIAlertController: 0x7fe013d05d40> on <submarine.ViewController: 0x7fe011f20370> whose view is not in the window hierarchy!
The NSNotification is posted from a completion block (callback I guess) from something else in my UI. Because it's a callback it's failing to display. Hence I thought I'd try NSNotificationCentre to get around the problem without using the rootViewController to display the alert.
My code is:
override func viewDidAppear(animated: Bool) {
// Handle onboarding
if needsOnboarding() {
handleOnboarding() // This create the completion block that posts the NSNotification
}
NSNotificationCenter.defaultCenter().addObserver(self, selector: "showTermsAlert:", name:"showTermsAlert", object: nil)
}
func showTermsAlert(notification: NSNotification) {
let termsAlert:UIAlertController = UIAlertController(title: "Terms And Conditions", message: "Please view the terms below before accepting them.", preferredStyle: UIAlertControllerStyle.Alert)
termsAlert.addAction(UIAlertAction(title: "View Terms", style: .Default, handler: { (action: UIAlertAction!) in
UIApplication.sharedApplication().openURL(NSURL(string: "my_terms_url")!)
}))
termsAlert.addAction(UIAlertAction(title: "I Agree to the Terms", style: .Default, handler: { (action: UIAlertAction!) in
self.onboardingFinished()
}))
self.presentViewController(termsAlert, animated: true, completion: nil)
}
Has anyone got an idea why this is happening? I don't see why it's not in the window hierarchy - it's being presented from the self viewController and is created in a top-level function inside the VC.
Thanks!
EDIT: original code inside the handleOnboarding():
Library used: Onboard
func handleOnboarding() {
let secondPage = OnboardingContentViewController(title: "What's going on?", body: "Submarine routes your data through our network, around any filters and restrictions, giving you unrestricted and unmonitored internet access.", image: UIImage(named: "back"), buttonText: "Next") { () -> Void in
// do something here when users press the button, like ask for location services permissions, register for push notifications, connect to social media, or finish the onboarding process
}
secondPage.movesToNextViewController = true
let thirdPage = OnboardingContentViewController(title: "Terms of Use", body: "You must agree to our Terms of Use to use Submarine.\nIf you don't, please close Submarine.", image: UIImage(named: "back"), buttonText: "View Terms") { () -> Void in
let termsAlert:UIAlertController = UIAlertController(title: "Terms And Conditions", message: "Please view the terms below before accepting them.", preferredStyle: UIAlertControllerStyle.Alert)
termsAlert.addAction(UIAlertAction(title: "View Terms", style: .Default, handler: { (action: UIAlertAction!) in
UIApplication.sharedApplication().openURL(NSURL(string: "my_policy_url")!)
}))
termsAlert.addAction(UIAlertAction(title: "I Agree to the Terms", style: .Default, handler: { (action: UIAlertAction!) in
self.onboardingFinished()
}))
self.presentViewController(termsAlert, animated: true, completion: nil)
// NSNotificationCenter.defaultCenter().postNotificationName("showTermsAlert", object: nil)
}
// Image
let onboardingVC = OnboardingViewController(backgroundImage: UIImage(named: "back"), contents: [secondPage, thirdPage])
self.navigationController?.presentViewController(onboardingVC, animated: false, completion: nil)
}
This happen when the presenting view controller is no longer part of the controller hierarchy, and it's view is no longer in the view hierarchy of any window. Most likely, the controller was dismissed or popped, but it heard the notification and attempted to present the alert controller.
You should manage your controller states more carefully. Perhaps remove observer when the controller is dismissed or popped from your controller hierarchy.
There are a few things i'd change in your code. Add a call to super in viewDidAppear:, and stop using the NSNotifications for your presentation. You don't know what thread showTermsAlert will get called on with this pattern. You can make your intent more explicit by calling showTermsAlert directly, and this will also guarantee you're on the main thread.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// Handle onboarding
if needsOnboarding() {
self.showTermsAlert()
}
}
func showTermsAlert() {
let termsAlert:UIAlertController = UIAlertController(title: "Terms And Conditions", message: "Please view the terms below before accepting them.", preferredStyle: UIAlertControllerStyle.Alert)
termsAlert.addAction(UIAlertAction(title: "View Terms", style: .Default, handler: { (action: UIAlertAction!) in
UIApplication.sharedApplication().openURL(NSURL(string: "my_terms_url")!)
}))
termsAlert.addAction(UIAlertAction(title: "I Agree to the Terms", style: .Default, handler: { (action: UIAlertAction!) in
self.onboardingFinished()
}))
self.presentViewController(termsAlert, animated: true, completion: nil)
}

Resources