26

My navigation controller intermittently will freeze on push. It seems to add the new view controller onto the stack, but the animation never takes place. I also have two other containers that hold view controllers on the screen, and I can interact with both of them just fine after the navigation controller freezes. The really interesting thing is if I try to push another view controller onto the navigation controller's stack, I noticed that there is an extra view controller on top of the stack (the view controller that I pushed initially that froze the navigation controller). So if I'm on the home screen (we'll call it VC-Home) and I try to push a new view (VC-1) and it freezes, then I try to push a new view (VC-2), this is what I see in the current stack before the push:

{ [VC-Home, VC-1] }

and after pushViewController is called, it remains the same; VC-2 is not added to the stack.

From what I can tell, the navigation controller starts the animation by making the previous view controller inactive before the animation begins, but then the animation never takes place, leaving the navigation controller in a frozen state.

I'm creating the new view controller from a storyboard by calling UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController") so I don't think there's any issues there. I'm also not overriding pushViewController on the navigation bar. Some unique things about my app is that it is very high-res image heavy (using SDWebImage to manage that) and I always have three containers on the screen at once (one navigation controller, one view controller for search, and one interactive gutter/side/slideout menu).

CPU usage is low and memory usage is normal (steadily around 60-70MB on device when freezes occur).

Are there any ideas with what might be causing this or any debugging tips that could help me discover the real problem?

Update

There's no unique code for the UINavigationController since I'm just pushing using pushViewController(). Here's the code that calls it:

func didSelectItem(profile: SimpleProfile) {
     let vc = UIStoryboard.profileViewController()
     vc.profile = profile
     navigationController?.pushViewController(vc, animated: true)
}

The ViewController that I pushed has the following code in viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    button.roundView()

    if let type = profile?.profileType {
        //load multiple view controllers into a view pager based on type 
        let viewControllers = ProfileTypeTabAdapter.produceViewControllersBasedOnType(type)
        loadViewPagerViews(viewControllers)

        let topInset = headerView.bounds.height + tabScrollView.contentSize.height
        if let viewPager = viewPager {
            for view in viewPager.views {
                if let tempView = view as? PagingChildViewController {
                    tempView.profile = fullProfile
                    tempView.parentVCDelegate = self
                    tempView.topInset = topInset
                }
            }
        }
    }
}

func loadViewPagerViews(viewControllers: [UIViewController]) {
    viewPager?.views = viewControllers
    viewPager?.delegate = self

    //loading views into paging scroll view (using PureLayout to create constraints)
    let _ = subviews.map { $0.removeFromSuperview() }
    var i = 0
    for item in views {
        addSubview(item.view)
        item.view.autoSetDimensionsToSize(CGSize(width: tabWidth, height: tabHeight))
        if i == 0 {
            item.view.autoPinEdgeToSuperviewEdge(.Leading)
        } else if let previousView = views[i-1].view {
            item.view.autoPinEdge(.Leading, toEdge: .Trailing, ofView: previousView)
        }
        if i == views.count {
            item.view.autoPinEdgeToSuperviewEdge(.Trailing)
        }
        i += 1
    }

    contentSize = CGSize(width: Double(i)*Double(tabWidth), height: Double(tabHeight))
}

Update 2

I finally got it to freeze again. The app was in the background and I brought it back and tried pushing a view controller on the stack when it froze. I noticed an animation was taking place. I have a scrollview at the top of the page that pages through its content every 10 seconds (think of the app stores top banner). On this freeze, I noticed that the banner was mid-animation.

Here's the scrolling function from the my UIScrollView that gets called every 10 seconds:

func moveToNextItem() {
    let pageWidth: CGFloat = CGRectGetWidth(frame)
    let maxWidth: CGFloat = pageWidth * CGFloat(max(images.count, profileImages.count))
    let contentOffset: CGFloat = self.contentOffset.x
    let slideToX = contentOffset + pageWidth

    //if this is the end of the line, stop the timer
    if contentOffset + pageWidth == maxWidth {
        timer?.invalidate()
        timer = nil
        return
    }

    scrollRectToVisible(CGRectMake(slideToX, 0, pageWidth, CGRectGetHeight(frame)), animated: true)
}

