calling popToRootViewController on the main thread after network callback is not working - ios

When a user sends its register form, I am trying to popToRootViewController who is a login screen. To achieve this, I have a delegation callback when server side response is success. The problem is that if I call the popToRoot... method during the delegation callback, the current viewController is not poped.
RegisterView->RegisterPresenter->NetworkManager
NetworkManager->PresenterInput->RegisterView->RegisterPresenter->RegisterWireframe(call popToRootViewController over RegisterView)
On my Wireframe:
extension RegisterRouter: RegisterRouterProtocol {
func presentLoginBack(from: RegisterViewProtocol) {
if let vc = from as? UIViewController {
DispatchQueue.main.async() {
vc.navigationController?.popToRootViewController(animated: true)
}
}
}
}
On networkLayer:
guard (200...207) ~= status else {
if status == 210 {
self.presenterInputDelegate?.notifyEndRegisterSuccess()
}

As vpoltave mentioned, there was a warning:
popToViewController:transition: called on <UINavigationController 0x126844c00> while an existing transition or presentation is occurring; the navigation stack will not be updated.
This one made me aware that I was presenting an alert in the main thread as the same time that I was performing the popToViewController transition.
So, the solution was to carry out the popToViewController when the user dismiss the alert.

Related

IOS swift UIBarButtonItem action

I've a table view with navigation controller embedded in. I've added a UIBarButtonItem (add) button. When I click this button it opens a new view where user enters the data and submits it (which makes a web service call) and returns back to the previous view. This navigation happens as shown below,
func addTapped(_ sender:UIBarButtonItem) {
print("Called Add")
let vc = (storyboard?.instantiateViewController( withIdentifier: "newNote")) as! newNoteVC
self.navigationController?.pushViewController(vc, animated: true)
}
And in new view I do following,
#IBAction func saveButton(_ sender: UIButton) {
if (self.noteDescription.text?.isEmpty)! {
print("Enter missing note description")
return
} else {
let desc = self.noteDescription.text
self.uploadNote(noteText: desc!, noteDate: self.dateInMilliseconds)
self.navigationController?.popViewController(animated: true)
}
}
This way a record gets saved and a view gets popped from the navigation controller stack but only thing I don't how to do is refresh the table view data in the parent view (where I might need to make a new http service call to read all the records).
I hope I'm able to explain the issue? Any help is appreciated.
As mentioned in the comments, making a service call just to update the tableview might be a overkill. However, if this is the business scenario which needs to be implemented, you can do the same in
func viewWillAppear
in the parent view controller. You can make the service call in this method and reload the table view with the data.
You would also need to check the overall navigation of the application as making service calls in viewWillAppear method is not a good approach as these gets called everytime the view is shown. For Ex: If coming back from a navigated view then also the method is called.

MVVM coordinators and popping a UIViewController

