ContentInset not updated when view is added as subviews - swift

I'm making a photo browsing app, which the user can view the details and comments from the web of the photo being viewed. To do this I have a UIViewController (parentVC) with a segmented control that act as the switch between the details and comments, and the two UITableViewControllers that serve the details and comments. Storyboard of the view controllers in question.
To switch between views, the tableVCs are instantiated from the storyboard and added to the parentVCs as childViewController, and their views are added as subviews.
Since the segmentedControl are placed in the navigationController's navBar, I'd want the tableViews to play nice with the parentVC's navigationbar as well. Which when the first tableview (detailVC) is instantiated, its contentInset is automatically adjusted.
However, when the second tableview (commentsVC) is instantiated following a switch from the segmented control, it's contentInset is not set (in fact it's 0 for all edges), and the tableview is hidden behind the navBar. But when the device is rotated, the tableview adjust its insets and it works fine again.
Right now I suspect the problem has something to do with how automaticallyadjustsscrollviewinsets is done by iOS, since switching which childVC is present first would always set the first VC being presented's contentInset. I don't know if there are any methods I could trigger to set the second VC's contentInset when it's added.
So the question is, how do I have both the childVCs' contentInset set to respect the navigationBar in parentVC? Creating a UIEdgeInset is a workaround but it won't update itself when device orientation is changed. A more minor question would be how should I work with the layouts and insets of navigation controllers when the childVCs are instantiated programmatically so that they work as if they are linked in the storyboard itself.
The following is my code when instantiating the childVCs from the parentVC.
if index == 0 { //details
if detailsVC == nil {
detailsVC = storyboard?.instantiateViewControllerWithIdentifier("InfoPaneDetailsVC") as! InfoPaneDetailsVC
}
if commentsVC != nil {
commentsVC.view.removeFromSuperview()
commentsVC.removeFromParentViewController()
}
addChildViewController(detailsVC)
detailsVC.didMoveToParentViewController(self)
view.addSubview(detailsVC.view)
view.layoutSubviews()
} else if index == 1 { //comments
if commentsVC == nil {
commentsVC = storyboard?.instantiateViewControllerWithIdentifier("InfoPaneCommentsVC") as! InfoPaneCommentsVC
}
if detailsVC != nil {
detailsVC.view.removeFromSuperview()
detailsVC.removeFromParentViewController()
}
addChildViewController(commentsVC)
commentsVC.didMoveToParentViewController(self)
view.addSubview(commentsVC.view)
view.layoutSubviews()
}

