Stop overlay UIWindow from controlling the status bar - ios

I'm creating a temporary UIWindow, on top of the main window, to show some small amount of data overlaid on the rest of my app. This information should not affect the status bar in any way. Note that the overlay window can be launched from a non-view controller context, meaning that the current view controller at the top of the stack of the main window might not be aware of the existence of the overlay window.
Unfortunately, as soon as I do overlayWindow.isHidden = false, it transfers control of the status bar (its style and whether it's hidden) to the root view controller of my overlay window, and I can't work out how to stop it.
Alternatively, I could just "remember" the previous state of the status bar and have my overlay root view controller output those, but there doesn't seem to be a good method to find out the current state and visibility of the status bar in iOS 13, at least which takes into account modally presented view controllers using the new sheet effect.
How can I have my overlay UIWindow reliably not affect the status bar?
Edit: I've prepared a small test case: https://github.com/Aquilosion/TestWindowView
The test case shows a view controller which changes its status bar appearance every second. You can open the same view controller again in a modal, and despite it also requesting status bar changes, iOS correctly locks the status bar white because the view controller never reaches it in sheet mode. Opening the window overlay currently always shows a black status bar, despite whether there's a modal presented or not. I attempt to set the status bar child to be the main window's root view controller's status bar child. Ideally, iOS would respect this and continue to change the status bar style while the overlay window was visible.

You can override a property childForStatusBarStyle of the root view controller of the overlay window.
class YourRootController { // root of the overlay window
var controllerToInheritStatusAttributesFrom: UIViewController?
override var childForStatusBarStyle: UIViewController? {
return controllerToInheritStatusAttributesFrom
}
}
// Call this from any place of the app, where you show the overlay window
if let controller = overlayWindow.rootViewController as? YourViewController {
controller.controllerToInheritStatusAttributesFrom = // needed controller, which lies underneath
controller.setNeedsStatusBarAppearanceUpdate()
}
I'm not sure it will help, considering that controllers will be in different windows, but it's worth a try.
I made your case work, just update the code:
private func updateStyle() {
styleIndex += 1
setNeedsStatusBarAppearanceUpdate()
overlayWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
}
Add this to MainViewController:
required init?(coder: NSCoder) {
super.init(coder: coder)
modalPresentationCapturesStatusBarAppearance = true
}
override var preferredStatusBarStyle: UIStatusBarStyle {
if self.presentingViewController != nil {
return .lightContent
}
return Self.styles[styleIndex % Self.styles.count]
}

Related

How to hide PTCardTabBar?

I use PTCardTabBarController like custom tab bar. I want to hide tab bar in my ViewController. I trying to use in storyboard Hide Bottom Bar on Push or in code: self.tabBarController?.tabBar.isHidden = true. But it not helped me.
When I stat to use PTCardTabBar I have next scheme in storyboard:
TabBarController (with class PTCardTabBarController and module PTCardTabBar) -> NavigationBarController -> ViewController.
Next I launch my app and I see under my PTCardTabBar system iOS tab bar. I use in storyboard Hide Bottom Bar on Push but it is hide only system tab bar and not PTCardTabBar. How to fix it and hide both tab bars?
Taking a quick look at that PTCardTabBar library...
What you see on-screen is not a UITabBar ... it is a UIView. So, any normal actions on a standard Tab Bar Controller's TabBar will not be related.
To hide it, you need to edit PTCardTabBarController.swift and make its customTabBar available to your code:
// make this "open"
open lazy var customTabBar: PTCardTabBar = {
return PTCardTabBar()
}()
Then, when you want to hide the tab bar (for example, in viewDidLoad() in your view controller:
if let ptcTBC = tabBarController as? PTCardTabBarController {
ptcTBC.customTabBar.isHidden = true
}
You'll also (obviously) need to set .isHidden back to false to show it again.
Probably, though, you want to do more than just have it showing or hidden... in which case you could further modify PTCardTabBarController.swift to add a function to animate it into or out-of view (for example).

Master View's Back Navigation in Split View Not Working with Display Modes "Auto" or "primaryHidden"

For my app, I have created a split view embedded in a container view, i.e. the split view is not at the root of the application.
As can be seen in the image, I added a navigation item to the master view that serves as a back button and pops the entire split view off the main navigation stack.
#IBAction func backButtonTapped(_ sender: UIBarButtonItem) {
parent?.navigationController?.popViewController(animated: true)
}
This is working well on all devices when I set the split view's display mode to allVisible. It works both for the collapsed view (e.g. on iPhone portrait mode) and the expanded split view.
Problem: When I use another display mode such as Auto or primaryHidden the navigation button in the master view still shows but does not work in landscape (expanded) view. The button handler is called but above statement does not navigate away from the split view. The parent's navigation controller does not seem to be accessible this way (= nil). Why doesn't the statement work in some display modes, and how can I make it work?
This feels a bit clumsy, but it does the job. A better solution may come from understanding why the parent navigation controller is accessible in some display mode but not in others.
What does work is going via the detail view's navigation controller and removing that from the stack. The display mode is changed here to prevent some ugly transition effect.
#IBAction func backButtonTapped(_ sender: UIBarButtonItem) {
if (splitViewController!.viewControllers.count > 1) {
// master and detail visible in split view
let detailViewController = splitViewController!.viewControllers[1]
splitViewController!.preferredDisplayMode = .primaryHidden
detailViewController.navigationController?.popViewController(animated: true)
} else {
// split view collapsed into single view mode (e.g. iPhone portrait)
parent?.navigationController?.popViewController(animated: true)
}
}

