Shrink Navigation Bar on scroll based on a Scroll View - ios

I have a Scroll View set to a fixed height inside my View Controller. I want to use a navigation bar on top with large titles, so when i scroll the Scroll View, it should collapse like in a Navigation Controller. Is it possible to do this? My scene look like this:
The navigation bar has top/left/right 0 constraints agains the View. Currently it stays on top correctly, however it won't collapse on scroll as expected.

Do not use a "loose" navigation bar like this. Use a navigation controller, even if you do not intend to do any navigation. It gives you the desired behavior, for free.

In the end i created a custom view to replicate the Navigation Bar. Here you can see how it looks and read the steps below to replicate:
To setup your View Controller to be used with a custom Scroll View, first make sure you are using Freeform size for your controller. To do this, select Freeform in the size inspector and set the height to your new Scroll View's height:
Insert your Scroll View and setup 0 top/left/right/bottom constraints, so it will be the same size as your View Controller:
Add your content to your scroll view as usual
Now to create your custom Navigation Bar, add a View outside of your Scroll View and setup constraints like this:
Notice a few things:
the top constraint is aligned to the Superview instead of the Safe Area, so the view goes behind the status bar
The height is set to >= 44, so its a minimum height and can expand if the content is larger
On the Attribute Inspector, select clip to bounds, so your content inside the view won't overflow(like in CSS, overflow:hidden)
At this point you might see some errors in your Storyboard, but don't worry about it: its because you don't have any content in your View and it doesn't know how tall it should be
Set the View background to transparent and add a "Visual Effect View with Blur" inside, with 0 top/left/right/bottom constraints. This will blur the content behind the custom navigation bar
Now make sure that you check the Safe Area Layout Guide checkbox in your navigation bar view(its above the constraints setup):
This way you can add content inside the view that won't be behind the status bar, because its outside of the safe area. And it works with the notch too.
Add a label inside your view, set top and bottom constraints to Safe Area and make sure you have a fixed height constraint defined too:
Now you can also see that the errors in your Storyboard are gone :) At this point this is how everything should look like:
Now the coding part. In your ViewController, make outlets for both the ScrollView and the custom navigation bar. To do this, switch to the assistant editor(the venn-diagram symbol top right), select the element in your storyboard, hold down CTRL and drag inside your ViewController class:
Do the same for your View that is your navigation bar:
#IBOutlet weak var mainScrollView: UIScrollView!
#IBOutlet weak var customNavigationBar: UIView!
Next, you need to add the UIScrollViewDelegate to your class, so you can listen to the scroll event and get the actual Y position of the current scroll offset using the scrollViewDidScroll function:
class ViewController: UIViewController, UIScrollViewDelegate {
You also need to setup the delegate in your viewDidLoad hook:
mainScrollView.delegate = self
Create a new function called scrollViewDidScroll to get the scroll position and you can use this to do various animations with other elements. In this case, if the scroll position reaches 44(this is the height i set for my custom navigation bar), it will animate to full opacity:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let y = self.mainScrollView.contentOffset.y
let barHeight = 44
if(y < barHeight) {
customNavigationBar.alpha = y/CGFloat(barHeight)
} else {
customNavigationBar.alpha = 1
}
}
You can use the same logic to animate the label inside the navigation bar, change its size etc...
The full ViewController:
class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var mainScrollView: UIScrollView!
#IBOutlet weak var customNavigationBar: UIView!
override func viewDidLoad() {
super.viewDidLoad()
mainScrollView.delegate = self
customNavigationBar.alpha = 0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let y = self.mainScrollView.contentOffset.y
let barHeight = 44
if(y < 44) {
customNavigationBar.alpha = y/CGFloat(barHeight)
} else {
customNavigationBar.alpha = 1
}
}
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
var height = CGFloat()
if(scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0) {
height = 130
}
else {
height = 44
}
UIView.animate(withDuration: 0.5) {
self.navBarHeightConstraint?.constant = height
self.view.layoutIfNeeded()
}
}

Related

How to auto resize width of Vertical UIStackView when one of the view is hidden?

