Using callbacks to reproduce UIAlertViewController - ios

I want to reproduce the functionality of the UIAlertController as the class isn't very customisable. So here is what I did:
Create a DeleteDeckViewController that only has two buttons Yes, and No, with a callback method
class DeleteDeckViewController: UIViewController {
var deleteDeck : ((deleteDeck : Bool) -> ())?
#IBAction func yesButton(sender: AnyObject) {
deleteDeck!(deleteDeck: true)
}
#IBAction func noButton(sender: AnyObject) {
deleteDeck!(deleteDeck: false)
}
}
In the main view controller when the delete button is pressed it creates a popup DeleteDeck
func handleDelete(detailDeckView: DetailDeckView) {
let deleteDeckViewController = DeleteDeckViewController(nibName: "DeleteDeckViewController", bundle: nil)
let popup = PopupDialog(viewController: deleteDeckViewController, transitionStyle: .BounceDown, buttonAlignment: .Horizontal, gestureDismissal: true)
presentViewController(popup, animated: true, completion: nil)
deleteDeckViewController.deleteDeck(true) { () -> Void in
print("Deck deleted")
}
}
What I want to do is to use a callback to tell my main view controller whether the user pressed yes or no and then do some actions depending on yes or no. Although, I don't know the correct way to do it. I've read a few articles on callbacks, but I don't know how to apply it in this situation. I've tried using delegates it's just that I would need to pass the DetailDeckView object around without it being used so that's why I thought callback would be a better choice.
Would someone be able to point me in the right direction?

Within DeleteDeckViewController you have declared the deleteDeck variable as a function. Therefore, when you use an instance of this class, you need to set it as a variable. Therefore you code would look more like that below (untested):
func handleDelete(detailDeckView: DetailDeckView) {
let deleteDeckViewController = DeleteDeckViewController(nibName: "DeleteDeckViewController", bundle: nil)
deleteDeckViewController.deleteDeck = myDeleteHandler
let popup = PopupDialog(viewController: deleteDeckViewController, transitionStyle: .BounceDown, buttonAlignment: .Horizontal, gestureDismissal: true)
presentViewController(popup, animated: true, completion: nil)
}
func myDeleteHandler(shouldDelete: Bool) {
if shouldDelete {
print("Delete deck")
}
}
Also, in your code, rather than using deleteDeck! which will fail if deleteDeck is nil, it is better practice to use code like:
if let deleteDeck = self.deleteDeck {
deleteDeck(true)
}
This will create a local non-Optional variable called deleteDeck, that can be assured to not contain nil.
You could build this code into DeleteDeckViewController to streamline further:
class DeleteDeckViewController: ....
func showDialog(vc: UIViewController, completion: ((deleteDeck : Bool) -> Void)?) {
self.deleteDeck = completion
let popup = PopupDialog(viewController: vc, transitionStyle: .BounceDown, buttonAlignment: .Horizontal, gestureDismissal: true)
presentViewController(popup, animated: true, completion: nil)
}

Related

Reloading TableView when a UIViewController is being dismissed?

