I'm currently learning about Swift and UIKit as I'm trying to make a tvOS app.
I am trying to stay away from Storyboards and so I'm doing this with just code.
I think this is more of a code design question though.
Let's say I have an OverlayVC class, derived from UIViewController (It's a VC which is presented modally on top of some existing context). That class has a redefined loadView method where I instantiate my own OverlayView class, which derives from UIView. I'm trying to strictly separate the View from the View controller.
Now, in that OverlayView constructor, I instantiate a UIVisualEffectView, and set the constraints so that it takes the entire frame. The goal is to make something appear on top of the existing context with a blur effect.
So far so good, when the OverlayView is being presented, I get everything blurry.
Now, I want to have a tab bar controller inside this, so I want to make a class that derives from UITabBarController and instantiate it in my OverlayView.
But I need to give a View to the UIVisualEffectView through its contentView attribute.
The easy way for me to do this would be to instantiate my TabBarVC within the OverlayView class. But I feel like it doesn't belong there, but should instead be held within the OverlayVC class. Problem is that the UIVisualEffectView is instantiated in the OverlayView, not the VC, so setting its contentView in the VC isn't straightforward.
I've been turning this around in my head a few times and can't actually figure out what is the proper class design for this.
Any suggestion would be appreciated. Thanks !
Below is a simplified snippet of how I have done this right now:
class TabBarViewController: UITabBarController {
convenience init() {
self.init(nibName: nil, bundle: nil)
let vc1 = ViewController1()
let vc2 = ViewController2()
self.viewControllers = [vc1, vc2]
}
}
class OverlayView: UIView {
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
let dimmingView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.clearsContextBeforeDrawing = false
self.isOpaque = false
blurView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurView)
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.75
dimmingView.translatesAutoresizingMaskIntoConstraints = false
addSubview(dimmingView)
NSLayoutConstraint.activate([
blurView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
blurView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
blurView.topAnchor.constraint(equalTo: self.topAnchor),
blurView.heightAnchor.constraint(equalToConstant: 750),
])
NSLayoutConstraint.activate([
dimmingView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
dimmingView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
dimmingView.topAnchor.constraint(equalTo: blurView.bottomAnchor),
dimmingView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class OverlayViewController: UIViewController {
convenience init() {
self.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .overCurrentContext
}
override func viewDidLoad() {
super.viewDidLoad()
let tabBarController = TabBarViewController()
addChild(tabBarController)
tabBarController.view.translatesAutoresizingMaskIntoConstraints = false
(view as! OverlayView).blurView.contentView.addSubview(tabBarController.view)
NSLayoutConstraint.activate([
tabBarController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tabBarController.view.topAnchor.constraint(equalTo: view.topAnchor),
tabBarController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
tabBarController.didMove(toParent: self)
}
override func loadView() {
view = OverlayView(frame: UIScreen.main.bounds)
}
}
So, here I have a clear split between the Overlay view and its View controller. The OverlayView class contains everything related to the content of the view and all the constraints for Auto Layout, which makes the OverlayViewController class free from any stuff related to the view itself, except for one thing: the constraints for the tab bar controller, which I can only set in the OverlayViewController because the OverlayViewController instantiates the TabBarController, and so I don't have access to the TabBarController view in the constructor of the OverlayView class. This is basically what annoys me here, as the constraints should, strictly speaking, be a View thing, not a ViewController thing, yet I can only set those in the ViewController, unless I've missed something.
I have a UINavigationController with default value of it's navigationBar.prefersLargeTitles = true .
I am switching that to false when I push into a new scene lets call it (DetailsViewController), by changing it into the viewWillDisappear .
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.prefersLargeTitles = false
}
Now in DetailsViewController I am using willMove(to parent:) .
override func willMove(toParent parent: UIViewController?) {
navigationController?.navigationBar.prefersLargeTitles = true
}
To transition back to large titles .
Observe the attached snapshots of how iOS 13 doing it with how iOS 12 is doing it, considering iOS 12 is the correct behavior.
iOS 13 :
iOS 12 :
What you're doing was always wrong. You should set prefersLargeTitles to true once for the navigation bar and never touch it again.
The way to change what each view controller does about large titles as it appears is that that view controller sets its own navigationItem (in its viewDidLoad) to have the desired largeTitleDisplayMode. So if the first v.c. has .always and the second has .never everything will be smooth.
Swift 5, Xcode 13:
UIViewController(1) + UINavigationController:
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "Your title here"
}
UIViewController(2 - "i.e.: detailsViewController"):
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.largeTitleDisplayMode = .never
navigationItem.title = "Your title here"
}
It works like a charm!
I had the same issue and had to place a NavigationItem on the second ViewController's storyboard. My NavigationItem was being created automatically by the segue and its prefersLargeTitle in the viewDidLoad() was not finished creating before the view appeared. Adding a NavigationItem to the storyboard fixed this issue and allowed me to set the prefersLargeTitle in the storyboard's properties menu.
In my case this problem was occurring during a segue to a view controller which is a child of a UITabBarController. Setting largeTitleDisplayMode on the child view controller was not enough to fix this bug.
I have solved the issue by adding a navigation item to the UITabBarController scene and setting largeTitleDisplayMode as .never there.
I solved this problem like this:
override func viewWillDisappear(_ animated: Bool) {
title = ""
}
All ingenious is simple))
final class CustomHosting<Content: View>: UIHostingController<Content> {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.largeTitleDisplayMode = .never
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationItem.largeTitleDisplayMode = .automatic
}
}
I fixed same problem like this :) My problem is presenting SUI Detail Collection view from UIKit Collection, and had some jumping while navigation title changing.
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.
I'm having issues with iOS 11's large titles when using a Table View Controller.
I have set prefersLargeTitles to true in the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
}
When running my app the title appears as if prefersLargeTitles is set to small, but if I then scroll down, the large title appears.
I have 2 UIViewControllers, and they both display the large title correctly, bar the UITableViewController.
I have tried different combinations of setting prefersLargeTitles to true in the code and within the storyboard, where you can set Large Title to Automatic, Always or Never.
I can only find one other question on Stack Overflow which refers to this exact issue, but none of the answers seem to solve it.
Has anyone else who has run into this issue been able to solve it?
After setting prefersLargeTitles, you can trigger the large titles to show by calling setContentOffset on your UITableView.
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
self.tableView.setContentOffset(CGPoint(x: 0, y: -1), animated: true)
}
Perhaps you are reloading the tableView prior to viewDidLoad? I observed the same behavior today. In my case, I had a didSet on a variable that was set before viewDidLoad that called tableView.reloadData.
I was able to fix this by adding a guard so the reloadData only happened when the variable wasn't previously nil, meaning the first time.
Here's my calling class for reference...
/// Master Controller in UISplitViewController
class MyMasterViewController: UIViewController {
private let controller = MyTableViewController()
override func viewDidLoad() {
super.viewDidLoad()
controller.instanceVariable = data
showDetailViewController(navigationController, sender: self)
}
...
private func reloadDetail() {
controller.instanceVariable = newData
}
}
And here's the fix - added the guard statement below...
/// Detail Controller in UISplitViewController
class MyTableViewController: UITableViewController {
var instanceVariable: MyData! {
didSet {
guard oldValue != nil else { return }
tableView.reloadData()
}
}
// UITableViewController Methods...
}
Hope this helps! If my example doesn't make sense, I'd suggest commenting out all references to tableView.reloadData and then only re-adding when sure they aren't getting called until after the tableView loads initially.
You need to enable large titles on the navigation controller. It is tricky to find, so please see the screenshot below.
If your table view controller seques on to other detail view controllers then you should set large title to Never in the storyboard for those other view controllers.
DO NOT MESS with prefersLargeTitles in code like this:
self.navigationController?.navigationBar.prefersLargeTitles = false
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