iOS7 UIScrollView show offset content below status bar - iphone

I'm developing my app to work with iOS7.
I have a UINavigationController I'm pushing a UIViewController that has a ScrollView inside it. Inside the scrollView I have a tableView.
Is it possible to achieve that when I scroll the tableView inside the scrollView the list will appear behind that Status bar. Same why it would be if I had a UINavigationController and a UIViewController with a tableView in it.
So this it the hierarchy :
UINavigationController -> UIViewController -> UIScrollView -> UITableView .
and I want that when a user scroll the table,the cells in the top will be visible under the status bar.
If there is no UIScrollView it happens automatically in iOS7.
Thanks.

Just set automaticallyAdjustsScrollViewInsets to NO in the viewController init method.
In Storyboard, you can switch the property directly in the property panel when the UIViewController is selected.
If you use xib, just set it like this:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self.automaticallyAdjustsScrollViewInsets = NO;
}
Note: this is right since iOS7 and still in iOS8.

none of the above workd for me, until I noticed that I had to set Content Insets from Automatically to Never in the interfacebuilder:

Starting with iOS 11 you can use this new property with a fallback (Swift 4):
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
self.automaticallyAdjustsScrollViewInsets = false
}

The answer from Skoua might work in some situations, but does have certain side-effects on iOS11 and later. Most notably, the scroll view will start propagating safe areas to its children, which can mess up your layout while scrolling if you use the safe areas or layout margins.
Apple explains this very well and in detail in this WWDC session and also mentions explicitly that contentInsetAdjustmentBehavior = .never can have side-effects and is not what you should use in most cases.
To get a scroll view that does not mess up our layout, but shows its content below the status bar (or navigation bar), you should observe the safe area of your scroll view and adjust your custom content insets accordingly:
private var scrollViewSafeAreaObserver: NSKeyValueObservation!
override func viewDidLoad() {
...
if #available(iOS 11.0, *) {
self.scrollViewSafeAreaObserver = self.scrollView.observe(\.safeAreaInsets) { [weak self] (_, _) in
self?.scrollViewSafeAreaInsetsDidChange()
}
} else {
self.automaticallyAdjustsScrollViewInsets = false
}
}
#available(iOS 11.0, *)
func scrollViewSafeAreaInsetsDidChange() {
self.scrollView.contentInset.top = -self.scrollView.safeAreaInsets.top
}
deinit {
self.scrollViewSafeAreaObserver?.invalidate()
self.scrollViewSafeAreaObserver = nil
}
Why does this work? Because we leave contentInsetAdjustmentBehavior = .automatic. This will give us normal behaviour when it comes to scrolling and non-scrolling axis, but the UIScrollView will also "convert" any safe areas to content insets. Since we don't want this for our top edge, we simply set the negative top safe area as our custom insets, which will counter any insets set by the scroll view.

Thats just dumb from Apple. One more weird behaviour to worry about. Anyhow, I ended up setting the scroll view content inset for top to -20 (from IB).

I found the solution! Just set:
self.automaticallyAdjustsScrollViewInsets = false
on the view controller that has the UIScrollView.

