I'm building an app where two view controllers share a UIView subclass as the main source of UI. It works perfectly when the app is starting, but if I navigate away from the initial view, and return to it, all of the UI is lost. What do I need to do to preserve the views UI post-navigation?
My app flow is: MainView -> TableView -> DetailView
Just going from Main to Table to Main itself makes the UI vanish.
(rank isn't 10 yet, so here's a link to view: https://gfycat.com/enormousanchoredindochinesetiger)
What I do is load the UI in the UIView class through layoutSubviews, and in the UIViewControllers I set the instantiate the class, UI in the loadViews method by saying view = viewClass. I've tried adding this (view = viewClass) to viewWillAppear() as well, but it does nothing.
I've also tried creating two unique view classes in case instantiating was a problem. It didn't change anything.
ViewController:
override func loadView() {
super.loadView()
view = baseView
view.backgroundColor = .white
self.navigationController?.isNavigationBarHidden = true
requestLaunchData()
setButtonTargets()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
view = baseView
}
//How I push to the next view
#objc func upcomingButtonTapped() {
let vc = TableViewController()
navigationController?.pushViewController(vc, animated: true)
vc.upcomingLaunches = upcomingLaunches
}
UIView:
class BaseView: UIView {
//Lots of labels and buttons instantiated
override func layoutSubviews() {
super.layoutSubviews()
setUI() //adding subviews
}
//Layout configurations
}
Before it was this structure, I had all the UI (labels, buttons, a map) directly created and configured in each ViewController, which made both massive. But, it also worked.
I solved it after a night's rest.
So here's how you need to use a custom UIView class as your ViewController's view:
class YourView: UIView {
//Create your properties, views, etc.
let exampleView = UIView()
override layoutSubviews(){
super.layoutSubviews()
addSubview(exampleView)
//Add layouts, etc.
}
And then in your ViewController, in either viewDidLoad, or loadViews (like me here):
let customView = YourView()
override func loadView() {
super.loadView()
view = customView //Sets the entire view to all the UI you created in the custom class
}
The FATAL mistake I made was this:
override layoutSubviews(){
super.layoutSubviews()
if let sView = superview { //This gives you frame size and such
sView.addSubview(exampleView)
}
}
This sets the UI's memory to the SuperView, which gets lost the moment you leave that view, because of view = customView. So my controller was rendering view = customView, which was empty, because all the UI was set to the superView which was superseded by customView.
I hope that helps anyone trying to use this same architecture in the future.
When I use storyboard segue, it is pretty smooth showing another viewcontroller onscreen. However, when I'm not using storyboard, just add a simple line of code with navigationController?.pushViewController(UIViewController(), animated: true), it's a little bit lagging in transition.
Also I read about Delay when pushing view controller (iOS). But even when I'm pushing an brand new viewcontroller (no extra code inside), the transition is still a little bit lagging, any idea?
Swift 5.3, Xcode 12
I was facing this same issue, tried adding to main thread, but apparently the problem was I was pushing the viewController created programatically, and it backgroundColor was nil.
Just by setting the color in pushedViewController to something else, solved the issue.
self.view.backgroundColor = yourColor
In app delegate, set your window's background color to red.
window?.backgroundColor = .red
Also in the the pushed view controller, set its view to red.
view.backgroundColor = .red
I experienced the same issue when programmatically embedding my view controller in a UINavigationController.
I came across the same issue when I was drawing UI programmatically.
In my case, overriding loadView function solved my problem.
import UIKit
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "My tasks"
}
override func loadView() {
// You can load your view here. For simplicity, I just give it a background color
let v = UIView()
v.backgroundColor = UIColor.orange
view = v // This line does the magic
}
}
Viewcontroller is expecting to render view so add below view background color or set title
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Title"
self.view.backgroundColor = .red
}
I debug with slow animation on simulator. The "lagging" is the underlying previous view controller’s view as #yesleon mentioned. I set the previous view's alpha to 0 when viewWillDisappear, and 1 when viewWillAppear. It seems much better when pushing to the new controller now, but when pushing back to original view controller, it still a little bit not perfect, any better solution?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = false
view.alpha = 1.0
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.tabBarController?.tabBar.isHidden = true
view.alpha = 0.0
}
#IBAction func barbuttonDidTapped(_ sender: UIBarButtonItem) {
navigationController?.pushViewController(UIViewController(), animated: true)
}
I met the same issue, put ui code and its constraints inside viewDidLoad into loadView() fix it. Now the animation is moving smoothly.
Refer here: https://stackoverflow.com/a/3424052/10158398
loadView is the method that actually sets up your view (sets up all
the outlets, including self.view).
viewDidLoad you can figure out by its name. It's a delegate method
called after the view has been loaded (all the outlets have been set)
that just notifies the controller that it can now start using the
outlets.
viewDidLoad: "This method is called after the view controller has
loaded its associated views into memory. This method is called
regardless of whether the views were stored in a nib file or created
programmatically in the loadView method."
loadView: "If you create your views manually, you must override this
method and use it to create your views."
For an example:
class DetailViewController: UIViewController {
let imageView = UIImageView()
var picture: Picture?
override func viewDidLoad() {
super.viewDidLoad()
}
override func loadView() {
view = UIView()
view.backgroundColor = .white
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
view.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
if let picture = picture {
let path = getDocumentDirectory().appendingPathComponent(picture.picture)
imageView.image = UIImage(contentsOfFile: path.path)
}
}
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
This question already has answers here:
Removing the title text of an iOS UIBarButtonItem
(39 answers)
Closed 7 years ago.
I do not want to see the "Back" text that is displayed automatically when going to a view controller with no Title. I would like this to happen everywhere on my application, is there a few lines of code I can put in the app delegate to make this possible?
I have tried a few approaches from SO before posting this question and have found no success. I want to this in Swift, not Obj-c.
I tried this with no success; it ran fine, but the back text was still displayed in the next view controller.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let backItem = UIBarButtonItem()
backItem.title = "Something Else"
navigationItem.backBarButtonItem = backItem // This will show in the next view controller being pushed
}
I would like to put this in app delegate so it would happen throughout my application rather than having to put that into every single swift file in my project. Anybody have any ideas?
There are several different ways to do this. I’ll present a solution that gives you a bit more flexability as you’ll want to display a button some times, hide other times, and customize at other times. This example assumes you are using Navigation Controller based UI, but can be adapted to other types.
Whenever I create an app I like to create my own UIViewController class and have all of my UIViewControllers inherit from this single view controller. If I want to customize something for all my UIViewControllers I can do this at my new super class level since all the other views inherit from that.
In my sample code below I create my custom UIViewController called MasterViewController. I have all of my UIViewControllers inherit from it: ViewController, ViewController2 & ViewController3. Read the notes in the code below to understand what’s going on.
NOTE: Make sure you check for the case where the UIViewController should not have the back button as this would be the case where there is nothing to return to. You can add this code to MasterViewController but I did not for this sample.
//=========================================================
//MasterViewController
//=========================================================
// Master View Controller that all UIViewControllers inherit from.
import UIKit
class MasterViewController: UIViewController {
// This will be title if the user doesn't change it in a subclass.
var backButtonTitle = "Default Back Title"
override func viewDidLoad() {
// This resets the default back button to always be shown unless the user calls createCustomBackButtonWithTitle from child class.
self.navigationItem.hidesBackButton = false
super.viewDidLoad()
}
func createCustomBackButtonWithTitle(customTitle: String) {
// Hide the default back button.
self.navigationItem.hidesBackButton = true
// Programmatically create custom back button.
let backButton = UIBarButtonItem(title: customTitle, style: UIBarButtonItemStyle.Plain, target: self, action: "goBack:")
self.navigationItem.leftBarButtonItem = backButton
}
#IBAction func goBack(sender: UIButton!) {
navigationController?.popViewControllerAnimated(true)
}
}
//=========================================================
//ViewController
//=========================================================
// This view controller customizes the back name and inherits the default "go back" behavior from MasterViewController.
import UIKit
class ViewController: MasterViewController {
override func viewDidLoad() {
super.viewDidLoad()
createCustomBackButtonWithTitle("ButtonName")
}
}
//=========================================================
//ViewController2
//=========================================================
// This view controller customizes the back name but overrides the default "goBack" behavior from MasterViewController to do something different
import UIKit
class ViewController2: MasterViewController {
override func viewDidLoad() {
super.viewDidLoad()
createCustomBackButtonWithTitle("ButtonName 2")
}
// Override the back button behavior from super class because we don't want default "back" behavior.
#IBAction override func goBack(sender: UIButton!) {
// Costume Code here
}
}
//=========================================================
//ViewController3
//=========================================================
// This view controller inherits from MasterViewController, but gets the default "back" behavior from a normal
// UINavigationController since it never calls createCustomBackButtonWithTitle to change the behavior
import UIKit
class ViewController3: MasterViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
Most people wind up doing something like #xdeleon suggested, but I highly recommend not doing that, if for no other reason that that you'll break the built-in swipe behavior to navigate backwards in the view controller stack. There are a couple answers on SO that tell you how to restore this back-swipe functionality, but as someone who's had to fix this in an existing app with 200+ view controllers, I'd like to save you a lot of pain.
If you don't mind working in storyboards, then make sure that each of your view controllers' navigation item's back button text is an empty string. However, since that's a lot to keep track of, it's marginally easier to do in code, and the only way I've found works perfectly is to create a cachedTitle instance variable on your view controller, and implement the view controller's viewWillAppear() and viewWillDisappear() methods like so:
override func viewWillAppear(animated: Bool) {
// Reset the view controller's title only if it doesn't already have one.
if (navigationItem.title == nil || navigationItem.title == "") && navigationItem.titleView == nil {
navigationItem.title = cachedTitle
}
}
override func viewWillDisappear(animated: Bool) {
if navigationItem.title != nil { // not 'if let'!
cachedTitle = navigationItem.title
navigationItem.title == ""
}
}
You can either put these methods in a UIViewController subclass that all of your view controllers extend, or, better yet, put these in custom methods in a category on UIViewController, and just call them in your view controllers' viewWillAppear() and viewWillDisappear() calls.
If you want to use something other than the back button's '<' symbol, don't add a custom back or left button; just set a custom back indicator image and mask on the navigation bar.