I have a Vertical UIStackView(S1) with UIView (V1) and UILabel(L1). UIView contains Horizontal StackView(S2) with an ImageView and Vertical StackViews(S3) to show label one below the other.
Now when I hide top view i.e V1, then my Label occupied full height which is expected. But I want stack view to compress to show only Label(L1) content. But in my case it is not reducing the width.
Here are my ViewTree and Snapshots when launched and when V1 is hidden.
When you set .isHidden = true on a stack view's arranged subview, the stack view will remove the space it was taking up... but only in the .axis direction.
So your Stack View still allocates the width of Top PINK View.
To remove the height and width of Top PINK View, you'll need to remove it from the stack view... not just hide it.
Try it like this - tapping the button will toggle between hidden and showing:
#IBOutlet var mainStackView: UIStackView!
#IBAction func showHide(_ sender: Any) {
if !topPINKView.isHidden {
topPINKView.isHidden = true
topPINKView.removeFromSuperview()
} else {
mainStackView.insertArrangedSubview(topPINKView, at: 0)
topPINKView.isHidden = false
}
}
Note: be sure to connect your stack view to the #IBOutlet var mainStackView: UIStackView!
Edit
You could even reduce that to:
#IBAction func showHide(_ sender: Any) {
if topPINKView.superview != nil {
topPINKView.removeFromSuperview()
} else {
mainStackView.insertArrangedSubview(topPINKView, at: 0)
}
}

Scroll UIView to expand into iPhone X top notch

I have the following layout:
UIImageView
UIView (with UILabel)
UITableView
As the tableView is scrolled up, the height of the imageView is decreased before actually scrolling the tableView. The following code is used for that:
let headerImageViewMaxHeight: CGFloat = 200
let headerImageViewMinHeight: CGFloat = UIApplication.shared.statusBarFrame.height
#IBOutlet var headerImageViewHeightConstraint: NSLayoutConstraint!
#IBOutlet var headerImageView: UIImageView!
#IBOutlet var subtitleView: UIView!
#IBOutlet var subtitleLabel: TextLabel!
private var contentOffsetDictionary: NSMutableDictionary!
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.isKind(of: UICollectionView.self) {
let horizontalOffset: CGFloat = scrollView.contentOffset.x
let collectionView: UICollectionView = scrollView as! UICollectionView
contentOffsetDictionary.setValue(horizontalOffset, forKey: collectionView.tag.description)
} else if scrollView == tableView {
let y: CGFloat = scrollView.contentOffset.y
let newHeaderImageViewHeight: CGFloat = headerImageViewHeightConstraint.constant - y
if newHeaderImageViewHeight > headerImageViewMaxHeight {
headerImageViewHeightConstraint.constant = headerImageViewMaxHeight
} else if newHeaderImageViewHeight < headerImageViewMinHeight {
headerImageViewHeightConstraint.constant = headerImageViewMinHeight
} else {
headerImageViewHeightConstraint.constant = newHeaderImageViewHeight
scrollView.contentOffset.y = 0
}
}
}
The following two images shown the current states of scrolled down or up:
Scrolled down (initial state).
Scrolled up.
As you can see, initially the grey bar saying To Table -> Table 1 is scrolled down and is about 60 high.
When it's scrolled up, it scrolles until the safeArea. What I want to achieve is that once it reaches the safe area, it continues scrolling up into the safeArea (top notch), while keeping the text in exactly the same place (topSpace to safeArea of the text should stay the same). So, basically growing the UIView to go into the safeArea.
This image shows the ViewController layout.
Header View can be ignored, as that is aligned to the top of the viewController, but no constraints to anything else than superView.
TopSpace of the view in question, is 0 to the UIImageView.
Changing headerImageViewMinHeight to 0, makes it scroll up to the actual top of the screen, without expanding it. So this seems like a good start, but needs some extra logic or constraints.
As the UIView doesn't have a specific height constraint, changing its UILabel's top space from = 16 to => 16, there's the warning of missing Y constraint or height.
Otherwise that, together with UILabel's top space to the viewController's safeArea of => 8 sounds like it could work.
Any ideas are welcome.
EDIT: The following picture basically shows what I want, except that here the UILabel's top is not aligned to the safe area.
What this picture shows is achieved by changing headerImageViewMinHeight to 0.
Add constriant
Uncheck constraint to margin
double click on Align bottom/ Align top
click on last baseline (for bottom)/ first baseline (for top constrraint)
DONE

Nested scrollview with Vertical scrolling for both [duplicate]