I've recently started using coordinators (Example: MVVM with Coordinators and RxSwift) to improve my current MVVM architecture. It's a nice solution to remove navigation related code from the UIViewController.
But I'm having trouble with 1 specific scenario. The issue arrises when a UIViewController is popped by the default back button or edge-swipe gesture.
Quick example using a list-detail interface:
A list UIViewController is shown by a ListCoordinator inside a UINavigationController. When an item is tapped, the ListCoordinator creates a DetailCoordinator, registers it as a child coordinator and starts it. The DetailCoordinator pushes the detail UIViewController onto the UINavigationController, just like every MVVM-C blog post illustrates.
What every MVVM-C blog post fails to illustrate is what happens when the detail UIViewController is popped by the default back button or edge-swipe gesture.
The DetailCoordinator should be responsible for popping the detail UIViewController, but a) it doesn't know the back button was tapped and b) the pop happens automatically. Also, the ListCoordinator wasn't able to remove the DetailCoordinator from its child coordinators.
One solution would be to use custom back buttons, which signal the tap and pass it on to the DetailCoordinator. Another one is probably using UINavigationControllerDelegate.
How have others solved this issue? I'm sure I'm not the first one.
I use Action for communication between coordinators and also between coordinator and a view controller.
AuthCoordinator
final class AuthCoordinator: Coordinator {
func startLogin(viewModel: LoginViewModel) {
let loginCoordinator = LoginCoordinator(navigationController: navigationController)
loginCoordinator.start(viewModel: viewModel)
viewModel.coordinator = loginCoordinator
// This is where a child coordinator removed
loginCoordinator.stopAction = CocoaAction { [unowned self] _ in
if let index = self.childCoordinators.index(where: { type(of: $0) == LoginCoordinator.self }) {
self.childCoordinators.remove(at: index)
}
return .empty()
}
}
}
LoginCoordinator
final class LoginCoordinator: Coordinator {
var stopAction: CocoaAction?
func start(viewModel: LoginViewModel) {
let loginViewController = UIStoryboard.auth.instantiate(LoginViewController.self)
loginViewController.setViewModel(viewModel: viewModel)
navigationController?.pushViewController(loginViewController, animated: true)
loginViewController.popAction = CocoaAction { [unowned self] _ in
self.stopAction?.execute(Void())
return .empty()
}
}
}
LoginViewController
class LoginViewController: UIViewController {
var popAction: CocoaAction?
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
if parent == nil { // parent is `nil` when the vc is popped
popAction?.execute(Void())
}
}
}
So the LoginViewController executes the action when it's popped. It's coordinator LoginCoordinator is aware that the view is popped. It triggers another action from its parent coordinator AuthCoordinator. The parent coordinator AuthCoordinator removes its child LoginCoordinator from the childControllers array/set.
BTW, why do you need to keep the child coordinators in the array and then thinking about how to remove them. I tried another approach, the child coordinator retained by a view model, once the view model deallocated, the coordinator deallocates too. Worked for me.
But I'm personally don't like so many connections and thinking about a simpler approach using a single coordinator object for everything.
I wonder if you have already solved your architecture problem and if you'd like to share your solution. I asked something related to your problem here and Daniel T. suggested to subscribe to navigationController.rx.willShow: you get back events whenever a ViewController is popped OR pushed onto the view controller stack, so you need to check yourself what kind of event it is (a pop or a push). I think the viewModel / viewController shouldn't know anything of the next story to present, so I think the viewModel could emit an event ("show detail of table cell #n") and a coordinator should push or pop the right scene ("detail of cell #n"). This kind of architecture is too advanced for me to write, so I end up with a lot of circular references / memory leaks.
Unless I am missing something, you can solve this by using this piece of code in your coordinate method. I am specifically using didShow instead of willShow (which was suggested in another answer) for the possibility of edge swipe gestures.
if let topViewController = navigationController?.topViewController {
navigationController?.rx
.didShow
.filter { $0.viewController == topViewController }
.first()
.subscribe(onSuccess: { [weak self] _ in
// remove child coordinator
})
.disposed(by: disposeBag)
}

I can not call more than one application message in Swift

i need to send multiple text messages , raising the application message several times.
But the console show this error:
2016-08-27 19:27:17.237 AlertaTel 2.0[841:263754] Attempt to present <MFMessageComposeViewController: 0x15e19ba00> on <AlertaTel_2_0.ViewController: 0x15de43af0> which is waiting for a delayed presention of <MFMessageComposeViewController: 0x15e24ca00> to complete
I read on this site about this issue, but only found solutions or topics in Objective- c and honestly do not master the language even (I'm more oriented Swfit ).
I attached my codes:
Class MessageComposer
class MessageComposer: NSObject, MFMessageComposeViewControllerDelegate {
// A wrapper function to indicate whether or not a text message can be sent from the user's device
func canSendText() -> Bool {
return MFMessageComposeViewController.canSendText()
}
// Configures and returns a MFMessageComposeViewController instance
func configuredMessageComposeViewController(unicaVariable : String) -> MFMessageComposeViewController {
let messageComposeVC = MFMessageComposeViewController()
messageComposeVC.messageComposeDelegate = self // Make sure to set this property to self, so that the controller can be dismissed!
messageComposeVC.recipients = textMessageRecipients
messageComposeVC.body = "Estoy en peligro, aca esta mi última ubicación: https://maps.google.com/maps?q="+(view.locationManager.location?.coordinate.latitude.description)!+","+(view.locationManager.location?.coordinate.longitude.description)!+". "+(unicaVariable)
//view.performRequestAndUpdateUI()
return messageComposeVC
}
// MFMessageComposeViewControllerDelegate callback - dismisses the view controller when the user is finished with it
func messageComposeViewController(controller: MFMessageComposeViewController, didFinishWithResult result: MessageComposeResult) {
controller.dismissViewControllerAnimated(true, completion: nil)
}
}
In the ViewController:
func levantarMensaje(datoWebService: String){
if (messageComposer.canSendText()) {
let messageComposeVC = messageComposer.configuredMessageComposeViewController(datoWebService)
presentViewController(messageComposeVC, animated: true, completion: nil)
} else {
// Let the user know if his/her device isn't able to send text messages
}
}
And i call this method in a #IBAction:
#IBAction func sendTextMessageButtonTapped(sender: UIButton) {
levantarMensaje()
}
When I implemented a simple " FOR" on the IBAction the error that I showed above appears.
Thank you very much for your answers , greetings !
What's happening here is that you're trying to begin a modal presentation while the previous modal presentation is still animating. UIKit doesn't like that; you need to wait until one presentation finishes before starting the next one. There are a couple of ways to do this.
The first is to have several modal presentations at the same time, but to make sure the animations don't happen simultaneously. You could do this by changing your call to presentViewController(_:, animated:, completion:) to use the completion argument to present the next message view controller. That way the first message view would appear, and when it was finished animating the next one would begin, etc.
The other would be to wait until one message is sent (or cancelled) before presenting the next one. For that you'd replace controller.dismissViewControllerAnimated(true, completion: nil) with something similar to what I described above. Instead of passing nil for the completion argument, pass a closure that presents the next message view, until none remain.