You probably has seen this recommendation a thousand times but, check the 'The Status Bar' section on the iOS 7 transition guide(can't post the direct link here).
Blunt resuming, on ios7 the status bar is part of the view. This means that almost anything you put on your view, will be under the bar, unless you say the contrary. A work around i found for that problem was making the status bar opaque.
Another way could be disabling the status bar on that specific view. Like on this thread.

I have had a similar sort of problem, and I found that to ensure my scrollview content doesn't go under the status bar, I applied a setContentInset.
So in your situation, if you are using an inset, I would use suggest using UIScrollViewDelegate or scrollViewDidScroll tailored to your tableview. If a scroll is occurring, disregard the scrollview inset.

don't hide the statusBar, the scrollView won't jump

Related

How to make a nav bar transparent before scrolling (iOS 14)

I'd like to implement a nav style like what is found in the "Add Reminder" view controller from Apple's Reminders (iOS 14). I've tried hooking into the scrollview delegate methods but I'm not sure how to change the alpha of the default nav bar background/shadow image.
I've tried changing the nav bar style on scroll and, while that works, it doesn't fade in/out like in the example. That makes me think the answer lies manipulating the alpha value. Thanks in advance!
I've found a (hacky) solution that works in iOS 14 (untested in other versions). It makes an assumption about the private view structure of UINavigationBar, so there's no guarantee that it will work in future iOS versions, but it's unlikely to crash - the worst that should happen is that the bar will fail to hide, or only partially hide.
Assuming that you are placing the code inside a UIViewController subclass that it acting as the delegate for a UITableView, UICollectionView or UIScrollView, the following should work:
override func viewDidLoad() {
super.viewDidLoad()
// this hides the bar initially
self.navigationController?.navigationBar.subviews.first?.alpha = 0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let navigationController = self.navigationController else { return }
let navBarHeight = navigationController.navigationBar.frame.height
let threshold: CGFloat = 20 // distance from bar where fade-in begins
let alpha = (scrollView.contentOffset.y + navBarHeight + threshold) / threshold
navigationController.navigationBar.subviews.first?.alpha = alpha
}
The magic threshold value is a little hard to explain, but it's basically the distance from the bar at which the fade in will start. A value of 20 means the bar starts to fade in when the scrollView content is 20 points away. A value of 0 would mean the bar snaps straight from fully transparent to fully opaque the moment the scrollView content touches it.

Swift - Programmatically refresh constraints

My VC starts with stackView attached with Align Bottom to Safe Area .
I have tabBar, but in the beginning is hidden tabBar.isHidden = true.
Later when the tabBar appears, it hides the stackView
So I need function that refresh constraints after tabBar.isHidden = false
When I start the app with tabBar.isHidden = false the stackView is shown properly.
Tried with every function like: stackView.needsUpdateConstraints() , updateConstraints() , setNeedsUpdateConstraints() without success.
Now I'm changing the bottom programatically, but when I switch the tabBarIndex and return to that one with changed bottom constraints it detects the tabBar and lifts the stackView under another view (which is not attached with constraints). Like is refreshing again the constraints. I'm hiding and showing this stackView with constrains on/off screen.
I need to refresh constraints after tabBar.isHidden = false, but the constraints don't detect the appearance of the tabBar.
As I mention switching between tabBars fixes the issue, so some code executes to detecting tabBar after the switch. Is anyone know this code? I tried with calling the methods viewDidLayoutSubviews and viewWillLayoutSubviews without success... Any suggestions?
This amateur approach fixed my bug... :D
tabBarController!.selectedIndex = 1
tabBarController!.selectedIndex = 0
Or with an extension
extension UITabBarController {
// Basically just toggles the tabs to fix layout issues
func forceConstraintRefresh() {
// Get the indices we need
let prevIndex = selectedIndex
var newIndex = 0
// Find an unused index
let items = viewControllers ?? []
find: for i in 0..<items.count {
if (i != prevIndex) {
newIndex = i
break find
}
}
// Toggle the tabs
selectedIndex = newIndex
selectedIndex = prevIndex
}
}
Usage (called when switching dark / light mode):
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
tabBarController?.forceConstraintRefresh()
}
If you want to update view's layout, you can try layoutIfNeeded() function.
after updating stackView constraints call this method:
stackView.superview?.layoutIfNeeded()
Apple's Human Interface Guidelines indicate that one should not mess around with the Tab Bar, which is why (I'm guessing) setting tabBar.isHidden doesn't properly update the rest of the view hierarchy.
Quick searching comes up with various UITabBarController extensions for showing / hiding the tab bar... but they all appear to push the tabBar down off-screen, rather than setting its .isHidden property. May or may not be suitable for your use.
I'm assuming from your comments that your VC in tab index 0 has a button (or some other action) to show / hide the tabBar?
If so, here is an approach that may do the job....
Add this enum in your project:
enum TabBarState {
case toggle, show, hide
}
and put this func in that view controller:
func showOrHideTabBar(state: TabBarState? = .toggle) {
if let tbc = self.tabBarController {
let b: Bool = (state == .toggle) ? !tbc.tabBar.isHidden : state == .hide
guard b != tbc.tabBar.isHidden else {
return
}
tbc.tabBar.isHidden = b
view.frame.size.height -= 0.1
view.setNeedsLayout()
view.frame.size.height += 0.1
}
}
You can call it with:
// default: toggles isHidden
showOrHideTabBar()
// toggles isHidden
showOrHideTabBar(state: .toggle)
// SHOW tabBar (if it's hidden)
showOrHideTabBar(state: .show)
// HIDE tabBar (if it's showing)
showOrHideTabBar(state: .hide)
I would expect that simply pairing .setNeedsLayout() with .layoutIfNeeded() after setting the tabBar's .isHidden property should do the job, but apparently not.
The quick frame height change (combined with .setNeedsLayout()) does trigger auto-layout, though, and the height change is not visible.
NOTE: This is the result of very brief testing, on one device and one iOS version. I expect it will work across devices and versions, but I have not done complete testing.