I don't recall ever having a push stop because of an animation/scroll taking place, but I could be wrong.

I've also rechecked the stack and the same situation as described above is still the case where [VC-Home, VC-1] is the stack and VC-2 is not pushed on. I've also gone through VC-1's variables and everything has loaded (data calls and image loads).

Update 3

This is getting stranger by the second. I've overriden pushViewController so I can put a breakpoint in there and do some debugging based on Alessandro Ornano's response. If I push a view controller unsuccessfully, then send my app to the background, put a breakpoint into the pushViewController call, and bring the app back, the breakpoint is immediately hit a number of times. If I then continue past all the hits, the next view controller suddenly becomes visible and the last view controller I tried to push is now on the stack as the last view controller. This means that the one that I see is still disabled, which essentially puts me in the same position as before.

Maxwell
  • 6,532
  • 4
  • 37
  • 55
  • Can you detail VC-1 methods like viewWillAppear, viewDidAppear or viewDidDisappear?Are there instructions in main thread that compromise layout constructions? – Alessandro Ornano Apr 12 '16 at 06:45
  • I agree @AlessandroOrnano , Are you in the same thread? – bourvill Apr 12 '16 at 09:42
  • I'm only overriding viewDidAppear, but I'm just setting a bool and bringing a subview to the front. No background threads there. I'll keep looking for anything in a background thread that could be suspicious and update my question if I find anything. – Maxwell Apr 12 '16 at 16:18
  • steadily around 80-100 MB... I don't think is normal, just if it is a game – Andrea Apr 12 '16 at 16:55
  • Could you please show some related code? – Sulthan Apr 12 '16 at 17:02
  • Added code @Sulthan. I've put what I think is relevant but if there's any specifics you'd like to see, let me know and I'll post it. – Maxwell Apr 12 '16 at 17:26

6 Answers6

51

We have faced the same problem couple of weeks back. And for our problem we narrowed it down to left-edge pop gesture recogniser. You can try and check if you can reproduce this problem using below steps

  • Try using the left edge pop gesture when there are no view controllers below it (i.e on root view controllers, your VC-Home controller)
  • Try clicking on any UI elements after this.

If you are able to reproduce the freeze, try disabling the interactivePopGestureRecognizer when the view controller stack have only one view controller.

Refer to this question for more details. Below is the code from the link for ease of reference.

- (void)navigationController:(UINavigationController *)navigationController
   didShowViewController:(UIViewController *)viewController
                animated:(BOOL)animate
{
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)])
{
        if (self.viewControllers.count > 1)
        {
            self.interactivePopGestureRecognizer.enabled = YES;
        }
        else
        {
            self.interactivePopGestureRecognizer.enabled = NO;
        }
    }
}
Community
  • 1
  • 1
Penkey Suresh
  • 5,816
  • 3
  • 36
  • 55
10

Great answer by @Penkey Suresh! Saved my day! Here's a SWIFT 3 version with a small addition that made the difference for me:

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {


    if (navigationController.viewControllers.count > 1)
    {
         self.navigationController?.interactivePopGestureRecognizer?.delegate = self
        navigationController.interactivePopGestureRecognizer?.isEnabled = true;
    }
    else
    {
         self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
        navigationController.interactivePopGestureRecognizer?.isEnabled = false;
    }
}

Just don't forget to add UINavigationControllerDelegate and set the navigationController?.delegate = self Another important part is to assign the interactivePopGestureRecognizer to self or to nil accordingly.

Tim Friedland
  • 1,045
  • 11
  • 18
  • I had to cast `self` to `UIGestureRecognizerDelegate`. Example: `self.navigationController?.interactivePopGestureRecognizer?.delegate = self as? UIGestureRecognizerDelegate` – lenooh Oct 23 '18 at 18:12
  • So this will remove the _UIParallaxView lightGray transition background from vc1 to vc2? because it is not working for me – Marlhex Aug 05 '19 at 10:57
2

I was thinking about intermittently freezing, main thread and SDWebImage.

Assuming that you using the image you downloaded from downloadImageWithURL:options:progress:completed:'s completed block .. if so, make sure you dispatch to the main queue before using using the image.

