UIKit TabBar with SwiftUI View - swift

I am updating my existing UIKit to use SwiftUI in some parts where I feel comfortable about being able to replace all of it with SwiftUI. It is a tab application, so there are 3 tabs and one of it are Settings, which I am replacing with SwiftUI.
My tab bar is configured to only show images and no titles (the issue I need help with would also occur with labels shown).
Now in UIKit I do this:
private let settingsViewController: UIViewController = UIHostingController<SettingsView>(SettingsView())
and later on before showing it:
settingsViewController.navigationItem.title = "Settings".
However from then on SwiftUI takes on with its view modifier: .navigationBarTitle("Text"), which I use for nested view controllers (pushed via NavigationLink), as UIKit cannot access those views to set a title.
The issue is however, that whenever .navigationBarTitle is used (same with setting the view's title property in UIKit, it sets the current tab bar items title. For UIKit there is always a way to set the navigationItem.title instead of title.
Is there any way to set the navigation bar title but not the tab bar title itself in SwiftUI?
Thanks

This is to be a bug in iOS. Please file a bug report to Apple.
I just discovered a workaround for this issue:
Create a custom subclass of UINavigationController and use it as the navigation controller containing your settingsViewController.
class WorkaroundUINavigationController: UINavigationController {
override var title: String? {
get { tabBarItem.title }
set { navigationItem.title = newValue }
}
}

Related

How to disable lazy loading in NSTabViewController?

I am designing a SwiftUI wrapper for NSTabViewController with the toolbar style. I want it to be a drop-in replacement for TabView. TabView uses a modifier tabItem(_:) to specify the tab name and icon. So I designed a similar modifier for my own ToolbarTabView:
extension View {
func toolbarTabItem(_ label: LocalizedStringKey, nsImage: NSImage? = nil, tooltip: LocalizedStringKey? = nil) -> some View {
self.preference(key: ToolbarTabItemPreferenceKey.self, value: ToolbarTabItemPreference(label: label, nsImage: nsImage, tooltip: tooltip))
}
}
I wrap each View in a NSHostingController and create a NSTabViewItem. Then I use onPreferenceChange to set the NSTabViewItem's label and image property. Finally, I have a NSViewControllerRepresentable to pass my array of NSTabViewItem to a NSTabViewController. This all works well except for the following issue.
By design NSTabViewController will only load its first tab. This loads the first NSHostingController which lays out the first View. That calls onPreferenceChange and sets the label for the first tab. However, the remaining tabs are not loaded and therefore the label remains unset.
I know that I can re-design my APIs to pass in the labels and images explicitly and that works, but then how does Apple implement their TabView? They must have the same issue with the views being lazy loaded because the macOS implementation of TabView looks like NSTabViewController.
I think a workaround would be to force all the tabs to load, which is the title of this question, but I am open to other ideas as well.
Reference:
https://github.com/utmapp/UTM/blob/dev/Platform/macOS/ToolbarTabView.swift
https://github.com/utmapp/UTM/blob/dev/Platform/macOS/ToolbarTabViewController.swift
Here is the dumb workaround I came up with
public class UTMTabViewController: NSTabViewController {
public override func viewDidAppear() {
super.viewDidAppear()
for i in self.tabViewItems.indices {
self.selectedTabViewItemIndex = i
}
self.selectedTabViewItemIndex = 0
}
}
Basically I force load every tab once the view appears. I really hope there's a better answer than this but I'll leave it here just in case.

StatusBar text color doesn't change to Light Content [duplicate]

Adding
application.statusBarStyle = .lightContent
to my AppDelegate's didFinishLaunchingWithOptions method nor adding
override var preferredStatusBarStyle: UIStatusBarStyle {
return UIStatusBarStyle.lightContent
}
to the VC no longer works on iOS12/Xcode10
Any ideas?
This has nothing to do with iOS 12. You just have the rules wrong.
In a navigation controller situation, the color of the status bar is not determined by the view controller’s preferredStatusBarStyle.
It is determined, amazingly, by the navigation bar’s barStyle. To get light status bar text, say (in your view controller):
self.navigationController?.navigationBar.barStyle = .black
Hard to believe, but true. I got this info directly from Apple, years ago.
You can also perform this setting in the storyboard.
Example! Navigation bar's bar style is .default:
Navigation bar's bar style is .black:
NOTE for iOS 13 This still works in iOS 13 as long as you don't use large titles or UIBarAppearance. But basically you are supposed to stop doing this and let the status bar color be automatic with respect to the user's choice of light or dark mode.
If you choose a same status bar color for each View Controller:
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
Ad this to your Info.plist and set status bar color from Project -> Targets -> Status Bar Style by desired color.
On the other hand, in your case, you have a navigation controller which is embedded in a view controller. Therefore, you want to different status bar color for each page.
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
Ad this to your Info.plist. Then, create a custom class for your NavigationController. After that you can implement the method:
class LightContentNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
Thats it! Please, inform me whether this was useful!
If Matt's answer isn't working for you, try adding this line of code before you present your viewController.
viewController.modalPresentationCapturesStatusBarAppearance = true
I encountered a bug where setting modalPresentationStyle to overFullScreen does not give the status bar control to the presented view controller or navigation controller.
I was using navigation controller for each tab of UITabBarController. Subclassing UINavigationController and overriding childForStatusBarStyle fixed the issue for me.
class MyNavigationController: UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return topViewController?.childForStatusBarStyle ?? topViewController
}
}
If you have a modal UIViewController the situation becomes very tricky.
Short answer:
Present modal using UIModalPresentationStyle.fullScreen
override preferredStatusBarStyle (in your modal vc)
call setNeedsStatusBarAppearanceUpdate() in viewWillAppear (in your modal vc)
If you don't want to use UIModalPresentationStyle.fullScreen you have to set modalPresentationCapturesStatusBarAppearance
According to apple doc:
When you present a view controller by calling the
present(_:animated:completion:) method, status bar appearance
control is transferred from the presenting to the presented view
controller only if the presented controller's modalPresentationStyle
value is UIModalPresentationStyle.fullScreen. By setting this property
to true, you specify the presented view controller controls status bar
appearance, even though presented non-fullscreen.
The system ignores this property’s value for a view controller
presented fullscreen.
You can set
vc.modalPresentationCapturesStatusBarAppearance = true
to make the customization works.
Customizing UINavigationController can fix the issue
class ChangeableStatusBarNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return topViewController?.preferredStatusBarStyle ?? .default
}
}
Ref: https://serialcoder.dev/text-tutorials/ios-tutorials/change-status-bar-style-in-navigation-controller-based-apps/

