performSegueWithIdentifier very slow when segue is modal - ios

I have a simple table view where I handle the select action on the table view. This action follows a segue.
If the segue is a push segue, the next view shows immediately.
If the segue is a modal segue, the next view either:
takes 6 seconds or so to display
shows immediately if I tap again (second tap)
I tried looking around for some ideas, but none seem applicable to my situation. In particular:
I'm performing the segue on the main UI thread
My view is very simple (so there's no issue in viewDidLoad). Plus the fact that it shows up near instantaneous when the segue is push indicates that there is no problem loading the target view
I tried passing nil to the sender; same effect.
Does anyone have any ideas on this?

Trust me and try this. I have run into this problem a few times.
In Swift 2:
dispatch_async(dispatch_get_main_queue(),{
self.performSegue(withIdentifier:mysegueIdentifier,sender: self)
})
or for Swift 3:
DispatchQueue.main.async {
self.performSegue(withIdentifier: mysegueIdentifier,sender: self)
}
As discussed here and here.

It seems (to me...) that this problem happens only when the cell selectionType is not .none.
You may change it to any other option (at the storyboard Attribute inspector, set the Selection field) and it will work fine (working for me...).
The cons is that it messing up the cell UI.
You can call the segue in DispatchQueue.main.async{} block at the didSelect delegate function of UITableViewDelegate as people mention before.
I used the first solution and added at the cell itself -
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(false, animated: false)
}
This will make the cell 'highlight' at the tap, but it will return to its usual UI immediately and it fine for me...

There seem to be various situations when performing a segue will not work properly. For example, if you call performSegue from the action handler of an unwind segue, you will run into various issues, even though you are on the main thread. On my current project, I am calling performSegue from the didSelectRowAt method of a table view. This is one of the most basic segues there is and of course I am on the main thread, yet I was seeing the exact symptoms that the OP described.
I do not know why this happens in some cases and not others, but I have found that deferring the performSegue call using async fixes any potential issues. This used to seem like a hack and made me nervous, but at this point I have several mature projects using this approach and it now seems like the "right" way to do a manual segue.
Here is the Swift 3 version of the code (see the other posts for Swift 2 and Obj-C versions):
DispatchQueue.main.async {
self.performSegue(withIdentifier: "theIdentifier", sender: theSender)
}

The accepted solution worked for me. Code updated for Swift 2.0 below:
dispatch_async(dispatch_get_main_queue(),{
self.performSegueWithIdentifier(mysegueIdentifier, sender:self)
})

Hope help this yo can create programatically modal transition like this in Swift:
let myStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let modalViewController = myStoryboard.instantiateViewControllerWithIdentifier("myModalViewController") as! myModalViewController
modalViewController.modalTransitionStyle = UIModalTransitionStyle.CoverVertical
let navController = UINavigationController(rootViewController: accountManager)
dispatch_async(dispatch_get_main_queue(),{
self.presentViewController(navController, animated: true, completion: nil)
})

For developers organizing their code through subclassing, I've come to quite simple a solution I'd like to share (Swift 4):
import UIKit
class ABCViewController: UIViewController {
// ... Other useful methods like overriding deinit and didReceiveMemoryWarning
// Performs a segue on the main thread to ensure it will
// transition once a UI-slot is available
func performSegueOnMainThread(with identifier: String, sender: Any?) {
DispatchQueue.main.async {
self.performSegue(with: identifier, sender: sender)
}
}
}
Then, simply call it from your implementation:
myViewController.performSegueOnMainThread(with: "ShowDetailsSegue", sender: self)

For me it was the that I had too many "Clear" colored views in my next view, so when the segue was animated, it seemed like it was delaying because of this. I went through the view controller's UI hierarchy looking for clear colors and replacing them with solid black or white, and making sure the alpha is 1 (if it didn't need to be otherwise). My delay is now gone and my modal presentation is smooth.

