Animate contraint change when user is scrolling in tableview - ios

I have a view which serves as a container for another 2 views(one view which takes 1/4 of the screen and a table view which takes 3/4 of the screen). What I want is that when the user scrolls up(swipe direction) the tableview, the table view will smoothly go up (up to the screen) and go down when users scrolls down(swipe direction).
So far I have managed so that the tableview goes up but it does it too sudden and can't make it go down.
Here is how i do it:
viewViewTopConstraint is the tableview top constraint to container top constraint
extension StatsOverlayViewController: UIScrollViewDelegate {
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView == statsTableView {
let scrolledOffset = scrollView.contentOffset.y
while viewViewTopConstraint.constant > 0 && viewViewTopConstraint.constant < 69 {
print("scrolling with offset\(scrollView.contentOffset.y)")
self.viewViewTopConstraint.constant -= scrolledOffset
}
}
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView == statsTableView {
if viewViewTopConstraint.constant < 0 {
viewViewTopConstraint.constant = 0
} else if viewViewTopConstraint.constant > 69 {
viewViewTopConstraint.constant = 69
}
}
}
}
Edit:
Problem with putting the view back down was due to the scrollview.bounces = true and the wrong use of the while loop
Code that works is :
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView == statsTableView {
let scrolledOffset = scrollView.contentOffset.y
UIView.animateWithDuration(0.2, animations: {
if scrolledOffset > 0 {
self.viewViewTopConstraint.constant = 0
} else {
self.viewViewTopConstraint.constant = self.viewViewTopConstraintOriginalValues
}
self.view.layoutIfNeeded()
})
}
}

You need to call self.view.layoutIfNeeded within UIView.animate block
UIView.animate(withDuration: 1) {
self.view.layoutIfNeeded
}
Add this code right after you change your constraint. If you want a little bit more control on your animation, use this
UIView.animate(withDuration: 1, delay: 0, options: [.curveEaseInOut], animations: {
self.view.layoutIfNeeded
}, completion: nil)

Related

How to show / hide custom view depending the contentOffset of my tableview?

I have a chat app and I'm trying to show a custom view that I've made when the user scrolls to the top, also hide it if it's on the bottom of tableview. (like whatsapp does it)
To be honest I'm struggling with the logic of show/hide button.
Tried to save the contentOffset.y of my tableview right after I reload the data so I'll know that's the bottom, and if it's smaller to show the custom view, but mainTableView.contentOffset.y it's always 0.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if (scrollView == mainTableView) {
print(mainTableView.contentOffset.y)
if let point = startingPointForView {
//where var startingPointForView: CGFloat?
// and tried to save it after I reload the data
//self.startingPointForView = self.mainTableView.contentOffset.y
// but it's always 0
}
// Show and hide button logic
}
}
An image of what I m trying to achieve: https://imgur.com/ZkYEi2P
try this code to hide/show custom view according to UIscrollview contentOffset
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollViewContentHeight = scrollView.contentSize.height
let scrollViewHeight = scrollView.frame.height
if scrollView.contentOffset.y < (scrollViewContentHeight - scrollViewHeight){
//Custom view show
}else{
//Custom view Hide
}
}
May be this code will help you
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView).y > 0 {
// down
button.isHidden = false
} else {
// up
button.isHidden = true
}
}
For someone who is looking to hide a button when tableview is scrolling can use below code:
var previousContentOffset: CGFloat = CGFloat()
extension YourViewController: UIScrollViewDelegate{
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.yourTableView{
let currentContentOffset = scrollView.contentOffset.y
if (currentContentOffset > previousContentOffset) {
// scrolling towards the bottom
if scrollView.contentOffset.y > 50 {
self.yourButton.isHidden = true
} else {
self.yourButton.isHidden = false
}
} else if (currentContentOffset < previousContentOffset) {
// scrolling towards the top
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
// Change 10.0 to adjust the distance from bottom
if maximumOffset - currentContentOffset <= 10.0 {
self.yourButton.isHidden = true
} else {
self.yourButton.isHidden = false
}
}
previousContentOffset = currentContentOffset
}
}
}