If you use the SDWebImageDownloader directly, the completion block (as you noted) will be invoked on a background queue, you can fix it using dispatch_async on the main queue from the completion.

Otherwise you can use: SDWebImageManager downloadImageWithURL:options:progress:completed: (method that invokes the completion blocks on the main queue).

if the problem persist (just because you speaking about "..some unique things about my app is that it is very image heavy..") look also Common problems expecially the Handle image refresh know problems.

Add to your check also this nice snippet code:

import UIKit.UINavigationController

public typealias VoidBlock = (Void -> Void)

public extension UINavigationController  
{
    public func pushViewController(viewController: UIViewController, animated: Bool, completion: VoidBlock) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        self.pushViewController(viewController, animated: animated)
        CATransaction.commit()
    }
}

maybe can help to understand if pushViewController finish, if finish with all viewControllers expected ..

Another test I try to make is to launch the app with iOS 8.x and iPhone 6+, because there are some issues in the pureLayout project around iOS 9. Can you send feedbacks around this test?

I've some suspicious also on the real scrollview dimension before the pushview action, can you analyze the current view by examing the view hierarchy?

Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
  • I'm using ```imageView.sd_setImageWithURL(url)``` for all my image loading which runs the completion block on the main thread for me, so I wouldn't expect any freezing to occur there. – Maxwell Apr 12 '16 at 16:40
  • Update: I've put in your code and the completion block prints to the console that the transition finished. I got the navigation controller to freeze, but I can still try to push new view controllers onto the stack from one of my other containers. When I do that, the completion block is executed, but the print statement never shows up in the console. – Maxwell Apr 12 '16 at 19:03
0

Please check if you have any unnecessary codes like below in your BaseNavigationViewController :

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

Here I have a sample code which reproduces this issue (freezing app in pushing VC) that you have to reproduce it by these steps:

  • Try using the (left | right) edge pop gesture when there are no view controllers below it (i.e on root view controllers, your VC-Home controller)
  • Try clicking on any UI elements after this(which pushes you to next ViewController).

Sample Code: https://github.com/aliuncoBamilo/TestNavigationPushBug

Aliunco
  • 399
  • 2
  • 9
0

My case solved by restarting Xcode and simulator, then choosing a different device from simulators list.

Okhan Okbay
  • 1,374
  • 12
  • 26
0

Answer by @Penkey Suresh! and @Tim Friedland helped me alot, one thing that i had to do extra for my usecase, might help someone else too.

USE CASE: I had tab bar controller and i wanted swipe-back-gesture on screens let's say i'm on tabA and opened VC1, VC2 from there. My swipe gesture was working correctly on VC1 and VC2 but for some reason it was not disabling when coming back to tabA which is why it was freezing when trying to swipe left from there and trying to click somewhere (as mentioned by @Pankey Suresh)

SOLUTION IN SWIFT 5 I have this custom class implemented:

class BaseSwipeBack: UIViewController, UIGestureRecognizerDelegate {
    func swipeToPop(enable: Bool) {
        if enable && (navigationController?.viewControllers.count ?? 0 > 1){
            self.navigationController?.interactivePopGestureRecognizer?.delegate = self
            navigationController?.interactivePopGestureRecognizer?.isEnabled = true;
        }
        else {
            self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
            navigationController?.interactivePopGestureRecognizer?.isEnabled = false;
        }
    }
}

USAGE

  • VC1 class - make it sub class of BaseSwipeBack that we just created so that you can access the functions

     class VC1: BaseSwipeBack {
     override func viewDidLoad() {
    
     }
    
     override func viewDidAppear(_ animated: Bool) {
         self.swipeToPop(enable: true)
     }
     deinit {
         self.swipeToPop(enable: false)
     }
     }
    
  • tabAController class - although i have disabled gesture in deiniting VC1 but it was not working fine so i had to disable it again tabAController

     class tabAController: BaseSwipeBack {
     override func viewDidLoad() {
    
     }
     override func viewDidAppear(_ animated: Bool) {
         self.swipeToPop(enable: false)
     }
     }
    
Malik Kulsoom
  • 136
  • 2
  • 6