I tried fixing this multiple ways including moving it to the main thread above. This worked for me:
In storyboard, select the table view in question, (select it in document outline to make sure you've got the right thing. Then in attributes inspector you will be able to see the attributes for the table view as well as the containing scrollview underneath it (all table views are based on scroll view). There is this stupid little box there called "delays content touches". Uncheck it. There is also one "allows cancellable touches" which I'll imagine you want to make sure is unchecked as well, as I think double touches were messing mine up.

Related

How to correctly dismiss previous view controller

Apologies as I know there are some similar questions, but I've been looking for two weeks through every one I can find, and cannot figure it out (I'm a bit of a novice).
I have a few different View Controllers, not using a Navigation Controller. I can segue between them no problem. The issue is, I need each view to be dismissed when I segue to a new one. Here is some of what I've tried so far.
Option 1 (in new View Controller)
override func viewDidAppear(_ animated: Bool) {
presentingViewController?.dismiss(animated: false, completion: nil)
}
Option 2 (in old View Controller)
override func viewDidDisappear(_ animated: Bool) {
self.dismiss(animated: false, completion: nil)
}
In both of these cases, the new view gets dismissed and I'm taken back to the old view. I've tried about 20 versions of similar code.
Should I be using the first VC in my program as my "main" view controller, and presenting/dismissing all others on top of it? I didn't think this approach seemed memory efficient, when the "main" VC is not often used after initially loading the app.
It seems like I'm missing or not understanding something. Any help would be greatly appreciated.
Think of it this way: A view controller can't exist on an island. It has to be presented on top of something.
That means when you present one VC on top of another, the presenting view controller is the "foundation" for the new one you just presented.
If you don't want to present VCs on top of each other, you have a couple of options:
1) Use a navigation controller. This is probably the best approach. You can present or push any view controller. If you decide to push, you can remove the old one from the navigation stack, or you can keep it there so the user can go back. There are lots of ways to use a navigation controller, and it's easily the most flexible way to navigate between controllers.
2) Use a tab bar controller. This works best if you have just a few different view controllers in your app, but it's good for certain use cases.
3) Do exactly what you said in your post (use the root view controller to present/dismiss all other VCs). As I said, you can't present a view controller out of thin air-- there always has to be something behind it. Unless there's a ton of stuff going on in your root VC, this shouldn't cause any memory issues. This approach should be fine unless you're very particular about the animations between your view controllers.
In general, I wouldn't worry too much about memory usage until it becomes a problem. It should be fine to present view controllers on top of each other for 99% of normal use cases.
if you want to present VC B from VC A and want to dismiss VC A while Presenting you can use this Code
let parentVC = presentingViewController
dismiss(animated: true) {
let vc = self.storyboard!.instantiateViewController(withIdentifier...)
parentVC.present(vc, animated: true)`enter code here`
} `enter code here`

Same view controller loads twice

I've read several posts on this issue but none of them solved my problem.
I'm coding an app where I have to click on a button ("Prepare") to go to the following ViewController. When the button is clicked, it also passes data between the two view controller.
The problem is, when I click the button, the following ViewController loads twice. Thus, if I want to go back I have to go back through two same ViewController.
I've checked the segue, the class names and files names but nothing fixes it.
I've also created a new project and rewritten all the code from the beginning but in the end, it still doesn't work.
However, I've noticed that the problem showed up when I added the prepare(forSegue:) function and the performSegue function. Without it the ViewController only loads once. But of course, I can't get the data passed between the views without it...
Here is the screenshot of my two view and the code of the two functions :
First view
Second view
// Prepare the segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "prepareSegue" {
let timerViewController = segue.destination as! CountdownViewController
timerViewController.timeInterval1 = waitingTime
timerViewController.timeInterval2 = shootingTime
timerViewController.lastSeconds = lastSeconds
timerViewController.currentTimeInterval = waitingTime
}
}
// Prepare the timer when the button is pushed
#IBAction func prepareTimer(_ sender: Any) {
performSegue(withIdentifier: "prepareSegue", sender: self)
}
Probably, when two ViewControllers appear it's because you have:
A segue on Storyboard which start directly from a Button
IBAction attached to the button where you call a performSegue of the same Segue
To fix this problem, you need to create a Segue which start directly from ViewController. After you can use the IBAction and the performSegue
You have added a seague but also an IBAction. If the seague is defined well in InterfaceBuilder it will perform and call your method. The IBAction is the alternative way for connecting an Action to a button. If you use both, you have two actions.
Without the rest of your project to check, one thing it could be is a completion closure which contains the performSegue(...) statement is called twice, so the performSegue statement is literally run twice. How might this happen?
If you have a networking call inside the closure which contains performSegue command, and the networking call has a closure which calls completion(...) twice, then you could get either the segue occuring twice or even recursively!
It's rare this will be the case, or in your instance, but this type of problem can be uncovered by using lots of breakpoints and following the "bouncing ball" to see the completion() calls occuring more than once.

UIAlertController actionsheet too slow to pop up [duplicate]