How to prevent a UITableView from scrolling up past its initial origin?

Currently, I have set up a UITableView below a UIView whose origin is (0, 287) in an example of an iPhone 7.
This is achieved using the following sample code:
extension ViewController: UIScrollViewDelegate
{
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
if scrollView.contentOffset.y > 0 && imageContainerViewHeightConstraint.constant == 223
{
return
}
if scrollView.contentOffset.y > 0
{
var sd = imageContainerViewHeightConstraint.constant + abs(scrollView.contentOffset.y)
if (sd < 223 )
{
print("path111 1")
self.dataTableView.contentOffset = CGPoint.init(x: 0, y: 0 )
return
}
else
{
imageContainerViewHeightConstraint.constant -= abs(scrollView.contentOffset.y)
}
view.layoutIfNeeded()
self.dataTableView.contentOffset = .zero
return
}
if scrollView.contentOffset.y < 0 && imageContainerViewHeightConstraint.constant >= initialContainerImageViewHeight * 2
{
self.dataTableView.contentOffset = .zero
return
}
else
{
imageContainerViewHeightConstraint.constant += abs(scrollView.contentOffset.y)
view.layoutIfNeeded()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
resetContainerViewSize()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
{
resetContainerViewSize()
}
}
func resetContainerViewSize()
{
imageContainerViewHeightConstraint.constant = initialContainerImageViewHeight
UIView.animate(withDuration: 0.4,
delay: 0.0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: .curveEaseInOut,
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
If I slowly scroll down, then slowly scroll up, the UITableView will stop at the origin and go no further up, and never enters the condition if (sd < 223 )....like so:
However, if I scroll down, and pull up fast, once the top of the section header reaches the initial origin of (0, 287), the UITableView will scroll a bit further up and enters the condition if (sd < 223 ), because once I let go, the UITableView will bounce back down to its origin.....like so:
If I pull up on the UITableView even faster, it will scroll even further up and bounce back down once I let go of the UITableView:
I've found some similar questions:
Stop UITableView over scroll at top & bottom?
Limit the scroll for UITableView
Prevent UITableView scrolling below a certain point
I've tried the suggested solution to a variation, by disabling alwaysBounceVertical and bounces, but this just disables scrolling altogether, which is not what I want.
Since UITableView is a subclass of UIScrollView, I've always tried setting scrollView.contentOffset = .zero within the condition if(sd < 223 ), which also did not help.
How would I prevent the UITableView from being pulled up past its initial origin points, i.e. (0, 287)?
I've included a sample project: sample project

Swift: change tableview height on scroll

I have a VC as shown in the image
It has a UICollectionView on top, and a UITableView at the bottom.
CollectionView has 1:3 of the screen and TableView has 2:3 of the screen(set using equalHeight constraint).
I want to change the height of the UICollectionView when the tableView is scrolled.
When the tableView is scrolled up,I want to change the multiplier of equalHeights constraint to like 1:5 and 4:5 of collectionView and tableView respectively.This will ensure that height of tableView increases and collectionView decreases
When the tableView is scrolled down, the multiplier of equalHeights constraint should reset to default.
I've tried adding swipe and pan gestures to tableView, but they are unrecognised.
How to achieve this functionality?
P.S: Would love to achieve this functionality by using a pan gesture, so that dragging up and down changes the heights progressively.
This is the view hierarchy
EDIT/UPDATE
This is the code that I'm using.
class MyConstant {
var height:CGFloat = 10
}
let myConstant = MyConstant()
MainScreenVC
override func viewWillAppear(_ animated: Bool) {
myConstant.height = self.view.frame.size.height
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (self.lastContentOffset < scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.decreaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = -self.view.frame.height / 6
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
} else if (self.lastContentOffset > scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.increaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = 0
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
}
self.lastContentOffset = scrollView.contentOffset.y
}
Cell.Swift
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(decreaseHeight), name: Notification.Name("decreaseHeightNotification"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(increaseHeight), name: Notification.Name("increaseHeightNotification"), object: nil)
self.contentView.translatesAutoresizingMaskIntoConstraints = false
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = heightConstraint.constant * 1.5
}
#objc func decreaseHeight() {
heightConstraint.constant = (myConstant.height / 6) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
#objc func increaseHeight() {
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
Now when I scroll both simultaneously, the screen freezes. Also is there a better way of resizing the collectionViewCell size?
I haven't tested it but you can do this. Use Autolayout in this view. it will work better with that.
Set the tableview constraint as Top, Left, Bottom, Right => 0, 0, 0, 0 with the main view and put collectionView under the tableview with constraint as Top,Left,Right, height => 0, 0, 0, x with the main view.
Note: Tableview is on top of the collectionView.
Connect your height constraint outlet of CollectionView and also define your defaultOffset variable
#IBOutlet weak var defaultHeightConstraint: NSLayoutConstraint
var defaultOffSet: CGPoint?
In viewDidLoad,
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset = UIEdgeInsetsMake(collectionView.size.height, 0, 0, 0)
}
In viewDidAppear, write
override func viewDidAppear(_ animated: Bool) {
defaultOffSet = tableView.contentOffset
}
In ViewDidScroll
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = tableView.contentOffset
if let startOffset = self.defaultOffSet {
if offset.y < startOffset.y {
// Scrolling down
// check if your collection view height is less than normal height, do your logic.
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant - deltaY
} else {
// Scrolling up
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant + deltaY
}
self.view.layoutIfNeeded()
}
}
Hope it helps.
Since UITableView is a subclass of UIScrollView, your table view's delegate can receive UIScrollViewDelegate methods.
You don't need to use PanGesture
First implement UITableviewDelegate and in that
var oldContentOffset = CGPoint.zero
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y - oldContentOffset.y
if (self.oldContentOffset < scrollView.contentOffset.y) {
// moved to top
} else if (self.oldContentOffset > scrollView.contentOffset.y) {
// moved to bottom
} else {
// didn't move
}
oldContentOffset = scrollView.contentOffset
}
Here you can write you logic to change constant value of your constraint
UPDATE/EDIT
Another thing is don't use multiplier for tabelview height.with collection view it is fine you can give height constraint with self.view in ratio of 1/3 . for tableview just give leading ,trailing top (UICollectionView) and bottom.
So when you change height constant of UICollectionView Tableview height automatically decrease and vice-versa for decrease.
You can not change the multiplier from the outlet as it is read only. so you have to constant value to self.view.frame.height / 3 will work for you
Hope it is helpful to you
For a much cleaner approach, why don't you use a UIScrollView as the container view and add the UICollectionView and UITableView inside it and give the collectionView a height constraint while disabling tableView's scroll till the height constraint of your collectionView becomes 0.
Here's the snippet:
Extend your VC with UIScrollViewDelegate and use:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= 0 && scrollView.contentOffset.y <= yourLimit {
heightConstraint.constant = collectionViewHeight - scrollView.contentOffset.y
tableView.isScrollEnabled = false
} else if scrollView.contentOffset.y > yourLimit {
heightConstraint.constant = collectionViewHeight
tableView.isScrollEnabled = true
}
}
Try this snippet after customizing small things in it. It should work mostly, if it doesn't there is another approach which I'd share once this doesn't work.
Why don't you just add Custom Table Cell with your collection view inside, and when indexPath.row == 0 , return your custom table cell.