the tableview is hidden behind the navBar. But when the device is rotated, the tableview adjust its insets and it
works fine again.
I can duplicate what you are seeing. I don't know why rotating the device gets rid of the underlap*. I can even start the app in landscape orientation, and the TableView will underlap the NavigationBar, then when I rotate to portrait the underlap disappears.
A couple of solutions work for me:
edgesForExtendedLayout = .None
navigationController?.navigationBar.translucent = false
If a ViewController's parent is a NavigationController (or a TabBarController), then two properties determine how the underlap works:
The ViewController's edgesForExtendedLayout property.
The NavigationBar's translucent property.
The edgesForExtendedLayout property has a default value of .All, which means that the ViewController's view will underlap all translucent bars.
You can check whether your NavigationBar is translucent like this:
import UIKit
class InfoViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if navigationController?.navigationBar.translucent == true {
print("NavigationBar is translucent.")
}
if edgesForExtendedLayout == .All {
print("This ViewController's view will underlap a translucent NavigationBar.")
}
I get:
NavigationBar is translucent.
This ViewController's view will underlap a translucent NavigationBar.
The UINavigationBar docs say**,
Additionally, navigation bars are translucent by default. Turning the
translucency off or on does not affect buttons, since they do not have
backgrounds.
You can prevent the underlap like this:
import UIKit
class InfoViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = .None
}
.None tells the ViewController's view not to underlap any translucent bars. Or, you could use .Bottom, which means the view will only underlap a translucent bottom bar, i.e. not a translucent top bar. (To see the various values--or combinations of values--that you can assign to edgesForExtendedLayout check out the docs).
Or, you can prevent the underlap like this:
import UIKit
class InfoViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.translucent = false
}
The value of edgesForExtendedLayout only applies to translucent bars--a view will not underlap an opaque bar. If you don't want to be bothered with having to set translucent to false in your code, you can select the Navigation Bar in the storyboard, and in the Attributes Inspector uncheck Translucent.
* After rotating the device, I printed out edgesForExtendedLayout and the NavBar's translucent property and they didn't change:
class InfoViewController: UIViewController {
override func viewWillTransitionToSize(size: CGSize,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
//executes before transition
coordinator.animateAlongsideTransition(//unlabeled first argument:
{ (UIViewControllerTransitionCoordinatorContext) -> Void in
//executes during transition
},
completion: { (UIViewControllerTransitionCoordinatorContext) -> Void in
//executes after transition
print("After rotating:")
if self.navigationController?.navigationBar.translucent == true {
print("NavigationBar is translucent.")
}
if self.edgesForExtendedLayout == .All {
print("This ViewController's view will underlap a translucent NavigationBar.")
}
}
)
}
--output:--
NavigationBar is translucent.
This ViewController's view will underlap a translucent NavigationBar.
After rotating:
NavigationBar is translucent.
This ViewController's view will underlap a translucent NavigationBar.
** If a NavigationBar has a background image, the value of the NavigationBar's translucent property will be set to true or false depending on the alpha of the image. See the Translucency section in the UINavigationBar docs.

Related

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/

Reapply constraints after hiding UITabBar

I have a UIButton that is constrained to the view's safeAreaLayoutGuide bottom anchor and a UITabBar in that UIView. Everything is okay there. However, when I fullscreen an image, I hide the UITabBar. When I dismiss the fullscreen, I show the UITabBar again. However, the UIButton moves down and doesn't constrain to the UITabBar as it did before the UITabBar was hidden. The UIButton is covered partially by the UITabBar. Any solutions?
Here is the dismiss fullscreen code.
#objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
sender.view?.removeFromSuperview()
self.navigationController?.isNavigationBarHidden = false
self.tabBarController?.tabBar.isHidden = false
}
Here are two things you can try:
Solution 1:
Add this line to dismissFullscreenImage():
self.view.setNeedsLayout()
This invalidates the layout of self.view and causes it to be laid out again.
Solution 2:
You can avoid the need to relayout the view by making the navigationBar and tabBar invisible.
Instead of hiding/showing the navigationBar and tabBar by changing their isHidden properties, try setting their alpha values:
// hide
self.navigationController?.navigationBar.alpha = 0
self.tabBarController?.tabBar.alpha = 0
// show
self.navigationController?.navigationBar.alpha = 1
self.tabBarController?.tabBar.alpha = 1

Swift - Adding a navigation bar that overrides the existing one

I have multiple VC's embedded in one NavigationController.
I have one VC, lets name it VCNotTransparent, that I want the bar to be not transparent, and on other VC's I want it to be transparent.
So in the main VC, I added these lines for making the bar transparent:
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()
navigationController?.navigationBar.isTranslucent = true
So now all of my bars are transparent in the app.
How can I make VCNotTransparent not transparent without it changing all of the other VC's? one solution I thought of is to add a new navigation bar only in VCNotTransparent, but I do not know how to do that.
EDIT
I also tried embedding VCNotTransparent in its own NavigationController, which works almost, but the issue is that I have navigation from it to some other VC's and they become not transparent as well, since they are sub navigation of the VCNotTransparent.
Handle this by enum -
Do below within your MainVC -
public enum NavigationType: Int {
case transparent = 1
case notTransparent = 2
}
var currentNavigationType: NavigationType?
override func viewDidLoad() {
super.viewDidLoad()
self.currentNavigationType = .transparent // default
self.setupNavigationControllerStyle()
}
func setupNavigationControllerStyle (){
switch self.currentNavigationType! {
case .transparent:
//do code here for transparent
case .notTransparent:
//do code here for not transparent
default:
break
}
}
default it will show transparent bar. in which controller you don't want transparent bar just update the currentNavigationType property from there like below -
class VCNotTransparent: MainVC {
override func viewWillAppear(_ animated: Bool) {
self.currentNavigationType = .notTransparent
super.viewWillAppear(animated)
}
}