I have a tab bar based app. There are navigation controllers in all 5 tabs with instances of custom view controller setup properly as root view controllers. This loads just fine. A couple of these view controllers contain table views. I want to show a modal view controller to the user when they select a row in the table view. The (relevant part of) didSelectRowAtIndexPath delegate method looks as follows:
SampleSelectorViewController *sampleVC = [[SampleSelectorViewController alloc] init];
[self presentViewController:sampleVC animated:YES completion:NULL];
The modal view controller appears BUT it appears after a very noticeable delay. At times it even requires the user to tap the row a second time. A few things that I have already verified are:
Table view's didSelectRowAtIndexPath method is called when the user taps the row
The didSelectRowAtIndexPath method does not contain any blocking calls. There are no network operations being performed and the modal view controller's setup does not involve any processing intensive task. The data it displays is static.
If I push the new view controller onto the navigation stack (everything else remaining exactly same), it behaves perfectly without any delays. It is only when presented modally that the delay is encountered.
Any ideas/suggestions?
It seems calling presentViewController:animated:completion from within tableView:didSelectRowAtIndexPath: is problematic. It's difficult to find anything that stands out when using the Time Profiler in Instruments, also. Sometimes my modal view comes up in less than a second and other times it takes 4s or even 9s.
I think it's related to the underlying UIPresentationController and layout, which is one of the few things I see when selecting the region of time between tapping on a row and seeing the modal presentation in the Time Profiler.
A Radar exists describing this issue: http://openradar.appspot.com/19563577
The workaround is simple but unsatisfying: delay the presentation slightly to avoid whatever contentious behavior is causing the slowdown.
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:nav animated:YES completion:nil];
});
If you call present(:animated:completion:) in tableView(:didSelectRowAt:), selectionStyle == .none for selected tableview cell and you’ve got this strange behavior then try to call tableView.deselectRow(at:animated:) before any operations in tableView(_:didSelectRowAt:).
Did it help?
Swift 4:
you can use as below.
DispatchQueue.main.async {
let popUpVc = Utilities.viewController(name: "TwoBtnPopUpViewController", onStoryboard: "Login") as? TwoBtnPopUpViewController
self.present(popUpVc!, animated: true, completion: nil)
}
It works for me.
I guess you also set the cell's selectionStyle to UITableViewCellSelectionStyleNone. I change to UITableViewCellSelectionStyleDefault and it work perfect.
You should display it modally from your root vc (e.g: customTabBarRootViewController).
save a reference, and use the reference controller to display it.
I have also had this strange delay when presenting from tableView:didSelectRowAtIndexPath: looks like an Apple bug.
This solution seems to work well though.
CFRunLoopWakeUp(CFRunLoopGetCurrent()); // Fixes a bug where the main thread may be asleep, especially when using UITableViewCellSelectionStyleNone
Solution in Swift 3
In the SampleSelectorViewController(the presented view controller) use the below code
DispatchQueue.global(qos: .background).async {
// Write your code
}
The common problem with this behaviour is as follows:
one sets selectionStyle = .none for a cell in the tableView (it seems that it doesn't depend on UITableViewController subclassing as was written at http://openradar.appspot.com/19563577) and uses at the delegate method
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
animating deselect
tableView.deselectRow(at: indexPath, animated: true)
which implies animation for non-animating cell.
In this case the subsequent view controller presentation has a delay.
There are some workaround solutions including dispatch_async on main thread, but it is better not to call deselectRow even without animation on unselectable cells in your code.
As per #Y.Bonafons comment, In Swift, You can try like this, (for Swift 4.x & 5.0)
DispatchQueue.main.async {
self.showAction() //Show what you need to present
}
Try this For swift version 5.2 you can use below code:
DispatchQueue.main.async(execute:{self.present(nav, animated: true)})

"whose view is not in view hierarchy" on Segue

I try to perform a segue programatically from one view to another, way after it's been loaded (server sends an order to proceed) but it won't happen because "whose view is not in view hierarchy". Any ideas on how to solve it?
P.S. I don't perform any segues from viewDidLoad() method, all of them are performed from a method which is also called from another method in this class (which reads a dictionary, sent from the server). Also I tried to perform segues like this
dispatch_async(dispatch_get_main_queue(), {
self.performSegueWithIdentifier("seg_wait_ready", sender: self)
})
and just like this (even though I know this is wrong):
performSegueWithIdentifier("seg_wait_ready", sender: self)
None of these work, I still get the same warning.
I solved this issue by changing the logic of how my application changes VCs. Now I do it directly through the delegate by doing the following (roughly):
delegate.window!.rootViewController = destinationViewController
It cost me all the system transition animations but I like to do custom ones anyway. But remember to keep it in main queue. To do it safe put it in the main queue manually like this:
dispatch_async(dispatch_get_main_queue(), {delegate.window! ... })

Xcode 7 - Some segues not working anymore

I upgraded my Swift app to Xcode 7 / Swift 2.0 and now suddenly certain segues in my app no longer work.
I have a segue popping up a "Check In" modal and it works perfectly, but then I have another segue popping up a "Check Out" modal that's near identical and it doesn't launch and the app is frozen.
I re-created the segue from scratch, confirmed that it is identical to the "Check In" one, and it still doesn't work.
I also, instead, tried launching a blank view instead of my Check Out modal and it works fine.
There are no errors, it just freezes, I did confirm that the "prepareForSegue" portion is being called correctly but the "viewDidLoad" portion of my modal is not being invoked.
FYI, I have auto-layout turned off.
Does your "Check Out" modal have a UITextView? If it does, then there's a bug in Xcode 7 / iOS9 where you cannot launch a modal (or any root view) that contains a UITextView if you have set a default text value in storyboard.
A work around is to make sure your UITextView in storyboard is either blank or has the default Lorem Ipsem value and instead, set the text programmatically in code on viewDidLoad.
Hopefully this bug will be fixed soon.
I suspect there is infinite loop somewhere in you "Check Out" controller code. Try pausing app in debugger after presenting controller (when it freezes) and check stacktrace. If it doesn't help, try commenting-out code line-by-line in viewDidLoad and viewWillAppear to find line causing freeze.
I had this problem, try with this
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let viewController:UIViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("Storyboard Id")
self.presentViewController(viewController, animated: true, completion: nil)
})
You just have to give a storyboard id in your view and normally it's working.
My app was working perfectly in iOS8. Its flow was:
VC: View Controller, NVC: Navigation View Controller, ER: Embedded root relationship between NVC and VC, PS: Push Segue, PG: Programmatic presentation
NVC1---(ER)->VC1---(PS)->NVC2---(ER)->VC2 and so on.
The problem was that VC1-(PS)->NVC2 segue did not work, there was no freeze. vc1.prepareForSegue() was executed, but VC2 was not presented. I experimented and found that I did not have the UITextView problem mentioned here.
By following the breadcrumbs outlined below, I got it work after hours of trying in the following way (code at the end):
NVC1---(ER)->VC1---(PG)->VC2
Here are the steps:
As mentioned in Segue issue in iOS9, multiple NVCs are out of style (shame on Apple for suddenly ditching what is actually recommended in your online tutorial and making apps break!). So I modified
NVC1---(ER)->VC1--(PS)->VC2 while VC2 was still embedded in NVC2. I got errors
similar to the StackOverflow post on view not in hierarchy. So I started doing the transition programmatically and after tackling present vs. push ViewController issue that results in "tried to push modally on active view controller" message and then ViewController lifecycle issues that result in "Unbalanced calls to begin/end appearance transactions" messages, I got the following code working. Based on this experience, I really think Apple should have left a working thing alone in Xcode7/iOS9 update.
//*********** VC1.swift. A translation of working code
class VC1:UIViewController{
private var viewController2:VC2?
private var showVC2: Bool = false
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if(showVC2) {
showVC2 = false
self.pushVC2()
}
}//viewWillAppear
private var info:String? // info from incoming user action.
#IBAction unwindToVC1FromUserAction(incomingSegue: UIStoryboardSegue?) {
// Do app-specific stuff to get info from incomingSegue and
// store in self.info variables.
let myboard: UIStoryBoard = self.storyboard!;
self.viewController2 = myboard.instantiateViewControllerWithIdentifier(
"VC2 Storyboard ID") as! VC2
self.prepareVC2() // Pass info to VC2.info, stuff you would have done in prepareForSegue
showVC2= true
} //unwind
private func prepareVC2() {
self.viewController2.info = self.info // etc..
}
private func pushVC2() {
self.navigationController!.pushViewController(viewController2!, animated:false)
}
} //class
I had this, but it was none of the above. My segue call happened to be inside a block. When I moved the call to the main thread I saw that I had a 'NSUnknownKeyException' error in the new View Controller. Being inside the block seemed to prevent the error from registering in Xcode, so the app just appeared to hang without any errors.
Once that was resolved the original code worked fine.

Resources