UISplitViewController: How to force showing the master ViewController

I am using a UISplitViewController, with the master and the detail viewcontrollers, without UINavigationControllers.
In some cases (for example when clicking on a universal link), I would like to force the app to always show the master viewcontroller.
How can I do that?
Is there a way to switch back from detail to master programmatically?
The split view controller is a beast, and the documentation is confusing. It is best understood by considering it as operating in two different modes: collapsed or not. Collapsed mode applies when the split view is presented in a horizontally compact view (i.e. iPhone), otherwise it is not collapsed (i.e. iPad).
The property preferredDisplayMode only applies if the view is NOT collapsed (i.e. iPad), and you can use this to select the master or detail view.
In collapsed mode, unless you are using navigation controllers, the original master view may be discarded:
After it has been collapsed, the split view controller reports having
only one child view controller in its viewControllers property. The
other view controller is collapsed into the other view controller’s
content with the help of the delegate object or discarded temporarily
But it is much better to use navigation controllers, as the split view controller is designed to work in conjunction with them:
The split view controller knows how to adjust the interface in more
intuitive ways. It even works with other container view controllers
(like navigation controllers) to present view controllers.
If you are using navigation controllers then the original master view may be at the bottom of the navigation stack:
In a horizontally compact environment, the split view controller acts
more like a navigation controller, displaying the primary view
controller initially and pushing or popping the secondary view
controller as needed
So you can do something like this:
if split.isCollapsed,
let nav = split.viewControllers[0] as? UINavigationController
{
nav.popToRootViewController(animated:false)
} else {
split.preferredDisplayMode = .allVisible
}
(It can get even more complicated if your master view pushes views in master as well as showing detail views. This code will pop to the root of the master view navigation stack)
You can set the preferredDisplayMode
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.allVisible
Or if you are looking for something like a toggle action:
extension UISplitViewController {
func toggleMasterView() {
let barButtonItem = self.displayModeButtonItem
UIApplication.shared.sendAction(barButtonItem.action!, to: barButtonItem.target, from: nil, for: nil)
}
}
Usage:
self.splitViewController?.toggleMasterView()
You can define a custom UISplitViewController and assign it to your split view in storyboard:
import UIKit
class GlobalSplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true
}
}
My solution is to swap the position of your primary and secondary ViewControllers if user is using an iPad. Then set preferredDisplayMode = .primaryHidden. Example code below.
splitViewVieController = UISplitViewController()
let isIphone = UIDevice.current.userInterfaceIdiom == .phone
splitViewVieController.viewControllers = isIphone ? [primaryNavController, seconaryNavController] : [seconaryNavController, primaryNavController]
splitViewVieController.preferredDisplayMode = .primaryHidden
We can change the position or width of the primary ViewController if needed.
splitViewVieController.maximumPrimaryColumnWidth = splitViewVieController.view.bounds.width
splitViewVieController.preferredPrimaryColumnWidthFraction = 0.5
splitViewVieController.primaryEdge = .trailing

Apple TV force focus another view