Update to Xcode 11.3.1 - navigationBar and half of the Views disappear after storyboard refactoring

Using Xcode 11.3.1, Simulator11.3.1, iPhoneX, Swift5.1.3, iOS13.3,
I am wondering why half of my app suddenly disappears !!
Could it be the update to Xcode 11.3.1 ???
The following shows a screenshot of the Xcode Debug View Hierarchy.
The left side is what the iPhone 11 Pro Simulator shows and the right side is the Debug View Hierarchy:
Clearly there are many more objects in the view hierarchy (such as the round buttons at the bottom) that are not shown on the Simulator (and also not on a physical iPhoneX). Also the NavigationBar is missing completely !!!!
The blue highlighted object is a custom navigationBar (consisting of a stackView). This worked before but not since the Xcode update. I am really not believing this. What could go wrong here ??
If it is not the Xcode-update, then my refactoring of the storyboard could also be a cause of this view-losses.
Before my refactoring, the VC at question was a ChildViewController of another ViewController. Now, it is the entry point of the App. Could this change bring the view-losses ? I want to see a NavigationController with largeTitle. But there is no NavigationController whatsoever now!
Here is the code that sets up the navigationBar:
override func viewDidLoad() {
// set up navigationItem and navigationController look and feeel
navigationItem.largeTitleDisplayMode = .always
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
navigationController?.set_iOS12_lookAndFeel()
navigationItem.title = "bluub"
}
And the needed NavigationController extension:
import UIKit
extension UINavigationController {
func set_iOS12_lookAndFeel() {
if #available(iOS 13.0, *) {
self.keep_iOS12_lookAndFeel()
} else {
let attrLargeTitle = AppConstants.FontAttributes.NavBar_LargeTitleTextAttributes
self.navigationBar.largeTitleTextAttributes = attrLargeTitle
let attrTitle = AppConstants.FontAttributes.NavBar_TitleTextAttributes
self.navigationBar.titleTextAttributes = attrTitle
}
}
private func keep_iOS12_lookAndFeel() {
if #available(iOS 13.0, *) {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithDefaultBackground()
navBarAppearance.backgroundEffect = .init(style: .systemThickMaterialDark)
navBarAppearance.titleTextAttributes = AppConstants.FontAttributes.NavBar_TitleTextAttributes
navBarAppearance.largeTitleTextAttributes = AppConstants.FontAttributes.NavBar_LargeTitleTextAttributes
navBarAppearance.buttonAppearance.normal.titleTextAttributes = AppConstants.FontAttributes.NavBar_ButtonAppearance_Normal
navBarAppearance.doneButtonAppearance.normal.titleTextAttributes = AppConstants.FontAttributes.NavBar_Done_ButtonAppearance_Normal
self.navigationBar.standardAppearance = navBarAppearance
self.navigationBar.scrollEdgeAppearance = navBarAppearance
}
}
}
.
---------------- more findings -----------------------------
After another storyboard refactoring, I could bring back the round menu buttons. However, the largeTitle-NavigationBar is still completely missing.
Frankly, the latest refactoring did not introduce any new constraints or other storyboard settings as before. The fact that I kicked out the NavigationController and replaced it by an identical new one, plus, re-assigned one or the other constraint of the menu-button-View, did bring the bottom menu back alive. As far as I can tell, no difference to the previous storyboard was introduced.
It is very annoying why a storyboard needs to be redrawn basically to render correctly. Something seems corrupt here as for the Xcode functionality with storyboard !
But lets leave this talk.
My remaining question:
How can I bring back a missing NavigationBar ?????????
.
---------------- another finding -----------------------------
If I reassign the "first-entry-ViewController" to the old ViewController that eventually adds the Menu-button-ViewController as a ChildViewController --> then everything works!
If I assign the "first-entry-ViewController" to be the Menu-button-ViewController directly, then the NavigationBar disappears !
Here is the overview:
I finally found a solution.
It indeed had to do with my login-architecture of this app.
The fact that only by setting the "first-entry-ViewController" as the old-Main-ViewController made a difference:
This old-Main-ViewController (that eventually adds the Menu-button-ViewController as its Child) did have the following line in its viewWillAppear method:
navigationController?.setNavigationBarHidden(true, animated: animated)
Its intention was actually to never show the navigationBar of its own. But instead load a ChildViewController that itself shows a navigationBar of its own.
The strange thing with storyboard: Even tough setting the Menu-button-ViewController as first-entry does somehow still consider the navigationController-hiding mechanism of the previous first-entry setting. This seems a bug to me inside storyboard. I would assume that visible navigationBar is the default behaviour. But having set it once to be hidden keeps it hidden, even tough the hiding-command is no longer executed. Anyway, very strange behaviour.
By eliminiting that line - or better - by adding it "with hidden = false" inside the Menu-Button-ViewController, makes the NavigationBar being shown again !!!
My learning is to keep an eye on all navigationController actions or mutations throughout the entire App hierarchy. The fact that a single ViewController might mutate something on its navigationController might not be enough. You have to check event parent-ViewControllers or segue-parents as well. And most annoying, applying a different first-entry to a VC does require you to overwrite default behaviours of your views to make sure your views are shown !

