I have a scrollView with the delegate method set.
private let scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero).usingAutoLayout()
scrollView.isPagingEnabled = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
I'm trying to making only scroll to the left to mimic a "delete cell", like in the phone book. I don't want the user to be able to scroll to the right. I have this, which kinda works:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x < 0 {
scrollView.contentOffset.x = 0
}
The problem is that if I swipe fast the contentOffSet is set to positive values, which makes the scrollView scroll in the opposite direction. This usually happens after I finish the swipe gesture. This makes me think it has to do with the bounce, but even setting it to false, it still occurs.
Was able to come up with a solution:
extension SwipeableCollectionViewCell: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView.superview).x > 0 {
self.scrollDirection = .rigth
} else {
self.scrollDirection = .left
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.scrollDirection == .rigth {
scrollView.contentOffset.x = 0
}
}
}
private enum ScrollDirection {
case rigth
case left
}
Related
In my collectionView the cells are the size of the entire screen, I use vertical scrolling, and I have .isPagingEnabled = true. I needed to prevent the user from swiping fast. Changing the .decelerationRate didn't work. The only thing that I found was to disable/enable the scrollView as the user swipes which almost works fine.
In the pic below, when the swiping starts, I disable the scrollView and show a red label at the top that says "cv Disabled". When swiping ends because the user lifted their finger, I enable the scrollView and remove the label. But when I swipe a hairline bit and quickly lift my finger from the cell, the red label never disappears and the scroll stays disabled. The orange arrow points to the next cell with a beige background that's about to be shown when I subtly swiped.
In the code below using both tries everything works fine when scrolling past a certain point occurs like full screen or a quarter of the screen. The issue is if I subtly swipe a hairline bit, the scrollView stays disabled because the methods myScrollView?.isUserInteractionEnabled = true are in never gets fired.
1st try:
var myScrollView: UIScrollView?
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if myScrollView == nil {
myScrollView = scrollView
}
myScrollView?.isUserInteractionEnabled = false
showRedDisabledLabel()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
myScrollView?.isUserInteractionEnabled = true
removeRedDisabledLabel()
}
2nd try:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if myScrollView == nil {
myScrollView = scrollView
}
myScrollView?.isUserInteractionEnabled = false
showRedDisabledLabel()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: nil, afterDelay: 0.3)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
myScrollView?.isUserInteractionEnabled = true
removeRedDisabledLabel()
NSObject.cancelPreviousPerformRequests(withTarget: self)
}
This problem literally only occurs when I swipe that hairline bit and quickly lift my finger, every other time the code from both tries works perfectly. The question is in scrollViewWillBeginDragging, how can I detect when a user swipes past a certain point so that I can then set myScrollView?.isUserInteractionEnabled = false
I couldn't find a way to to determine how much a user swiped in scrollViewWillBeginDragging but I did find a workaround to the actual problem of hairline scrolling. I put myScrollView?.isUserInteractionEnabled and the redLabel into their own functions, combined this answer and this answer, created an instance property named isScrolling, and use didSet to determine if it's scrolling or not and call the functions from there:
// Properties
var myScrollView: UIScrollView?
var isScrolling = false {
didSet {
if isScrolling {
disableScrollView()
} else {
enableScrollView()
}
}
}
// My 2 Functions
func disableScrollView() {
myScrollView?.isUserInteractionEnabled = false
showRedDisabledLabel()
}
func enableScrollView() {
myScrollView?.isUserInteractionEnabled = true
removeRedDisabledLabel()
}
// UIScrollView Delegate Methods
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if myScrollView == nil {
myScrollView = scrollView
}
isScrolling = true
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
isScrolling = false
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
isScrolling = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: nil, afterDelay: 0.3)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
isScrolling = false
NSObject.cancelPreviousPerformRequests(withTarget: self)
}
I want to show / hide UISearchBar programmatically but I am not having the desire results.
What I am trying to do is hide search bar when scrolling down and show it when scrolling up:
I set my UISearchBar like this:
var mySearchcontroller = UISearchController(searchResultsController: nil)
mySearchcontroller.obscuresBackgroundDuringPresentation = false
mySearchcontroller.searchBar.placeholder = "search"
mySearchcontroller.searchBar.delegate = self
definesPresentationContext = true
self.navigationItem.searchController = mySearchcontroller
self.navigationItem.hidesBackButton = true
self.navigationItem.hidesSearchBarWhenScrolling = false
And the result is
I implement the scrollViewDidScroll to make the search bar show or hide when scrolling:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0)
{
navigationItem.hidesSearchBarWhenScrolling = false
}
else
{
navigationItem.hidesSearchBarWhenScrolling = true
}
}
And the result is:
As you realise that the GRP label or a segmented control is been masked by the search bar and I am not having the same effect where the search bar push down all controls (figure 1).
How can I solve this?
You achieve this functionality by following steps:
Give proper Auto-layout to your views.
Set IBOutlet for the Height of your headerview/searchview like:
#IBOutlet weak var constrainHeightHeader: NSLayoutConstraint!
Write the scrollview delegate methods like:
//MARK: - Scrollview delegate
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
//
self.contentOffSet = self.cwProducts.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//
let scrollPos = self.cwProducts.contentOffset.y
if scrollPos == self.contentOffSet{
return
}
if(scrollPos > self.contentOffSet ){
//Fully hide your toolbar
self.constrainHeightHeader.constant = 0
UIView.animate(withDuration: 0.2, animations: {
self.view.layoutIfNeeded()
}, completion: { (status) in
self.headerVW.isHidden = true
})
} else {
if(self.isFromBanner) {
self.constrainHeightHeader.constant = 0
}else{
self.constrainHeightHeader.constant = 50
}
UIView.animate(withDuration: 0.2, animations: {
self.view.layoutIfNeeded()
}, completion: { (status) in
self.headerVW.isHidden = false
})
}
}
I hope you are getting my point and by minor changes in the above code you will get solution.
It's working in my project 100% tested.
in my app i am using small menu at the bottom of uiwebview. and i want to make like when user scroll downside that view must be hide. and when scrolling upside view must be unhide.
Like Safari.
this is what i tried
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
print("Going Down")
viewbottom.hidden = true
viewHieght.constant = 0
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
print("Going Up")
viewbottom.hidden = false
viewHieght.constant = 45
}
but by using this code its continuously showing up and down.
Use scroll view pan gesture recogniser to determine the direction.
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translationInView(scrollView.superview).y > 0 {
// scrolls down
} else {
// scrolls up
}
}
keep tracking the scrollView.contentOffset.y value and compare the last with the current value like so:
in your mainView add: var lastScrollOffset = CGFloat()
compare the last value with the current one in func scrollViewDidScroll(scrollView: UIScrollView)
func scrollViewDidScroll(scrollView: UIScrollView) {
let scrollOffset = scrollView.contentOffset.y
if lastScrollOffset < scrollOffset{
//scrolling down
}else if lastScrollOffset > scrollOffset {
//scrolling up
}else{
//going crazy
}
lastScrollOffset = scrollOffset
}
I want to disable my scrollview bounce when scroll down.
When I disable bounce vertically I can't refresh my table.
Any suggestion how to disable bounce, but enable refresh table?
I'm refreshing this way:
self.refreshControl = UIRefreshControl()
self.refreshControl.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refreshControl)
func refresh(sender:AnyObject) {
getJson()
self.tableView.reloadData()
self.refreshControl.endRefreshing()
}
Thanks.
Just did it this way:
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0.0 {
return
}
if (scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.size.height) {
scrollView.setContentOffset(CGPointMake(scrollView.contentOffset.x, scrollView.contentSize.height - scrollView.frame.size.height), animated: false)
}
}
Implement the scrollViewDidScroll method in the UIScrollViewDelegate
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > 0 {
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: 0), animated: false)
}
}
}
This allows the table view to pull to refresh down, but restricts pulling up. This removes the bounce in the upwards direction.
Side-effect: this will not let you go beyond the cells already on view, so you can't scroll down to cells not visible.
I am using a UIPageViewController, and I need to get the scroll position of the ViewController as the users swipe so I can partially fade some assets while the view is transitioning to the next UIViewController.
The delegate and datasource methods of UIPageViewController don't seem to provide any access to this, and internally I'm assuming that the UIPageViewController must be using a scroll view somewhere, but it doesn't seem to directly subclass it so I'm not able to call
func scrollViewDidScroll(scrollView: UIScrollView) {
}
I've seen some other posts suggestion to grab a reference to the pageViewController!.view.subviews and then the first index is a scrollView, but this seems very hacky. I'm wondering if there is a more standard way to handle this.
You can search for the UIScrollView inside your UIPageViewController. To do that, you will have to implement the UIScrollViewDelegate.
After that you can get your scrollView:
for v in pageViewController.view.subviews{
if v.isKindOfClass(UIScrollView){
(v as UIScrollView).delegate = self
}
}
After that, you are able to use all the UIScrollViewDelegate-methods and so you can override the scrollViewDidScroll method where you can get the scrollPosition:
func scrollViewDidScroll(scrollView: UIScrollView) {
//your Code
}
Or if you want a one-liner:
let scrollView = view.subviews.filter { $0 is UIScrollView }.first as! UIScrollView
scrollView.delegate = self
UIPageViewController scroll doesn't work like normal scrollview and you can't get scrollView.contentOffset like other scrollViews.
so here is a trick to get what's going on when user scrolls :
first you have to find scrollview and set delegate to current viewController like other answers said.
class YourViewController : UIPageViewController {
var startOffset = CGFloat(0) //define this
override func viewDidLoad() {
super.viewDidLoad()
//from other answers
for v in view.subviews{
if v is UIScrollView {
(v as! UIScrollView).delegate = self
}
}
}
.
.
.
}
extension YourViewController : UIScrollViewDelegate{
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startOffset = scrollView.contentOffset.x
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
var direction = 0 //scroll stopped
if startOffset < scrollView.contentOffset.x {
direction = 1 //going right
}else if startOffset > scrollView.contentOffset.x {
direction = -1 //going left
}
let positionFromStartOfCurrentPage = abs(startOffset - scrollView.contentOffset.x)
let percent = positionFromStartOfCurrentPage / self.view.frame.width
//you can decide what to do with scroll
}
}
Similar to Christian's answer but a bit more Swift-like (and not unnecessarily continuing to loop through view.subviews):
for view in self.view.subviews {
if let view = view as? UIScrollView {
view.delegate = self
break
}
}
As of iOS 13, the UIPageViewController seems to reset the scrollview's contentOffset once it transitions to another view controller. Here is a working solution:
Find the child scrollView and set its delegate to self, as other answers suggested
Keep track of the current page index of the pageViewController:
var currentPageIndex = 0
// The pageViewController's viewControllers
let orderredViewControllers: [UIViewController] = [controller1, controller2, ...]
pageViewController.delegate = self
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard completed, let currentViewController = pageViewController.viewControllers?.first else { return }
currentPageIndex = orderredViewControllers.firstIndex(of: currentViewController)!
}
Get the progress that ranges from 0 to 1
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffsetX = scrollView.contentOffset.x
let width = scrollView.frame.size.width
let offset = CGFloat(currentPageIndex) / CGFloat(orderredViewControllers.count - 1)
let progress = (contentOffsetX - width) / width + offset
}
var pageViewController: PageViewController? {
didSet {
pageViewController?.dataSource = self
pageViewController?.delegate = self
scrollView?.delegate = self
}
}
lazy var scrollView: UIScrollView? = {
for subview in pageViewController?.view?.subviews ?? [] {
if let scrollView = subview as? UIScrollView {
return scrollView
}
}
return nil
}()
extension BaseFeedViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.x
let bounds = scrollView.bounds.width
let page = CGFloat(self.currentPage)
let count = CGFloat(viewControllers.count)
let percentage = (offset - bounds + page * bounds) / (count * bounds - bounds)
print(abs(percentage))
}
}
To make the code as readable and separated as possible, I would define an extension on UIPageViewController:
extension UIPageViewController {
var scrollView: UIScrollView? {
view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView
}
}
It's quite easy to set yourself as the delegate for scroll view events, as so:
pageViewController.scrollView?.delegate = self