I need to do this app that has a weird configuration.
As shown in the next image, the main view is a UIScrollView. Then inside it should have a UIPageView, and each page of the PageView should have a UITableView.
I've done all this so far. But my problem is that I want the scrolling to behave naturally.
The next is what I mean naturally. Currently when I scroll on one of the UITableViews, it scrolls the tableview (not the scrollview). But I want it to scroll the ScrollView unless the scrollview cannot scroll cause it got to its top or bottom (In that case I'd like it to scroll the tableview).
For example, let's say my scrollview is currently scrolled to the top. Then I put my finger over the tableview (of the current page being shown) and start scrolling down. I this case, I want the scrollview to scroll (no the tableview). If I keep scrolling down my scrollview and it reaches the bottom, if I remove my finger from the display and put it back over the tebleview and scroll down again, I want my tableview to scroll down now because the scrollview reached its bottom and it's not able to keep scrolling.
Do you guys have any idea about how to implement this scrolling?
I'm REALLY lost with this. Any help will be greatly appreciate it :(
Thanks!
The solution to simultaneously handling the scroll view and the table view revolves around the UIScrollViewDelegate. Therefore, have your view controller conform to that protocol:
class ViewController: UIViewController, UIScrollViewDelegate {
I’ll represent the scroll view and table view as outlets:
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var tableView: UITableView!
We’ll also need to track the height of the scroll view content as well as the screen height. You’ll see why later.
let screenHeight = UIScreen.mainScreen().bounds.height
let scrollViewContentHeight = 1200 as CGFloat
A little configuration is needed in viewDidLoad::
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentSize = CGSizeMake(scrollViewContentWidth, scrollViewContentHeight)
scrollView.delegate = self
tableView.delegate = self
scrollView.bounces = false
tableView.bounces = false
tableView.scrollEnabled = false
}
where I’ve turned off bouncing to keep things simple. The key settings are the delegates for the scroll view and the table view and having the table view scrolling being turned off at first.
These are necessary so that the scrollViewDidScroll: delegate method can handle reaching the bottom of the scroll view and reaching the top of the table view. Here is that method:
func scrollViewDidScroll(scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if scrollView == self.scrollView {
if yOffset >= scrollViewContentHeight - screenHeight {
scrollView.scrollEnabled = false
tableView.scrollEnabled = true
}
}
if scrollView == self.tableView {
if yOffset <= 0 {
self.scrollView.scrollEnabled = true
self.tableView.scrollEnabled = false
}
}
}
What the delegate method is doing is detecting when the scroll view has reached its bottom. When that has happened the table view can be scrolled. It is also detecting when the table view reaches the top where the scroll view is re-enabled.
I created a GIF to demonstrate the results:
Modified Daniel's answer to make it more efficient and bug free.
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var tableHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
//Set table height to cover entire view
//if navigation bar is not translucent, reduce navigation bar height from view height
tableHeight.constant = self.view.frame.height-64
self.tableView.isScrollEnabled = false
//no need to write following if checked in storyboard
self.scrollView.bounces = false
self.tableView.bounces = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 30))
label.text = "Section 1"
label.textAlignment = .center
label.backgroundColor = .yellow
return label
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Row: \(indexPath.row+1)"
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.scrollView {
tableView.isScrollEnabled = (self.scrollView.contentOffset.y >= 200)
}
if scrollView == self.tableView {
self.tableView.isScrollEnabled = (tableView.contentOffset.y > 0)
}
}
Complete project can be seen here:
https://gitlab.com/vineetks/TableScroll.git
After many trials and errors, this is what worked best for me. The solution has to solve two needs 1) determine who's scrolling property should be used; tableView or scrollView? 2) make sure that the tableView doesn't give authority to the scrollView until it has reached the top of it's table/content.
In order to see if the scrollview should be used for scrolling vs the tableview, i checked to see if the UIView right above my tableview was within frame. If the UIView is within frame, it's safe to say the scrollView should have authority to scroll. If the UIView is not within frame, that means that the tableView is taking up the entire window, and therefor should have authority to scroll.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.intersects(UIView.frame) == true {
//the UIView is within frame, use the UIScrollView's scrolling.
if tableView.contentOffset.y == 0 {
//tableViews content is at the top of the tableView.
tableView.isUserInteractionEnabled = false
tableView.resignFirstResponder()
print("using scrollView scroll")
} else {
//UIView is in frame, but the tableView still has more content to scroll before resigning its scrolling over to ScrollView.
tableView.isUserInteractionEnabled = true
scrollView.resignFirstResponder()
print("using tableView scroll")
}
} else {
//UIView is not in frame. Use tableViews scroll.
tableView.isUserInteractionEnabled = true
scrollView.resignFirstResponder()
print("using tableView scroll")
}
}
hope this helps someone!
None of the answers here worked perfectly for me. Each one had it's owned nuanced problem (needing to do a repeated swipe when one scrollview hit it's bottom, or the scroll indicator not looking correct, etc), so figured I'd throw in another answer.
Ole Begemann has a great write up on doing this exactly https://oleb.net/blog/2014/05/scrollviews-inside-scrollviews/
Despite being an old post, the concepts still apply to the current APIs. Additionally, there is a maintained (Xcode 9 compatible) Objective-C implementation of his approach https://github.com/eyeem/OLEContainerScrollView
If you are facing problem with the nested scrolling issue , here tis the simplest solution for it .
go to your design screen
select your scroll view and then disable bounce on scroll
if your view uses table view inside scroll view then disable bounce on scroll of the table view as well
run and check it is solved
check how to disable bounce on scroll of a scroll view
check how to disable bounce on scroll of a tableview view
I was struggling with this problem, too. There is a very simple solution.
In interface builder:
create simple ViewController
add a simple View, it will be our header, and constrain it to superview
it's the red view on the example below
I have added 12px from top, left and right, and set fixed height to 128px
embed a PageViewController, making sure it is constrained to the superview, and not the header
Now, here comes the fun part: for each page you add, make sure its tableView has an offset from top. Thats it. You can do if with this code, for example (assuming you use UITableViewController as a page):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let tables = viewControllers.compactMap { $0 as? UITableViewController }
tables.forEach {
$0.tableView.contentInset = UIEdgeInsets(top: headerView.bounds.height, left: 0, bottom: 0, right: 0)
$0.tableView.contentOffset = CGPoint(x: 0, y: -headerView.bounds.height)
}
}
No messy scroll inside scroll inside table view, no mangling with delegates, no duplicated scrolls, perfectly natural behavior. If you can't see the header, it is probably because of the tableView background color. You have to set it to clear, for the header to be visible from under the tableView.
I think there are two options.
Since you know the size of the scroll view and the main view, you are unable to tell whether the scroll view hit the bottom or not.
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) {
// reach bottom
}
So when it hit; you basically set
[contentScrollView setScrollEnabled:NO];
and other way around for your tableView.
The other thing, which is more precise I think, is to add Gesture to your views.
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(respondToTapGesture:)];
// Specify that the gesture must be a single tap
tapRecognizer.numberOfTapsRequired = 1;
// Add the tap gesture recognizer to the view
[self.view addGestureRecognizer:tapRecognizer];
// Do any additional setup after loading the view, typically from a nib
So when you add Gesture, you can simply control the active view by changing setScrollEnabled in the respondToTapGesture.
I found an awesome library
MXParallaxHeader
In Storyboard just set UIScrollView class to MXScrollView then magic happens.
I used this class to handle my UIScrollView when I embed a UIPageViewController container view. even you can insert a parallax header view for more detail.
Also, this library provides Cocoapods and Carthage
I attached an image below which represent UIViewHierarchy.
MXScrollView Hierarchy
SWIFT 5
I had some trouble using Vineet's answer for when I could not guarantee the scrollView content offset (Y) due to various different screen sizes. To resolve this, I changed the first trigger event of when the tableView's scroll gets enabled.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.contains(button.frame) {
tableView.isScrollEnabled = true
}
if scrollView == tableView {
self.tableView.isScrollEnabled = (tableView.contentOffset.y > 0)
}
}
The scrollView.bounds.contains will check if a given element's frame is FULLY within the scrollView's visible content. I set this to a button that I have below the tableView. You could set this to your tableVIew's frame instead if your only condition is that your tableView is fully visible.
I left the original implementation of when to disable the tableView's scroll and it works very well.
I tried the solution marked as the correct answer, but it was not working properly. The user need to click two times on the table view for scroll and after that I was not able to scroll the entire screen again. So I just applied the following code in viewDidLoad():
tableView.addGestureRecognizer(UISwipeGestureRecognizer(target: self, action: #selector(tableViewSwiped)))
scrollView.addGestureRecognizer(UISwipeGestureRecognizer(target: self, action: #selector(scrollViewSwiped)))
And the code below is the implementation of the actions:
func tableViewSwiped(){
scrollView.isScrollEnabled = false
tableView.isScrollEnabled = true
}
func scrollViewSwiped(){
scrollView.isScrollEnabled = true
tableView.isScrollEnabled = false
}
One easy trick, if you want to achieve it is replacing parent scrollview with normal container view.
Adding a pan gesture on container view, you can play with top constraint of first view to assign negative values. You can keep a check of page View's origin if it achieves to top you can start assigning that value on content offset of the pageView's child view. Until user achieves the table view in a state of top most view in container view, you can keep page tableView's scrolling disabled and allow scrolling manually by setting content offset.
So initially the page view height will be collapsed (or say out of screen) or less at bottom. Later on scrolling down it will expand to take more space.
Gesture will automatically stop responding if out of frames say on nav bar or other view outside container view.
Gestures are a key to user interactive transitions used in many apps. You can mimic scroll for a certain time with it.
In my case I'm using constraint for height like that:
self.heightTableViewConstraint.constant = self.tableView.contentSize.height
self.scrollView.contentInset.bottom = self.tableView.contentSize.height
Below code works great for me
As I wanted to show some header after some scroll and table view supposed to scroll
And in ViewDidLoad add
override func viewDidLoad() {
super.viewDidLoad()
mainScrollView.delegate = self
}
Change 265 to whatever number you want to stop upper scroll
extension AccountViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(notebookTableView.contentOffset.y)
if notebookTableView.contentOffset.y < 265 {
if notebookTableView.contentOffset.y > 0 {
mainScrollView.setContentOffset(notebookTableView.contentOffset, animated: false)
} else {
mainScrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false)
}
} else {
mainScrollView.setContentOffset(CGPoint(x: 0.0, y: 265), animated: false)
}
}
}
CGFloat tableHeight = 0.0f;
YourArray =[response valueForKey:#"result"];
tableHeight = 0.0f;
for (int i = 0; i < [YourArray count]; i ++) {
tableHeight += [self tableView:self.aTableviewDoc heightForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
}
self.aTableviewDoc.frame = CGRectMake(self.aTableviewDoc.frame.origin.x, self.aTableviewDoc.frame.origin.y, self.aTableviewDoc.frame.size.width, tableHeight);
Maybe brute-force, but working perfectly if cell heights are the same: by the way, I use auto layout.
for the tableView (or collectionView or whatever), set an arbitrary height in storyboard, and make an outlet to class. Wherever appropriate, (viewDidLoad() or...) set the tableView's height big enough so that tableView doesn't need to scroll. (need to know the number of rows in advance) Then only the outer scrollView will scroll nicely.

storyboard dynamic auto layout issue

I have this situation :
When I tap on "add" button I reduce the pink view's(first view) height and I execute this code:
#IBOutlet weak var viewPink: UIView!
#IBAction func add(_ sender: AnyObject) {
viewPink.frame = CGRect(x: viewPink.frame.origin.x, y: viewPink.frame.origin.y, width: viewPink.frame.size.width, height: viewPink.frame.size.height - 50)
}
but I want that the last view remains to the same distance from the pink view , essentially you have to climb on why the pink view reduces its height , instead the second view remains where it was before.
Can you help me about it?
P.S I set the vertical spacing constraint between the two views but It doesn't work
You should add an Height constraint on your pink view, create an IBOutlet to this constraint in your ViewController, and set the "constant" property to change the height.
Example:
heightConstraint.constant = 150
This will change the height with Autolayout, you shouldn't change the height by setting a new frame because it doesn't use Autolayout.

Animate UIView from center to top using autolayout constraings swift 2.1

I want to animate UITextField from center to top of the view on a button click. TextField has following constraints
Following is the code to on button click to move textfiled to top.
UIView.animateWithDuration(0.5) {
self.txtfield.center.y = 10
}
above code works, textfiled moved to top but when I go back and come to this view again textfield is again in center. I am new to swift I want that once textfield is moved to top it should stay on top.
You should get a reference to the constraint in your code. Then you change the constant value of the constraint instead.
I have added the code to move the text field to top in viewDidLoad but you will of course have this code in your button action. You have to drag from the "Align Center Y" and into your view controller in order to create a reference to the constraint. The reason why I subtract 10 is because your example and intention was to have a 10 points margin from the top. And take note that I "reversed" the first and second item like this:
Remember that when you use auto layout, the frame and center and the size of the views bounds get set by auto layout constraints.
class ViewController: UIViewController {
#IBOutlet weak var textFieldYAlignConstraint: NSLayoutConstraint!
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
UIView.animateWithDuration(1.2) { () -> Void in
self.centeraligntConstraint.constant = -400
self.view.layoutIfNeeded()// to animate layout constraint
}
}
}

Resources