UITableview adding white space at top of view

I am running into a weird issue with a tableview adding an empty "white space" at the top. I have the table view constraint bound to 5 of the segmented control field above.
I am new to Swift and I am not sure how to further debug these types of UI issues. I have checked the constraints and I do not think that is the issue. The storyboard does not show this additional white space... where is it coming from?
EDIT: It appears to only create the whitespace on iOS10. Looks fine on iOS11.
EDIT: xCode screen
EDIT: I see someone else took my code and got selected already but for the sake of providing full answer here it is.
This behavior is caused by automatic insets by the ios platform. There are two options here:
If you snap your table view to bottom edge of navbar be sure to execute the code below. It will disable automatic insets on both iOS 11 and older iOS versions.
Otherwise you can snap your tableview to edge of the view and omit the code, because the purpose of the code is to compensate the size of navbar/tabbar, and since you snap your tableview behind/under them, you need that compensation to happen.
Code in case of #1 scenario that works on iOS 11 and older platforms.
Objective-c:
if (#available(iOS 11, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
self.automaticallyAdjustsScrollViewInsets = NO
}
Swift:
if #available(iOS 11.0, *) {
tableView.contentInsetAdjustmentBehavior = .never
} else {
automaticallyAdjustsScrollViewInsets = false
}
It seems that the automatic content insets on the table view are activated, you can stop this behavior by adding this code to your view controller.
if #available(iOS 11, *) {
self.tableView.contentInsetAdjustmentBehavior = .never;
}else{
self.automaticallyAdjustsScrollViewInsets = false
}
Try to look in the 'attribute inspector' (in the right menu) of the Participants ViewController. Check for the option 'Extend Edges' and uncheck the 'Under Top Bars', and then relocate your tableview.
This is the updated 2022 iOS 15 solution
if #available(iOS 15.0, *) {
UITableView.appearance().sectionHeaderTopPadding = CGFloat(0)
}