How should a custom segue support an unwind to a viewcontroller earlier in the call chain?

I have a storyboarded app using segues without a navigation controller. We have a simple custom segue like the code below that works fine for normal cases. However we have a situation where after invoking a chain of view controllers A->B->C we’d sometimes like C to unwind directly back to A. This works fine with the built-in transitions as UIKit seems to dismiss the intervening controller without any trouble, properly showing a transition from C back to A.
However in my custom segue I am not certain if or how I should dismiss the intervening view controllers. Assume that the code below knows both that it is unwinding and that the destination view controller is not the one presenting the source view controller. Here is what I’ve tried:
1) If I simply leave the code as is seems to work fine! However I get errors “Unbalanced calls to begin/end appearance transitions” for view controller C.
2) If I change the code to dismiss view controller B first I see the transition to B. I don’t see any way to suppress this.
From what I've read the unbalanced calls error comes up often when you have overlapping calls to transitions. However I don't see how that could be the case here since UIKit is dismissing them (presumably). I've also tried dispatching the completion work to the main thread (to try to get it into the next run loop) but no change.
class MyCustomSegue: UIStoryboardSegue {
override func perform()
{
let sourceView = sourceViewController.view
let destView = destinationViewController.view
let unwinding = // …
// Put the destination view on top
sourceView.superview?.insertSubview(destView, aboveSubview: sourceView)
// Do the animation
UIView.animateWithDuration(1.0, animations: { complete in
} ) { (Finished) -> Void in
if unwinding {
// calling this on presenter or presentee is equivalent
self.sourceViewController.dismissViewControllerAnimated(false, completion: nil)
} else {
self.sourceViewController.presentViewController(self.destinationViewController, animated: false, completion: nil)
}
}
}
}

Swift segue - Presenting view controllers on detached view controllers is discouraged

I want to open "Select city" controller, when selectedCityId is 0 (Undefined) - to select a city. In my root view controller (MainVC) in viewDidLoad():
let selectedCityId = NSUserDefaults.standardUserDefaults().integerForKey("selectedCityId")
if(selectedCityId == 0){
self.performSegueWithIdentifier("startupSegue", sender: self)
} else {
....
}
So the startupSegue opens settingsViewController (SettingsVC) modally! Both - MainVC and SettingsVC have embed in Navigation View Controller.
There are 2 warnings:
Presenting view controllers on detached view controllers is
discouraged "myapp.MainVC: 0x7fee8871e670".
Unbalanced calls to begin/end appearance transitions for
"UINavigationController: 0x7fee8872c950".
Thanks for answers.
I had a same warning and fixed it by moving code to -viewDidAppear. One thing you should be aware is that -viewDidLoad is called only once while -viewDidAppear maybe called multiple times if page becomes visible so you need to add additional mechanism to skip performing segue again.
Moving code to -viewDidAppear is a good idea but I would also suggest to perform your seque call from the main thread:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("startupSegue", sender: self)
})
Swift 3
DispatchQueue.main.async
{
self.performSegue(withIdentifier: "startupSegue", sender: self)
}

Resources