Easiest way to disable back scrolling in scrollview

I want to stop backward scrolling on ScrollView after user scrolls to the next page. How can I do that.
I tried the following two codes, but the first one does not have any effect
self.scrollView.contentOffset = CGPointMake(self.view.frame.width,0)
and the second only disables the forward scrolling.
self.scrollView.contentSize = CGSizeMake( 2 * scrollWidth, scrollHeight);
To disable scrolling in one direction you implement the UIScrollViewDelegate method scrollViewDidScroll and put your logic there. For instance this TableViewController can only ever scroll down, because if the user tries to scroll up, we just overwrite the contentOffset, effectively undoing their scroll before they see it.
class ViewController: UITableViewController {
var lastScrollPosition = CGPoint.zero
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.y > lastScrollPosition.y else {
scrollView.setContentOffset(lastScrollPosition, animated: false)
return
}
lastScrollPosition = scrollView.contentOffset
}
}
If your cell is equal in size to your screen, you can apply the following option, which is very smooth:
var lastScrollPosition = CGPoint.zero
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x == lastScrollPosition.x + UIScreen.main.bounds.width {
lastScrollPosition.x += UIScreen.main.bounds.width
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.x > lastScrollPosition.x else {
scrollView.setContentOffset(lastScrollPosition, animated: false)
return
}
}