How to handle iOS 11 large title animation when using multiple container views?

I am making an app at the moment where 1 screen has a segmented control with 3 segments. Initially I had 1 table view and when you change segment I would simply change the data source/cell etc and reload the table. While this works great there is always the problem that when you change segments it will not remember your last scroll position because the table view gets reloaded.
I tried to get around this with storing offset position, rows etc but I could never get it to work like I wanted. Seems especially annoying when you have different cell types for the segments and they are self sizing as well.
I than decided to have a master view controller with the segmented control and 3 container views with their own VC and table view for each segment. I simply hide/show the correct container view when changing segments. This also works great but I have 1 problem with iOS 11 style large headers. Only the 1st container view added as a subview to the ViewControllers view manipulates the collasping/expanding of the title when you scroll.
Therefore when I change to the 2nd or 3rd container view and start scrolling I do not get the large title collapsing animation. How can I get around that?
I tried the following
1) Change Container view zPosition when changing segments
2) Move the container view to the front by calling view.bringSubview(toFront: ...)
3) Looping through the subviews and calling
view.exchangeSubview(at: 0, withSubviewAt: ...)
I believe I could remove all container views and add the one I need again and give them constraints but I wonder if there is a more straight forward solution.
Or if someone has a good solution to remember a tableViews scroll position before reloading it I would appreciate that too.
So I found an answer that seems to work for me, thanks to this great article. https://cocoacasts.com/managing-view-controllers-with-container-view-controllers/
Essentially what I did is
1) Remove the ContainerViews and Segues from the MasterViewController Storyboard.
2) Add a lazy property for each VC of the segmented control in the MasterViewController. They are lazy so that they only get initialised when you actually need them
lazy var viewController1: LibraryViewController = {
let viewController = UIStoryboard.libraryViewController // convenience property to create the VC from Storyboard
// do other set up if required.
return viewController
}()
lazy var viewController2: LibraryViewController = {
let viewController = UIStoryboard.libraryViewController // convenience property to create the VC from Storyboard
// do other set up if required.
return viewController
}()
3) Create an extension of UIViewController with the following 2 methods. I added them in an extension purely for code organisation as they might be reused on other ViewControllers.
extension UIViewController {
func add(asChildViewController viewController: UIViewController) {
// Add Child View Controller
addChildViewController(viewController)
// Add Child View as Subview
view.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = view.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
func remove(asChildViewController viewController: UIViewController) {
// Notify Child View Controller
viewController.willMove(toParentViewController: nil)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParentViewController()
}
}
4) Now in my segmented control method that gets called when you change segment I simply add the correct ViewController. The nice thing is that remove/adding them does not actually deallocate them.
func didPressSegmentedControl() {
if segmentedControl.selectedSegmentIndex == 0 {
remove(asChildViewController: viewController2)
add(asChildViewController: viewController1)
} else {
remove(asChildViewController: viewController1)
add(asChildViewController: viewController2)
}
}
5) Make sure you call the method at point 4 in ViewDidLoad so that the correct VC is added when the VC is loaded the 1st time.
func viewDidLoad() {
super.viewDidLoad()
didPressSegmentedControl()
}
This way when we remove a ChildViewController and add another one it will always be the the top VC in the subviews array and I get my nice title collapsing animation.
Another added benefit of this approach is that if you never go to a particular segment that particular VC will never get initialised, because they are lazy properties, which should help with efficiency.
Hope this helps somebody trying to do the same.
This is a horrible issue which I hope will be resolved soon, but there is another fix - although I freely admit that this is a nasty hack.
Basically, the issue only applies to the FIRST container view in the hierarchy. Add another container view to your hierarchy. Set it as hidden, then delete its segue and its target view controller just to be neat. Finally, make sure that this is the first container view in the hierarchy by dragging it to the top of the list.