UISearchController SearchBar misaligns while active during rotation

I have been struggling with UISearchController's search bar for quite a while now. I need a search feature to be implemented on a tableview but unlike conventional methods, I didn't put the search bar on a table header view. Instead, I created a UIView and added the search bar as a subview to it. The UIView which acts as a container to search bar has its constraints properly set on the storyboard with auto layout.
Here are my codes for it. Note that I did this programmatically because UISearchDisplayController and UISearchBar has been deprecated as of iOS 8 in favour of UISearchController and has yet to come to UIKit.
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.autoresizingMask = .FlexibleRightMargin
searchController.searchBar.delegate = self
definesPresentationContext = true
self.searchContainerView.addSubview(searchController.searchBar)
However, I did notice one odd behaviour of the search bar during rotation. When it is active on Portrait, I rotate the simulator to Landscape, and press Cancel, the search bar goes back to Portrait Width.
The same happens the other way around too.
I would appreciate any ideas or maybe some hints towards the correct direction to solve this as I have been at this for days at least. Thank you very much
So after so many days of struggling with this:
Portrait to Landscape
Make SearchBar/SearchController active in Portrait
Rotate to Landscape
Press Cancel and SearchBar will go back to Portrait width
Landscape to Potrait
Make SearchBar/SearchController active in Landscape
Rotate to Portrait
Press Cancel and SearchBar will go back to Landscape width
I finally solved it on my own. It seems that when a user presses Cancel on the SearchBar, the ViewController will call viewDidLayoutSubviews thus I tried to reset the width by calling this function in viewDidLayoutSubviews:
func setupSearchBarSize(){
self.searchController.searchBar.frame.size.width = self.view.frame.size.width
}
But that didn't work as well as I thought. So here is what I think happens. When a user activates the SearchController/SearchBar, the SearchController saves the current frame before it resizes itself. Then when a user presses Cancel or dismisses it, it will use the saved frame and resize to that frame size.
So in order to force its width to be realigned when I press Cancel, I have to implement the UISearchControllerDelegate in my VC and detect when the SearchController is dismissed, and call setupSearchBarSize() again.
Here are the relevant codes to solve this question:
class HomeMainViewController : UIViewController, UISearchControllerDelegate, UISearchResultsUpdating, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
#IBOutlet weak var searchContainerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.autoresizingMask = .FlexibleRightMargin
searchController.searchBar.delegate = self
searchController.delegate = self
definesPresentationContext = true
self.searchContainerView.addSubview(searchController.searchBar)
setupSearchBarSize()
}
func setupSearchBarSize(){
self.searchController.searchBar.frame.size.width = self.view.frame.size.width
}
func didDismissSearchController(searchController: UISearchController) {
setupSearchBarSize()
}
override func viewDidLayoutSubviews() {
setupSearchBarSize()
}
}
Here is simpler solution:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
self.searchController.searchBar.frame.size.width = self.view.frame.size.width
}, completion: nil)
}

how hide tap bar on another controller before show everything else

I have a simple tap bar example. And for my next view i have a ViewController with tableView and on bottom textInput. when i want hide tap bar i have a code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject? {
if segue.identifier == "showMe" {
(segue.destinationViewController as! MyViewController)
destinationController.hidesBottomBarWhenPushed = true
}
}
and on my next view when i tap a row on tableView i see first rendering tap bar and then tap bar is hidden and on last input Edit goes down :( how hide this tap bar before show next screen ?
This isn't exactly the best solution, but its a workaround:
set destinationController.hidesBottomBarWhenPushed = false
set contraints properly in your view controller (as if there is no tab bar)
use the following code (as shown) in the view controller where you want to hide the tab bar:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.tabBarController?.tabBar.frame = CGRectZero
self.tabBarController?.tabBar.userInteractionEnabled = false
}
This will make sure that the tab bar is hidden. Now the Autolayout constraints will make sure your view displays correctly with the tab bar height as zero.