UITableView get middle position via content offset

I want to add a little button in the bottom right corner of a UITableView. When we are in the top half of the tableview, the button programmatically scrolls you to the bottom, and when you are in the bottom half, it takes you to the top. And the button's icon changes from "goto top" to "goto bottom" or vice versa depending on the situation.
In my code below, the button works fine- you press it when you are at the top, you hit the bottom, and the button graphic changes to the "goto top" icon. However, dragging up and down like you would traditionally in a table doesn't make the button change it's icon properly. You even need to pull past the border (top or bottom) of the table to make it flip to the correct state.
When a button is pressed, if we are past halfway (as defined by a function pastHalfway), we go either to the top or bottom of the table. Something is wrong with it, but I've tussled a bit and basically made things not work well in various ways. I think the problem is I am not determining the midpoint content offset of the table correctly.
func pastHalfway() -> Bool {
// TODO: This doesn't work
let offset:CGPoint = self.tableView.contentOffset
let height:CGFloat = self.tableView.frame.height
println("pastHalfway offset.y=\(offset.y) height=\(height)")
return offset.y > (height/3.0) // 3.0 is 3/4 down the table
}
func gotoButtonPressed(sender: UIButton!) {
if let realPost = post {
if realPost.numberComments > 0 {
if self.pastHalfway() {
let indexPath:NSIndexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
} else {
let indexPath:NSIndexPath = NSIndexPath(forRow: realPost.numberComments - 1, inSection: 1)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Bottom, animated: true)
}
}
}
}
func maybeShowGotoButtons() {
let yOffset:CGFloat = self.tableView.contentOffset.y
if (self.post!.numberComments < minRowsToShowGotoButtons) {
// Hide and disable buttons
self.gotoButton.hidden = true
self.gotoButton.enabled = false
} else {
// Show buttons, depending on content offset
if self.pastHalfway() {
self.gotoButton.setImage(UIImage(named: "gotoTopIcon"), forState: .Normal)
} else {
self.gotoButton.setImage(UIImage(named: "gotoBottomIcon"), forState: .Normal)
}
// And enable
self.gotoButton.hidden = false
self.gotoButton.enabled = true
}
}
UIScrollView Delegates
override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
UIView.animateWithDuration(0.1, animations: {
self.gotoButton.alpha = 0.5
})
}
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
UIView.animateWithDuration(0.2, animations: {
self.gotoButton.alpha = 1.0
self.maybeShowGotoButtons()
})
}
override func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
// This is used for the programmatic scroll top/bottom when clicking buttons
self.maybeShowGotoButtons()
}
The necessary fixes were adding 1)
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
self.maybeShowGotoButtons()
}
2)
func atBottom() -> Bool {
let height = self.tableView.contentSize.height - self.tableView.frame.size.height
if self.tableView.contentOffset.y < 10 {
//reach top
return false
}
else if self.tableView.contentOffset.y < height/2.0 {
//not top and not bottom
return false
}
else {
return true
}
}

Resources