The problem here is that I'm presenting EditCommentVC modally, over the current context of the CommentVC because I want to set the background of the UIView to semi-transparent. Now, on the EditCommentVC I have a UITextView that allows the user to edit their comment, along with 2 buttons - cancel (dismisses the EditCommentVC) and update that updates the new comment and push it to the database.
In term of code, everything is working, except that once the new comment is being pushed and EditCommentVC is being dismissed, the UITableView on CommentsVC with all the comments is not being reloaded to show the updated comments. Tried calling it from viewWillAppear() but it doesn't work.
How can I reload the data in the UITableView in this case?
#IBAction func updateTapped(_ sender: UIButton) {
guard let id = commentId else { return }
Api.Comment.updateComment(forCommentId: id, updatedComment: editTextView.text!, onSuccess: {
DispatchQueue.main.async {
let commentVC = CommentVC()
commentVC.tableView.reloadData()
self.dismiss(animated: true, completion: nil)
}
}, onError: { error in
SVProgressHUD.showError(withStatus: error)
})
}
The code in the CommentVC where it transitions (and passes the id of the comment). CommentVC conforms to a CommentActionProtocol that passes the id of that comment:
extension CommentVC: CommentActionProtocol {
func presentActionSheet(for commentId: String) {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let editAction = UIAlertAction(title: "Edit", style: .default) { _ in
self.performSegue(withIdentifier: "CommentVCToEditComment", sender: commentId)
}
actionSheet.addAction(editAction)
present(actionSheet, animated: true, completion: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "CommentVCToEditComment" {
let editCommentVC = segue.destination as! EditCommentVC
let commentId = sender as! String
editCommentVC.commentId = commentId
}
}
}
I see atleast 2 problems here:
You are creating a new CommentVC which you should not do if you want to update the tableView in the existing view controller.
Since you have mentioned that Api.Comment.updateComment is a an asynchronous call, you need to write the UI code to run on the main thread.
So first you need to have the instance of the commentVC in a variable inside this viewController. You can store the instance of the view controller from where you are presenting this view controller.
class EditCommentVC {
var commentVCdelegate: CommentVC!
// Rest of your code
}
Now you need to pass the reference commentVC in this variable when you are presenting the edit view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "CommentVCToEditComment" {
let editCommentVC = segue.destination as! EditCommentVC
let commentId = sender as! String
editCommentVC.commentId = commentId
editCommentVC.commentVCdelegate = self
}
}
Now you need to use this reference to reload your tableView.
Api.Comment.updateComment(forCommentId: id, updatedComment: editTextView.text!, onSuccess: {
DispatchQueue.main.async {
commentVCdelegate.tableView.reloadData() // - this commentVC must be an instance that you store of the your commentVC that you created the first time
self.dismiss(animated: true, completion: nil)
}
}, onError: { error in
SVProgressHUD.showError(withStatus: error)
})
Well, i had this problem too, and the solution i found was to use Protocol. I would recommend you to search how to send data back to previous ViewController. That way, when you dismiss the EditCommentVC, you then send back a value(in my case i send true) to the previous ViewController(in your case, CommentVC), and then you'll have a function in CommentVC checking if the value is true and if it is, reload the TableView.
Here, let me show you an example of how i used (those are the names of my ViewControllers, functions and protocols, you can use whatever you want and send whatever data you want back):
In your CommentVC, you'll have something like this:
protocol esconderBlurProtocol {
func isEsconder(value: Bool)
}
class PalestranteVC: UIViewController,esconderBlurProtocol {
func isEsconder(value: Bool) {
if(value){
//here is where you can call your api again if you want and reload the data
tableView.reloadData()
}
}
}
Also, dont forget that you have to set the delegate of EditCommentVC, so do it when you're presenting EditCommentVC, like this:
let viewController = (self.storyboard?.instantiateViewController(withIdentifier: "DetalhePalestranteVC")) as! DetalhePalestranteVC
viewController.modalPresentationStyle = .overFullScreen
viewController.delegate = self
self.present(viewController, animated: true, completion: nil)
//replace **DetalhePalestranteVC** with your **EditCommentVC**
And in your EditCommentVC you'll have something like this:
class DetalhePalestranteVC: UIViewController {
var delegate: esconderBlurProtocol?
override func viewWillDisappear(_ animated: Bool) {
delegate?.isEsconder(value: true)
}
}
That way, everything you dismiss EditCommentVC, you'll send back True and reload the tableView.

Get notified of dismissed PopupDialog in view controller

I have a project that is making use of https://github.com/Orderella/PopupDialog for popup dialogs which works very well.
A dialog is created an presenting like:
let ratingVC = PopupViewController(nibName: "PopupViewController", bundle: nil)
ratingVC.apiKey = self.apiKey
ratingVC.accountNumberString = accountNumberString
let popup = PopupDialog(viewController: ratingVC, buttonAlignment: .horizontal, transitionStyle: .bounceDown, gestureDismissal: true)
ratingVC.presentedPopup = popup
self.present(popup, animated: true, completion: nil)
Which allows for custom view controller work within the popup. Within the PopupViewController the PopupDialog might get dismissed using self.dismiss(animated: true)
This works well however I'm not sure how the launching view controller (where self.present is run) would get notified that the PopupDialog has been dismissed.
I have tried
override func dismiss(animated flag: Bool, completion: (() -> Void)?)
{
super.dismiss(animated: flag, completion:completion)
}
In the launching view controller but this doesn't get called.
You could create a PopupViewControllerDelegate like the one described in this SO answer, something like this.
protocol PopupViewControllerDelegate:class {
func viewControllerDidDismiss(_ sender: PopupViewController)
}
class PopupViewController: UIViewController {
...
weak var delegate: PopupViewControllerDelegate?
...
}
and call it when the ViewController is dismissed.
Then implement PopupViewControllerDelegate protocol in the launching view controller and set it when the PopupViewController is created:
let ratingVC = PopupViewController(nibName: "PopupViewController", bundle: nil)
ratingVC.delegate = self
...
Based on the docs of the PopupDialog you're using, the buttons of the popup have completion blocks, in which you can catch if the dialog is going to be dismissed or not.
OR the easiest way you can do right now is to add the completion block in your PopupDialog instantiation.
Like so:
From:
let popup = PopupDialog(viewController: ratingVC, buttonAlignment: .horizontal, transitionStyle: .bounceDown, gestureDismissal: true)
To:
let popup = PopupDialog(viewController: ratingVC, buttonAlignment: .horizontal, transitionStyle: .bounceDown, gestureDismissal: true) {
print("PopupDialog has been dismissed! ✅")
}
Let me know if this helps!