How to get the navigation bar to show large title upon a segue back?

I have a tab bar app where one of the views is a UITableViewController containing static cells as content with 1 section and 1 row.
I want the Large Title to be set to "Always," so I made the selection on the storyboard and the title was large on the simulator. Now when the user taps "Start Chat," the app will segue to the Virtual Assistant View Controller, where the Large Title is set to "Never" on the storyboard. Now the problem is that when the user segues back to the previous view controller with the "Start Chat" table view cell, the title is not large anymore.
It is interesting that when I set the table view to be scrollable, the title becomes large again upon dragging down the table view. I made sure the navigation bar on the Navigation Controller storyboard is checked with the "Prefers Large Titles." I am using Xcode 11, and this was not a problem when using Xcode 10.
I tried creating a custom class for the view with the start chat button and this code did not work in making the title large from a segue back:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationItem.largeTitleDisplayMode = .always
navigationController?.navigationBar.prefersLargeTitles = true
What else could I do? Any help will be greatly appreciated!
I'd use willMove(toParent:) to change the title back before the segue is performed.
override func willMove(toParent parent: UIViewController?) {
navigationController?.navigationItem.largeTitleDisplayMode = .always
navigationController?.navigationBar.prefersLargeTitles = true
}
Set the properties when setting up the UINavigationController, before presenting it. If you already presented the navigation controller, try doing this to force-update the navigation bar:
navigationController?.navigationItem.prompt = ""
navigationController?.navigationItem.prompt = nil
I took this workaround from this question.
In your particular case, it would be better to subclass the navigation controller and set those properties in its viewDidLoad method, so its properties (largeTitleDisplayMode and prefersLargeTitles) are set in a self-contained code.

How to maintain the navigation with a segue "present modally"?

I have : navigation controller -> tableViewController -> tab bar Controller -> ViewController1 / ViewController2 / ViewController3
I click on a cell on the TableViewController and I open the TabBar. All is OK
But, I wanted to have more details from the datas in the TableViewController so I decided to make a popup with the content of the cell. I found this tutorial https://www.youtube.com/watch?v=S5i8n_bqblE => GREAT ! It's about the use of segue "present modally" with a viewcontroller containing the popup. I made a link from the popup to the tabBarController and I lose the Navigation Bar
I tried to play with navigationBar but nothing is working. I changed the type of segue but I don't obtain what I want.
I think the problem come from the type of segue. It's OK if I use it like a go/back in viewController. Do you have any solution about using this sort of popup or do I have to use another way ?
Thanks
Ok, let's take a look.
Navigation Bar is a view which is provided by Navigation Controller. Sometimes we are confused with navigation bars and navigation items. Navigation bar is the only one and it belongs to navigation controller, navigation item belongs to individual view controller from navigation stack. So, first step is simple: if you want navigation bar, wrap your modally presented controller into navigation stack.
Yes, you will face other problem, the blurred view of previous controller will become a black area. Why? there is special object called Presentation Controller (UIPresentationController) which is responsible for how controller will be presented. And it hides view of previous controller by default (in sake of performance, I think).
Ok, let's move. We can create custom presentation controller and tell it not to hide view of previous controller. Like this:
class CustomPresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool {
return false
}
}
Next step. In controller we want present modally we have to specify to things: we want to use custom presentation controller and we also want to adjust delegate object for transitioning (where we can specify custom presentation controller). The trick is that you have to do it inside initialiser, because viewDidLoad is too late: controller had been already initialised:
class PopupViewController: UIViewController {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .custom
transitioningDelegate = self
}
}
Final step. When PopupViewController became delegate for its own transitioning, it means this controller is responsible for all of them. In our particular case popup controller provides custom version of presentation controller. Like this:
extension PopupViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
}
That's all. Now you should see view of previous controller.

Setting SplitViewItem's ViewController

Im having trouble setting a nssplitviewcontroller's split view's view controller. I have a reference from the story board and am trying to set the items view controller programmatically:
override func viewDidLoad() {
dash = storyBoard.instantiateControllerWithIdentifier("dash_viewcontroller") as? NSViewController
print(dash)
main_view.viewController = dash!
}
I get this error from the console(doesn't crash) and doesn't show the programmatically set vc:
2016-02-21 10:03:19.475 HealthDash[62950:3960447] Failed to set (contentViewController) user defined inspected property on (NSWindow): Cannot remove a SplitViewItem's viewController if it is currently in a SplitViewController
Looks like the splitViewItem has a content controller that is actively being displayed. My guess: first you will have to remove that view controller from screen before you can replace it. Probably easier to create a new NSSplitItemView, add that to the NSSplitViewController and remove unwanted NSSplitItemView (and their associated view controllers).