Related
I implemented a basic UIViewController with a UITableView that's wrapped in a UINavigationController. I set prefersLargeTitles to true:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "Coffees"
}
However, the title stays small until I scroll the view, at which point it enlarges. I tried moving that call to where I create the UINavigationController, but there was no effect. I am sure the navigationController is not nil when I set prefersLargeTitles.
Should I be updating that property elsewhere? Or should I file a Radar?
Update:
This only seems to happen if my view contains a UITableView or is itself a UITableViewController
I recently hit the same issue and none of the suggestions worked for me. Instead, all I needed to do was to invoke sizeToFit(). Sample code:
private func configureNavigator() {
guard let navigationController = navigationController else { return }
navigationController.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .automatic
navigationController.navigationBar.sizeToFit()
}
I hope this helps!
For me the only working solution is:
DispatchQueue.main.async { [weak self] in
self?.navigationController?.navigationBar.sizeToFit()
}
in
viewWillAppear()
I had the same issue only on one tableview ...
I had to set :
self.tableView.contentInsetAdjustmentBehavior = .never
so that my tableview stop scrolling when uiviewcontroller was loaded.
It's the tableview automatic scrolling that makes the large title being hidden
Hope this helps
I had the same problem. Although you are not using Storyboards but I hope this could help someone. I checked "Prefer Large Titles" for the Navigation Controller (not the View Controller) I embedded my TableViewController in. All the View Controllers after the Navigation Controller turned and had large titles, and it should work.
Same issue with Swift 5.2
my view contains tableView and prefersLargeTitles is not updating until scroll, I fixed it by setting
self.tableView.contentInsetAdjustmentBehavior = .never
Modifying the contentInset of the tableView with top:1 will force the NavigationBar to expand and display the large titles.
Obj-C
-(void) viewWillAppear:(BOOL)animated {
if (#available(iOS 11.0, *)) {
tableView.contentInset = UIEdgeInsetsMake(1, 0, 0, 0);
}
}
Swift
override func viewWillAppear(_ animated: Bool) {
if #available(iOS 11.0, *) {
tableView.contentInset = UIEdgeInsetsMake(1, 0, 0, 0)
}
}
Note: If you have a tableView.reloadData() in your viewWillAppear make sure to call it after editing the contentInset
In my case the solution was to set tableView's top align to Safe Area and not Superview
I ran into the same issue and found that it’s usually best to set the prefersLargeTitles property from the view controller or object that sets it up, and to do so before it is presented.
For instance, if the view controller in question is shown upon app launch:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
let someViewController: UIViewController = CustomViewController()
let theNavController = UINavigationController(rootViewController: someViewController)
theNavController.navigationBar.prefersLargeTitles = true
window.rootViewController = theNavController
window.makeKeyAndVisible()
return true
}
or if presenting a particular view controller:
let someViewController: UIViewController = CustomViewController()
let theNavController = UINavigationController(rootViewController: someViewController)
theNavController.navigationBar.prefersLargeTitles = true
present(theNavController, animated: true, completion: nil)
I found this method to be a more sure-fire way to ensure that the navigation title is displayed accordingly. Hope this helps! :)
I have wasted some considerable amount of time on this as prefersLargeTitle saga works on some view controllers as expected and with some it produces the same issue above.
Solution for me was to uncheck Extended Edges Under Top Bars in IB - for those view controllers who show large title momentarily until the contents of the table view are loaded then navigation bar jumps back up to regular size. It only shows the large title when scrolling the table view down.
This is backward compatible with iOS 10 and does not leave any empty space above the first row in the table view.
I had checked prefersLargeTitle on the navigation controllers attributes inspector only in IB - nothing in code. Same for largeTitleDisplayMode = .always
As for why this happens with some view controllers and not others, I have absolutely no idea!
In the storyboard I set the Navigation Item's Large Title to Never.
In my ViewController's viewDidLoad method I set the following:
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
Programmatically:
In AppDelegate.swift:
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
let navigationController = UINavigationController.init(rootViewController: ViewController())
window?.rootViewController = navigationController
In ViewController:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .automatic
}
override func loadView() {
super.loadView()
view.addSubview(tableView)
view.addSubview(loadingView)
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor),
tableView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor)
])
}
Make sure your tableView has beed previously added to your view.
I tried all of the other answers but what worked for me was to set the content offset of my UITableView to CGPoint(x: 0, y: -1) in viewDidLoad:
tableView.setContentOffset(CGPoint(x: 0, y: -1), animated: false)
I just had this same issue and, in my case, it turns out that the Storyboard structure that was working in iOS 10 with Swift 3 (and also works with iOS 11 with Swift 3) was causing the issue on iOS 11 with Swift 4.
To elaborate:
I had a regular UIViewController in my storyboard that I had set to a UINavigationController subclass (my hierarchy is similar to yours, with UITabBarController subclass → UINavigationController subclass → UITableViewController subclass).
In iOS 10, this worked fine.
In iOS 11, this also works fine when you run the existing Swift 3 app.
However, with the Swift 4 app, running on iOS 11, I was seeing the same symptoms you described (large titles only appear when you pull/scroll the view down).
To fix, I replaced the UIViewController-based elements in the Storyboard with actual UINavigationController instances (which contain a UINavigationBar explicitly in the Storyboard – I have a hunch this is where the crux of the issue stems from, as the UIViewController instances didn’t have that element explicitly declared within the Storyboard).
Anyway, that fixed the issue for me.
I’ll file radar as this looks like a Swift 4-based regression as, for me, it works both in iOS 10 with Swift 3 and in iOS 11 with Swift 3.
General changing the behaviour of the navigationBar should be done in viewWillAppear(_:)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.prefersLargeTitles = true
}
After doing that it worked fine for me.
One more possible solution is to end refresh in your refreshHandler(). like this-
#objc func refreshPage() {
self.refreshControl?.endRefreshing() //End here
self.loadTableData() //Get fresh data and reload table
}
I solved this issue via storyboard
Navigation Controller -> Navigation Bar -> Attributes inspector -> Prefers Large Titles(Checked)
View Controller -> Navigation Item -> Attributes inspector -> Large Title (Automatic or Always checked)
I think It does seem a little bit dummy but I effectively solved the problem with this:
self.navigationItem.prompt = ""
self.navigationItem.prompt = nil
It's like navigationBar needs a sort of update in one of its elements to update the layout.
Sometimes to update something in navigationBar I need to hide and unhide it.. That's why I think there is a best way to do it.. For the moment that's my workaround.
I had a similar issue with navigation bar, but in my case it had a custom title view, and navigation bar remained empty until table view is scrolled down, which triggered UILayoutContainerView to layout its subviews, one of which are navigation controller's view and navigation bar. I assume the root of it is the same as the large title navigation bar issue.
Anchoring tableView to the safeAreaLayoutGuide didn't work out for me, largeTitleDisplayMode couldn't be other then .never
So I managed to fix it by calling self.navigationController?.view.setNeedsUpdateConstraints in the top presented controller's viewDidAppear(animated:) function, or scheduling this call for the next run loop in viewWillAppear(animated:), like:
DispatchQueue.main.async {
self.navigationController?.view.setNeedsUpdateConstraints()
}
In this case, navigation bar appeared with the correct content and size along with presenting transition, instead of popping in after transition was completed
What worked for me is setting the self.navigationController?.navigationBar.prefersLargeTitles = true before calling tableviews reload method.
The answers above for tableview also works for collection view if parent view not scrolled before navigation:
self.collectionView.contentInsetAdjustmentBehavior = .never
If parent view scrolled before navigation the solution above did not work in my case. I had to add the answer above as:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.sizeToFit()
}
That does seem like a weird behavior at first, but try setting the navigationItem.largeTitleDisplayMode to always. The default is automatic - and it's not defined how that works in the docs.
Also wrote / will update an answer about large titles here.
I had the similar issue. The view is a table view. The property of prefersLargeTitles is set at viewDidLoad event. Then I set view's title in viewWillAppear event.
override open func viewDidLoad() {
if #available(iOS 11.0, *) {
self.navigationController?.navigationBar.prefersLargeTitles = true
} else {
// Fallback on earlier versions
}
...
}
override open func viewWillAppear(_ animated: Bool) {
self.navigationItem.title = "something"
...
}
In my prepare segue event, I set navigation item's tile to nil so that the next view left navigation var item displays "Back" automatically.
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
self.navigationItem.title = nil
...
}
The first time the table view displays large title correctly. However, if I select a row to next view and back to the table view, the navigation item's title becomes empty.
After several hours' struggling, I finally find out that the view's title should be set in viewDidAppear event! It seems that whatever set to view's title in Will event would be reset by UIKit internally back to nil. So it has to be set in a different event.
override func viewDidAppear(_ animated: Bool) {
self.navigationItem.title = "something"
...
}
override open func viewWillAppear(_ animated: Bool) {
// self.navigationItem.title = "something" // Remove it and set title in Did event!
...
}
Before I introduced this iOS 11 new feature, my app runs OK. It seems that the new feature has some changes in UIKit so that the previous version app may need some updates/changes to make it work.
I had the same issue and fixed it by changing order of views in my ViewController in InterfaceBuilder.
It seems like if the first view in Hierarchy is NOT a ScrollView then NavigationBar appears in LargeTitle mode and does not animates together with scroll view. If you need to have Navigation Bar title to reflect your scroll then you need to put your scroll view as the first in view hierarchy.
Also, I am not completely sure in this but looks like Navigation Bar appearance in standard or Large Title mode depends on views hierarch of previous Controller.
Similar issue for me with a UITableViewController added to a UIViewController. In my instance, these view controllers are themselves embedded in a UITabBarController and only the first tab displayed correctly used a large title. Other tabs required a manual scroll before the large title was displayed.
The only thing I could get to work was adjusting the contentInset as per #pau-senabre's answer, except I the top inset wasn't helpful for me. Instead, I set the left inset and then reset it on the next runloop.
private var isFirstAppearance = true
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if isFirstAppearance {
applyLargeTitlesFix()
}
}
private func applyLargeTitlesFix() {
let originalInset = tableViewController.tableView.contentInset
tableViewController.tableView.contentInset = UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 0)
DispatchQueue.main.async { [weak self] in
self?.tableViewController.tableView.contentInset = originalInset
}
isFirstAppearance = false
}
Same issue here with Swift 4.2, iOS 12, and refactored Storyboards.
Tried adding prefersLargeTitles = true to viewWillAppear and viewDidLoad, but neither fixed my issue.
Instead, I copied the refactored storyboards back into main.storyboard and found the option to enable large titles in IB. Set that option, then refactored the storyboards back out and everything is working now. For some reason, the initial refactoring stripped out the option and I couldn't enable it programmatically.
I had the same issue (iOS 14, Xcode 12.2).
It only affected navigation controllers displaying table views.
I had originally set tableView.tableFooterView = UIView() to get rid of extra separators after the last cell. Setting the footer view to nil fixed the scrolled-up navigation title.
I had same issue.
I have set below code in viewdidload method. and it get fixed.
self.navigationController?.navigationBar.prefersLargeTitles = true
self.navigationItem.largeTitleDisplayMode = .never
Issue:
Modally presented view controller does not move back up after in-call status bar disappears, leaving 20px empty/transparent space at the top.
Normal : No Issues
In-Call : No Issues
After In-Call Disappears:
Leaves a 20px high empty/transparent space at top revealing orange view below. However the status bar is still present over the transparent area. Navigation Bar also leaves space for status bar, its' just 20px too low in placement.
iOS 10 based
Modally presented view controller
Custom Modal Presentation
Main View Controller behind is orange
Not using Autolayout
When rotated to Landscape, 20px In-Call Bar leaves and still leaves 20px gap.
I opt-out showing status bar in landscape orientations. (ie most stock apps)
I tried listening to App Delegates:
willChangeStatusBarFrame
didChangeStatusBarFrame
Also View Controller Based Notifications:
UIApplicationWillChangeStatusBarFrame
UIApplicationDidChangeStatusBarFrame
When I log the frame of presented view for all four above methods, the frame is always at (y: 0) origin.
Update
View Controller Custom Modal Presentation
let storyboard = UIStoryboard(name: "StoryBoard1", bundle: nil)
self.modalVC = storyboard.instantiateViewController(withIdentifier: "My Modal View Controller") as? MyModalViewController
self.modalVC!.transitioningDelegate = self
self.modalVC.modalPresentationStyle = .custom
self.modalVC.modalPresentationCapturesStatusBarAppearance = true;
self.present(self.modalVC!, animated: true, completion: nil)
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
toViewController!.view.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [.curveEaseOut], animations: { () -> Void in
toViewController!.view.transform = CGAffineTransform.identity
}, completion: { (completed) -> Void in
transitionContext.completeTransition(completed)
})
}
I've been looking for a solution for 3 days. I don't like this solution but didn't found better way how to fix it.
I'he got situation when rootViewController view has bigger height for 20 points than window, when I've got notification about status bar height updates I manually setup correct value.
Add method to the AppDelegate.swift
func application(_ application: UIApplication, didChangeStatusBarFrame oldStatusBarFrame: CGRect) {
if let window = application.keyWindow {
window.rootViewController?.view.frame = window.frame
}
}
After that it works as expected (even after orientation changes).
Hope it will help someone, because I spent too much time on this.
P.S. It blinks a little bit, but works.
I faced this problem too but after I put this method, problem is gone.
iOS has its default method willChangeStatusBarFrame for handling status bar. Please put this method and check it .
func application(_ application: UIApplication, willChangeStatusBarFrame newStatusBarFrame: CGRect) {
UIView.animate(withDuration: 0.35, animations: {() -> Void in
let windowFrame: CGRect? = ((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame
if newStatusBarFrame.size.height > 20 {
windowFrame?.origin?.y = newStatusBarFrame.size.height - 20
// old status bar frame is 20
}
else {
windowFrame?.origin?.y = 0.0
}
((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame = windowFrame
})
}
Hope this thing will help you.
Thank you
I had the same issue with the personnal hospot modifying the status bar.
The solution is to register to the system notification for the change of status bar frame, this will allow you to update your layout and should fix any layout issue you might have.
My solution which should work exactly the same for you is this :
In your view controller, in viewWillAppear suscribe to the UIApplicationDidChangeStatusBarFrameNotification
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(myControllerName.handleFrameResize(_:)), name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
Create your selector method
func handleFrameResize(notification: NSNotification) {
self.view.layoutIfNeeded() }
Remove your controller from notification center in viewWillDisappear
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
You also need your modal to be in charge of the status bar so you should set
destVC.modalPresentationCapturesStatusBarAppearance = true
before presenting the view.
You can either implement this on every controller susceptible to have a change on the status bar, or you could make another class which will do it for every controller, like passing self to a method, keep the reference to change the layout and have a method to remove self. You know, in order to reuse code.
I think this is a bug in UIKit. The containerView that contains a presented controller's view which was presented using a custom transition does not seem to move back completely when the status bar returns to normal size. (You can check the view hierarchy after closing the in call status bar)
To solve it you can provide a custom presentation controller when presenting. And then if you don't need the presenting controller's view to remain in the view hierarchy, you can just return true for shouldRemovePresentersView property of the presentation controller, and that's it.
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool {
return true
}
}
or if you need the presenting controller's view to remain, you can observe status bar frame change and manually adjust containerView to be the same size as its superview
class PresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
NotificationCenter.default.addObserver(self,
selector: #selector(self.onStatusBarChanged),
name: .UIApplicationWillChangeStatusBarFrame,
object: nil)
}
#objc func onStatusBarChanged(note: NSNotification) {
//I can't find a way to ask the system for the values of these constants, maybe you can
if UIApplication.shared.statusBarFrame.height <= 20,
let superView = containerView?.superview {
UIView.animate(withDuration: 0.4, animations: {
self.containerView?.frame = superView.bounds
})
}
}
}
I've been looking for a solution to this problem. In fact, I posted a new question similar to this one. Here: How To Avoid iOS Blue Location NavigationBar Messing Up My StatusBar?
Believe me, I've been solving this for a couple of days now and it's really annoying having your screen messed up because of the iOS's status bar changes by in-call, hotspot, and location.
I've tried implementing Modi's answer, I put that piece of code in my AppDelegate and modified it a bit, but no luck. and I believe iOS is doing that automatically so you do not have to implement that by yourself.
Before I discovered the culprit of the problem, I did try every solution in this particular question. No need to implement AppDelegate's method willChangeStatusBar... or add a notification to observe statusBar changes.
I also did redoing some of the flows of my project, by doing some screens programmatically (I'm using storyboards). And I experimented a bit, then inspected my previous and other current projects why they are doing the adjustment properly :)
Bottom line is: I am presenting my main screen with UITabBarController in such a wrong way.
Please always take note of the modalPresentationStyle. I got the idea to check out my code because of Noah's comment.
Sample:
func presentDashboard() {
if let tabBarController = R.storyboard.root.baseTabBarController() {
tabBarController.selectedIndex = 1
tabBarController.modalPresentationStyle = .fullScreen
tabBarController.modalTransitionStyle = .crossDissolve
self.baseTabBarController = tabBarController
self.navigationController?.present(tabBarController, animated: true, completion: nil)
}
}
I solve this issue by using one line of code
In Objective C
tabBar.autoresizingMask = (UIViewAutoResizingFlexibleWidth | UIViewAutoResizingFlexibleTopMargin);
In Swift
self.tabBarController?.tabBar.autoresizingMask =
UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleTopMargin.rawValue)))`
You just need to make autoresizingMask of tabBar flexible from top.
In my case, I'm using custom presentation style for my ViewController.
The problem is that the Y position is not calculated well.
Let's say the original screen height is 736p.
Try printing the view.frame.origin.y and view.frame.height, you'll find that the height is 716p and the y is 20.
But the display height is 736 - 20(in-call status bar extra height) - 20(y position).
That is why our view is cut from the bottom of the ViewController and why there's a 20p margin to the top.
But if you go back to see the navigation controller's frame value.
You'll find that no matter the in-call status bar is showing or not, the y position is always 0.
So, all we have to do is to set the y position to zero.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let f = self.view.frame
if f.origin.y != 0 {
self.view.frame = CGRect(x: f.origin.x, y: 0, width: f.width, height: f.height)
self.view.layoutIfNeeded()
self.view.updateConstraintsIfNeeded()
}
}
Be sure to set the frame of the view controller's view you are presenting to the bounds of the container view, after it has been added to the container view. This solved the issue for me.
containerView.addSubview(toViewController.view)
toViewController.view.frame = containerView.bounds
My initial view controller is a navigation controller and its root view controller is a UIViewController which conforms to the UIPageViewControllerDataSource protocol. The content for my pages are three unique view controllers which have both a scene in the storyboard and a swift file.
I am hiding the navigation bar in the root view controller's viewDidLoad().
self.navigationController?.navigationBar.hidden = true
When the app launches, the status bar is white and opaque. As soon as I scroll to the next page, it becomes translucent and the background color of the page shows through.
Appearance on Launch
Appearance While Swiping to Next Page
Can someone please help me understand what is happening here and how to fix it? In case it is not obvious, I want the second behavior from launch; the status bar is translucent and the page background shows through.
I have tried
In viewDidLoad(): UIApplication.sharedApplication().statusBarStyle = .LightContent
In the root view controller:
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .Default
}
Followed by self.setNeedsStatusBarAppearanceUpdate() in viewDidLoad()
In the root view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
self.navigationController?.navigationBar.barStyle = .Default
self.navigationController?.navigationBar.barTintColor = UIColor.clearColor()
}
In viewDidLoad() (above navigationBar.hidden):
self.navigationController?.navigationBar.barStyle = .Default
self.navigationController?.navigationBar.barTintColor = UIColor.clearColor()
As a note, when I remove the navigation controller and just make the root view controller the initial view controller, the status bar appears as expected- translucent.
This question is similar, but none of the solutions worked, it is over a year old, and when I contacted the poster, he said that he thought he put a view underneath where the status bar would be. I'm not sure that I can manage the the view in such a way that it works seamlessly with the scroll aspect of the page view controller.
I found a solution. A friend of mine made a suggestion which led me to start thinking about the frame of the UIPageViewController. This led me to grab the height of the status bar and then adjust the frame down by that much.
In viewDidLoad:
let pageViewController = self.storyboard?.instantiateViewControllerWithIdentifier("PageViewController") as! UIPageViewController
// More pageViewController setup
let statusBarHeight: CGFloat = UIApplication.sharedApplication().statusBarFrame.height
let x = pageViewController.view.bounds.origin.x
let y = pageViewController.view.bounds.origin.y - statusBarHeight
let width = pageViewController.view.bounds.width
let height = pageViewController.view.bounds.height + statusBarHeight
let frame = CGRect(x: x, y: y, width: width, height: height)
pageViewController.view.frame = frame
I still do not understand why this needs to be manually adjusted. (Hopefully someone else will come along with a solution/explanation.) But I hope this bit of code helps someone.
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.
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;
}
}
}
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.
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?
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
My case solved by restarting Xcode and simulator, then choosing a different device from simulators list.
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)
}
}
I have a UIViewController that is presented in two ways, either modally or pushed on top of a navigation controller stack. The UIViewController contains a UITableView and a UIToolbar. When presented modally, i needed a way of showing a title for the ViewController, so I added in another UIToolbar, topToolbar. My problem is, whenever I push the UIViewController, I don't need topToolbar anymore, since the navigation tabbar already shows the title. When I set topToolbar's hidden property to true, however, my UITableView is not bound to the bottom of the navigation tab bar and there's space between the UITableView and the navigation tabbar, which doesn't look so good. I tried to call removeFromSuperview() on topToolbar instead of setting its hidden property to true, but that didn't work out, and topToolbar appeared under the navigation bar, and now i have two titles instead of one. Any idea on how this can be done? I can't add pictures, but here's my code for manipulating the appearance of the UIViewController based on whether it's presented modally or pushed on top of the navigation stack:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if itemBought != nil {
cart.items.append(itemBought!)
}
totalView.layer.borderColor = UIColor.grayColor().CGColor
totalView.layer.borderWidth = 0.5
totalLabel.text = "$" + String(format: "%.2f", cart.getTotal())
if let navBar = self.navigationController?.navigationBar {
//hide toolbar and tabbar
topToolbar.removeFromSuperview()
self.tabBarController?.tabBar.hidden = true
//hide shop button
var bottomItems: [UIBarButtonItem] = bottomToolbar.items as! [UIBarButtonItem]
if let index = find(bottomItems, shopToolbarButton) {
bottomItems.removeAtIndex(index)
}
bottomToolbar.items = bottomItems
}
}
I should also mention that i have a constraint on the UITableView that's basically: distance between UItableView.top and Top Layout Guide.Bottom is <= to the height of topToolbar, which is 44.
Any ideas?
When you present the View Controller modally, why not put it in a UINavigation Controller?
let navigationController = UINavigationController(rootViewController: myViewControllerInstance)
self.navigationController?.presentViewController(navigationController, animated: true, completion: { () -> Void in
//do something here when animation is complete if you want
})