I'm attempting to find a good way to restrict user scrolling around certain sections within my UITableView. For instance, when section 3 is selected (by my logic) I would like to have it locked at the top of the screen so you cannot scroll away from it until it is unlocked, but the important part is I still want to keep the responsive feel of the UIScrollView (ie. the bounce interactivity).
I attempted to recreate the logic on my own as seen below (which works pretty well, but doesn't have the same bounce feel I am looking for):
override func scrollViewDidScroll(scrollView: UIScrollView) {
let sectionRect = tableView.rectForSection(activeHeaderSection)
let topY = sectionRect.origin.y
let bottomY = topY + sectionRect.height
let frameSize = self.tableView.frame.size.height - self.navigationController!.navigationBar.frame.size.height
let translation = scrollView.panGestureRecognizer.translationInView(scrollView)
if (scrollView.contentOffset.y + frameSize > bottomY) && (frameSize < sectionRect.size.height) && (translation.y < 0) {
UIView.animateLinear(0.7, initialSpringVelocity: 0, animations: { self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: self.checkTuple[self.activeHeaderSection].pickChecks.count-1, inSection: self.activeHeaderSection), atScrollPosition: .Bottom, animated: false) }, completion: nil)
} else if (frameSize > sectionRect.size.height) || (scrollView.contentOffset.y < topY) {
UIView.animateLinear(0.7, initialSpringVelocity: 0, animations: { self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: self.activeHeaderSection), atScrollPosition: .Top, animated: false) }, completion: nil)
}
}
So I did some searching around here and some question pointed towards restricting the scrollView's contentView height and position (ie. something like this) :
override func scrollViewDidScroll(scrollView: UIScrollView) {
var activeHeaderSection = 4
let sectionRect = tableView.rectForSection(activeHeaderSection)
tableView.contentOffset.y = sectionRect.origin.y // Set once when locked
tableView.contentSize.height = sectionRect.size.height
tableView.clipsToBounds = false
}
But this also isn't really giving me the behaviour I want as it always sticks to the top of my UITableView (with the correct height) but does not scroll down to the proper section. I'd prefer to use something simple like the second method if possible, I'm not sure if there is an even easier method for accomplishing something like this so I thought I'd ask here.
I think this
tableView.contentOffset.y = sectionRect.origin.y
tableView.contentSize.height = sectionRect.size.height
should be:
tableView.contentInset.top = -sectionRect.origin.y
tableView.contentSize.height = sectionRect.origin.y + sectionRect.size.height
Related
I want a horizontal scrollable collection view to scroll in both side(left and right), now it has continuous scrolling only in right direction, how to implement in left side ?
let itemcount = array?.count ?? 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == DemoCollectionView {
let offSet = scrollView.contentOffset.x
let width = scrollView.frame.width
let horizontalCenter = width / 2
let currentPage = Int(offSet + horizontalCenter) / Int(width)
if currentPage >= itemCount / 2 {
itemCount += array?.count ?? 0
DemoCollectionView.reloadData()
}
}
}
You can do it using "scrollToItemAtIndexPath" method. Swipe direction you can get from scrollview delegate methods.
Scroll right to left
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: dataArray.count - 1, inSection: 0), atScrollPosition: .Right, animated: false)
Scroll left to right
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: 0, inSection: 0), atScrollPosition: .left, animated: false)
You can also apply a transformation on collection view to get similar.
I want to stop UIScrollView scrolling when it reached at specific point.
I thought it will be solve by below code.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollY: CGFloat = scrollView.contentOffset.y
stopScrollViewIfNeeded(by: scrollY)
}
func stopScrollViewIfNeeded(by scrollY: CGFloat) {
guard scrollY <= SpecificY else {
return
}
scrollView.isScrollEnabled = false
}
But I was wrong. because contentOffset.y be to 0 when scroll be disabled.
And I was improved my function like below code, but it still didn't work as I wanted.
func stopScrollViewIfNeeded(by scrollY: CGFloat) {
guard scrollY <= SpecificY else {
return
}
scrollView.setContentOffset(.init(x: 0, y: SpecificY), animated: false)
scrollView.isScrollEnabled = false
scrollView.isPagingEnabled = true
}
How do I improve stopScrollViewIfNeeded() to work as I wanted?
just set contentOffset as:
Just need to get the offset value at which you want it to be stopped and Assign values as Below
Horizontal ScrollView
UIView.animate(withDuration: 0.5) {
self.MainScollView.contentOffset.x = self.view.frame.size.width*2
}
or Vertical scrollView
UIView.animate(withDuration: 0.5) {
self.MainScollView.contentOffset.y = 90
}
You can override the scroll position in didScroll. Logic in the didScroll can effectively make the scroll stick for as long as you want.
I've two tableviews side by side, I use below code to make them scroll like one table when I scroll on either of the two tables.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.coinNameTableView {
self.coinInfoTableView?.setContentOffset(CGPoint.init(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y), animated: false)
}
if scrollView == self.coinInfoTableView {
self.coinNameTableView?.setContentOffset(CGPoint.init(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y), animated: false)
}
}
But there is one issue, when the first cell is at the begin of the table, then I scroll down, the whole table will shift down and not bounce back to the original location.
In the scrollViewDidScroll method, write logic for content offset. If content offset is less than 0, than set the content offset == 0.
Reference code:-
if scrollView.ContentOffset.y < 0{
self.coinInfoTableView?.setContentOffset(CGPoint.init(x: 0, y: 0), animated: false)
self.coinNameTableView?.setContentOffset(CGPoint.init(x: 0, y: 0), animated: false)
}
I made an UIScrollView in a XIB for my onboarding. The UIScrollView has 3 onboarding views. Long story short:
This works perfect. However I want the top left and right buttons (Overslaan - Volgende) to animate up / off the screen when the third/last page is on screen. My UIScrollView starts behaving weird when I animate the buttons off:
This is the code im using:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageIndex = Int(targetContentOffset.pointee.x / self.frame.width)
pageControl.currentPage = pageIndex
if stepViews[pageIndex] is OnboardingLoginView {
moveControlConstraintsOffScreen()
} else {
moveControlConstraintsOnScreen()
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
}, completion: nil)
}
I debugged the code and it turns out that setting a new constant for the constraints causes the issue, regardless of the animation block. How do I make the buttons move up/off the screen without my scrollView behaving weird?
It looks like triggering a layout pass is interfering with your scroll view positioning. You could implement func scrollViewDidScroll(_ scrollView: UIScrollView) and try to update the button view on a per-frame basis, without changing Auto Layout constraints. I usually use the transform property for frame changes outside of Auto Layout, since any changes to frame or bounds are overwritten during the next layout pass.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard mustBeOnLastPage else {
buttonOne.tranform = .identity
buttonTwo.tranform = .identity
return
}
let offset = scrollView.contentOffset.x
buttonOne.tranform = .init(translationX: 0, y: offset)
buttonTwo.tranform = .init(translationX: 0, y: offset)
}
Old answer
This interpretation is a bit of tangent of what you're asking for.
Since it looks like you're using the scroll view in a paging context, I would approach this problem by using UIPageViewController. Since UIPageViewController uses UIScrollView internally, you can observe the contentOffset of the last view in the scroll view to determine how far along the page has scrolled.
Yes, this involves looking inside the view hierarchy, but Apple hasn't changed it for half a decade so you should be safe. Coincidentally, I actually implemented this approach last week and it works like a charm.
If you're interested, I can further expand on this topic. The post that pointed me in the right direction can be found here.
Here is what i do
I can't post image,you can look at this http://i.imgur.com/U7FHoMu.gif
and this is the code
var centerYConstraint: Constraint!
func setupConstraint() {
fadeView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
centerYConstraint = make.centerY.equalToSuperview().constraint
make.size.equalTo(CGSize(width: 50, height: 50))
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint,targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageIndex = Int(targetContentOffset.pointee.x / self.view.frame.width)
if pageIndex != 1 {
centerYConstraint.update(offset: self.view.frame.height)
} else {
centerYConstraint.update(offset: 0)
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Based on #CloakedEddy's answer I made 2 changes:
1: It seems layoutSubviews is responsible for the weird behaviour. To fix this I prevent the scrollView from calling layoutSubviews all the time:
override func layoutSubviews() {
super.layoutSubviews()
if !didLayoutSubviews {
for index in 0..<stepViews.count {
let page: UIView = stepViews[index]
let xPosition = scrollView.frame.width * CGFloat(index)
page.frame = CGRect(x: xPosition, y: 0, width: scrollView.bounds.width, height: scrollView.frame.height)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(index + 1)
}
didLayoutSubviews = true
}
}
2: If you want to update your views for device orientations:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
onboardingView.didLayoutSubviews = false
onboardingView.setNeedsLayout()
}
I have a tableView which has a custom inputAccessoryView, and I have tableView.keyboardDismissMode set to .interactive. I subclassed UIView, and I create this class as my custom inputAccessoryView.
There is a textView in the view, and I have it resize automatically until a certain number of lines is reached. This is my code for that:
override var intrinsicContentSize: CGSize {
DispatchQueue.main.async {
let path = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0)
self.tableView.scrollToRow(at: path, at: .bottom, animated: true)
}
sendButton.frame.origin.y = self.frame.maxY - 5 - sendButton.frame.height
return CGSize(width: self.bounds.width, height: (textView.numberOfLines() < 10 ? textView.text.sizeForWidth(width: textView.frame.width, font: textView.font!).height : 254) + textView.frame.origin.y * 2)
}
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = !textView.text.isEmpty
if textView.numberOfLines() < 10 { // I defined numberOfLines() elsewhere
textView.isScrollEnabled = false
} else {
textView.isScrollEnabled = true
}
invalidateIntrinsicContentSize()
}
The .async {} block is for scrolling to the bottom of the tableView again once the inputAccessoryView height increases.
My problem is that when I drag down on the tableView to hide the keyboard, the tableView runs the scrolling animation (the one in the async block) when I do not want it to scroll to the bottom. Here is a video of what is happening.
I have been struggling with getting this to work how I want for a couple days now. Could anyone please explain to me why it is not working like I want it to and how I can fix it?
Thanks a lot in advance!
I solved my own problem! Hooray!
To do this, I created a public value in the ViewController for if the tableView is currently being dragged/scrolled. I used the UIScrollView delegate methods to set if the tableView is currently being scrolled:
scrollViewWillBeginDragging(_:)
and
scrollViewDidEndDragging(_:)
I then changed intrinsicContentSize to this:
override var intrinsicContentSize: CGSize {
if viewController.isDragging == false && textView.isFirstResponder {
DispatchQueue.main.async {
let path = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0)
self.tableView.scrollToRow(at: path, at: .bottom, animated: true)
}
}
sendButton.frame.origin.y = self.frame.maxY - 5 - sendButton.frame.height
return CGSize(width: self.bounds.width, height: (textView.numberOfLines() < 10 ? textView.text.sizeForWidth(width: textView.frame.width, font: textView.font!).height : 254) + textView.frame.origin.y * 2)
}