I'm working on Apple TV project. The project contains tab bar view controller, normally the tab bar will be appeared when swiping up on remote and hidden when swiping down. But now I reverse that behavior and I want to force focus another view when swiping up(normally focus on tab bar). Any way to do that? Thank you.
In your UIViewController, override shouldUpdateFocusInContext. If you detect an upward navigation into the tab bar, return false to prevent focus from reaching the tab bar. Then use a combination of preferredFocusEnvironments + setNeedsFocusUpdate to redirect focus somewhere else:
override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
if let nextView: UIView = context.nextFocusedView{
if ( context.focusHeading == .up && nextView.isDescendant(of: tabBar) ){
changeFocusTo(myView)
return false
}
}
}
internal var viewToFocus: UIView?
func changeFocusTo(_ view:UIView? ){
viewToFocus = view
setNeedsFocusUpdate()
}
override var preferredFocusEnvironments: [UIFocusEnvironment]{
return viewToFocus != nil ? [viewToFocus!] : super.preferredFocusEnvironments
}
This is a generally useful technique for customizing focus updates. An alternative technique is to use UIFocusGuide. You could insert a focus guide underneath the tab bar or surround the tab bar with a focus guide to redirect focus. Though focus guides are useful for simple cases, I have generally had better results using the technique I am describing instead.
I got the same issue with focus of UITabbarController before and I found the solution in Apple Support
Because UIViewController conforms to UIFocusEnvironment, custom view
controllers in your app can override UIFocusEnvironment delegate
methods to achieve custom focus behaviors. Custom view controllers
can:
Override the preferredFocusedView to specify where focus should start
by default. Override shouldUpdateFocusInContext: to define where focus
is allowed to move. Override
didUpdateFocusInContext:withAnimationCoordinator: to respond to focus
updates when they occur and update your app’s internal state. Your
view controllers can also request that the focus engine reset focus to
the current preferredFocusedView by callingsetNeedsFocusUpdate. Note
that calling setNeedsFocusUpdate only has an effect if the view
controller contains the currently focused view.
For more detail, please check this link
https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/WorkingwiththeAppleTVRemote.html#//apple_ref/doc/uid/TP40015241-CH5-SW14

HidesBottomBarWhenPushed leaves indicator

I have a settings dialog that I want to be full screen and cover the tab bar at the bottom of the screen. I used this SO answer and added HidesBottomBarWhenPushed to my view controller and it does hide the tab bar. Unfortunately it leaves behind the little triangle indicator subview that is displayed by the UITabBarController subclass.
I'm assuming there is some form of notification that I can subscribe to in order to hide the indicator but I don't know what that is. A little help here?
Maybe you could post a NSNotification when you set the bar to hidden using the method described here?
Can't access TabBarController from ImageView
I'm going to answer this myself because I think it's worth recording for future reference. I have a SettingsDialogViewController I wire up in the ViewDidLoad() method of my HomeDialogViewController:
NavigationItem.LeftBarButtonItem = new UIBarButtonItem("Settings", UIBarButtonItemStyle.Plain, (e, sender) => {
ActivateController (_settingsDvc());
});
The SettingsDialogViewController is created with HidesBottomBarWhenPushed = true. So when the settings dialog is activated, the bottom bar is hidden which causes the ViewWillLayoutSubviews() method of the CustomTabBarController to be called. By overriding that method I can set the visibility of my indicator based on whether the visible view controller (e.g. SettingsDialogViewController) hides the bottom bar when pushed. When that view controller is popped the indicator will automatically reappear.
public override void ViewWillLayoutSubviews () {
base.ViewWillLayoutSubviews ();
var selectedVc = SelectedViewController as UINavigationController;
indicator.Hidden = selectedVc != null && selectedVc.VisibleViewController.HidesBottomBarWhenPushed;
}
A final note, I found that the animation that occurred when activating the new settings view would display a black band across the screen below the status bar. I resolved this by setting the AutoResizingMask in the "from" view controller.
public override void ViewDidLoad () {
base.ViewDidLoad ();
NavigationItem.LeftBarButtonItem = new UIBarButtonItem("Settings", UIBarButtonItemStyle.Plain, (e, sender) => {
ActivateController (_settingsDvc());
});
View.AutoresizingMask = UIViewAutoresizing.FlexibleHeight;
}

Resources