How do I prevent a navigationController from returning to root when dismissing MFMailComposeViewController

When I dismiss an instance of MFMailComposeViewController or MFMessageComposeViewController that is presented modally from the third viewController in a navigation stack, the navigation stack is reset, and the root VC is reloaded. How can I prevent this behavior and remain on the original presenting viewController (third VC in the stack)? I get the same behavior whether I call dismiss from the presenting VC, the presented VC, or the navigationController.
This has been asked before, but I have not seen a solution.
App Structure looks like this:
TabBarController
Tab 1 - TripsNavController
-> Trips IntroductionVC (root VC) segue to:
-> TripsTableViewController segue to:
-> TripEditorContainerVC
- TripEditorVC (child of ContainerVC)
- HelpVC (child of ContainerVC)
Tab 2...
Tab 3...
Tab 4...
In the TripEditorVC I present the MFMailComposeViewController. The functions below are declared in an extension to UIViewController that adopts the MFMailComposeViewControllerDelegate protocol
func shareWithEmail(message: NSAttributedString) {
guard MFMailComposeViewController.canSendMail() else {
showServiceError(message: "Email Services are not available")
return
}
let composeVC = MFMailComposeViewController()
composeVC.setSubject("My Trip Plan")
composeVC.setMessageBody(getHTMLforAttributedString(attrStr: message), isHTML: true)
composeVC.mailComposeDelegate = self
present(composeVC, animated: true, completion: nil)
}
Then in the delegate method I dismiss the MFMailComposeVC:
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
switch result {
case .sent:
print("Mail sent")
case .saved:
print("Mail saved")
case .cancelled:
print("Mail cancelled")
case .failed:
print("Send mail failed")
}
if error != nil {
showServiceError(message: "Error: \(error!.localizedDescription)")
}
dismiss(animated: true, completion: nil)
}
I have tried the following to present and dismiss and get the same behavior, i.e.: the TripsNavController clears the nav stack and reloads the TripsIntroductionVC as its root VC:
self.present(composeVC, animated: true, completion: nil)
self.parent?.present(composeVC, animated: true, completion: nil)
self.parent?.navigationController?.present(composeVC, animated: true, completion: nil)
self.navigationController?.present(composeVC, animated: true, completion: nil)
You can also check with presentingViewController?.dismiss method to get the solution.
I have tried with following navigation stack.
I am able to send email successfully from Container VC's Send Email button using your code only.
Can you please check and verify navigation flow?
Please let me know if you still face any issue.
dismiss(animated: true, completion: nil)
to
self.dismiss(animated: true, completion: nil)
Try this
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let secondViewController = storyboard.instantiateViewControllerWithIdentifier("secondViewControllerId") as! SecondViewController
self.presentViewController(secondViewController, animated: true, completion: nil)
or
https://stackoverflow.com/a/37740006/8196100
Just Use Unwind Segue its Pretty simple and perfect in your case
I have used many times..
just look at this Unwind segue's example
. If u still don't able to find answer or not able to understand the Unwind segue then just reply to me.
Hope this will solve your problem.
I found the problem with my navigation stack today. I built a simple
project that duplicated the tabBarController/NavigationControler architecture of my problem project, component by component, until dismissing a MFMailComposeViewController caused my navigation stack to reset as described in my original post.
That immediately pointed to the source of the bug. In my subclassed UINavigationCotroller I was instantiating the root viewController in code so that I could skip an introductory view if the user had set a switch in the apps settings. In order to pick up changes in that switch setting I was calling my instantiation code in viewDidAppear of the navigationController. That worked fine EXCEPT when dismissing the mailComposeVC. The fix was to add a guard statement in viewDidAppear to return if the navControllers viewController collection was not empty, and send and respond to an NSNotification when the switch was changed.
class TopNavigationController: UINavigationController {
var sectionType: SectionType?
var defaults = UserDefaults.standard
var showIntroFlag: Bool = true
override func viewDidLoad() {
super.viewDidLoad()
// Handle initial load of the tab bar controller where we are not sent a sectionType
if sectionType == nil {
sectionType = .groups
}
setShowIntroFlag()
NotificationCenter.default.addObserver(self, selector: #selector(resetControllers), name: NSNotification.Name(rawValue: "kUserDidChangeShowIntros"), object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
guard self.viewControllers.isEmpty else {
return
}
loadControllers()
}
func setShowIntroFlag() {
showIntroFlag = true
// Check NSUserDefaults to see if we should hide the Intro Views for all sections
if defaults.bool(forKey: "SHOW_SECTION_INTROS") == false {
showIntroFlag = false
}
}
func loadControllers() {
if showIntroFlag == true {
showIntro()
} else {
skipIntro()
}
}
func resetControllers() {
setShowIntroFlag()
loadControllers()
}

Swift alert custom show and dispense

I created a custom alert in a viewcontroller, following the guidelines of the most voted answer of that question:
https://stackoverflow.com/a/37275840/6196609
I use this to display the alert, it is used as a "loading".
let pending = UIAlertController()
override func viewDidLoad() {
super.viewDidLoad()
[…]
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let pending = storyboard.instantiateViewControllerWithIdentifier("alertaLoad")
pending.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
pending.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
[…]
}
to show:
self.presentViewController(self.pending, animated: true, completion: nil)
I succeeded in showing it, but I need to terminate it by the viewcontroller that invoked it after the end of my process, not by itself as was done in the example I quoted.
I've tried this but nothing happens.
self.pending.dismissViewControllerAnimated(false, completion: { (vetor) -> Void in
[…]
})
How could I do this correctly?
Call dismisson the presenting UIViewController, not on the presented one:
self.dismiss(animated: true) {
// go on
}

Cannot subscript a value of type '[OnboardingViewController]'

I'm creating an array of ViewControllers and XCode is giving me lip about trying to access them via bracket notation. The following code instantiates two OnboardingViewControllers (subclasses of UIViewController).
var controllers = [OnboardingViewController]()
var controller = OnboardingViewController()
let totalOnboardingPages = 2
override func viewDidLoad() {
super.viewDidLoad()
populateControllersArray()
createPageViewController()
}
func populateControllersArray(){
for i in 0...totalOnboardingPages-1 {
controllers.append(getPageController(i))
}
}
func getPageController(itemIndex: Int) -> OnboardingViewController {
var controller = OnboardingViewController()
switch itemIndex {
case 0:
// Welcome
controller = storyboard!.instantiateViewControllerWithIdentifier("OnboardingWelcomeViewController") as! OnboardingViewController
case 1:
// Find Facebook Friends
controller = storyboard!.instantiateViewControllerWithIdentifier("OnboardingFacebookViewController") as! OnboardingViewController
default: ()
}
controller.itemIndex = itemIndex
return controller
}
func createPageViewController() {
let pageController = storyboard!.instantiateViewControllerWithIdentifier("OnboardingPageViewController") as! UIPageViewController
pageController.dataSource = self
// BREAKS ON THIS LINE
pageController.setViewControllers(controllers[0], direction: .Forward, animated: false, completion: nil)
pageViewController = pageController
addChildViewController(pageViewController!)
view.addSubview(pageViewController!.view)
pageViewController!.didMoveToParentViewController(self)
}
The error occurs in createPageViewController():
pageController.setViewControllers(controllers[0], direction: .Forward, animated: false, completion: nil)
Cannot subscript a value of type '[OnboardingViewController]'
OK, but when I set a breakpoint at that line I can po controllers[0] and get my controller:
<MyApp.OnboardingWelcomeViewController: 0x618000086080>
In fact, the very next function I have this which XCode doesn't complain about.
return controllers[controller.itemIndex - 1]
controllers is not optional and I have no idea what's going on here. I've cleaned, deleted the DerivedData directory and restarted XCode. Help!
The solution is to pass an array of ViewControllers into setViewControllers(). My corrected code is:
pageController.setViewControllers([controllers[0]], direction: .Forward, animated: false, completion: nil)
Thanks to Paulw11 for the helpful comment.

Resources