How to change UIPageControl dot not only when scrollView did end decelerating? - ios

I have created a User Onboarding as a Collection View with 2 items.
The Collection View has a UIPageControl which shows an active page user currently on.
The problem is I can change the state of active UIPageControl dot only when the transition is ended, after user swiped to the next\previous screen.
This is how it looks now: GIF
I want the behaviour so when you start to swipe UIPageControl should already change its active dot onto the next\previous one without me necessarily ending the swipe (lift the finger).
You can check what I want on the gif: GIF
Here is how I change the UIPageControl dot now:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let width = scrollView.frame.width
currentPage = Int(scrollView.contentOffset.x / width)
pageControl.currentPage = currentPage
}
How my code should be modified, which method to use to achieve the behaviour? I couldn't find any similar questions on StackOverflow.

Based on your question...
you have only TWO cells (pages)
you want the page control to reflect which page is most visible
Instead of scrollViewDidEndDecelerating, implement:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let fw = scrollView.frame.width
// get the percentage scrolled
let pct = scrollView.contentOffset.x / fw
// if it's less-than 50%, we're at page 0
// else, we're at page 1
pageControl.currentPage = pct < 0.5 ? 0 : 1
}

Related

Implement twitter Profile like view with parallax scroll

I want to implement the following sort of view where the view can be completely scrolled and houses 2 different scrollview (Main and the secondary) with infinite scrollable content. This represents the exact thing I want.
The red view is superview - should scroll vertically
The green view is of the height of the current view and is just static. That doesnt scroll
The blue view is the horizontal scrollview where for each label there is a yellow vertically scrolling infinity collection view
the labels scroll as in the given video. under each label there is the collection view I mentioned in point 3
The blue box is the scroll view and I want the scrolling to happen horizontally in a parallax way such as this.
I am able to implement the above parallax in the correct fashion but each title contains their own collectionview. When I implement this I am not able to have an infinite scroll. Below is the code for that :
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == containerScrollView {
for i in 0..<shotsData.count {
let label = scrollView.viewWithTag(i + tagValueL) as! UILabel
let view = scrollView.viewWithTag(i + tagValueV) as! ShotsMediaView
let scrollContentOffset = scrollView.contentOffset.x + scrollView.frame.width
let viewOffset = (view.center.x - scrollView.bounds.width/4) - scrollContentOffset
label.center.x = scrollContentOffset - ((scrollView.bounds.width/4 - viewOffset)/2)
}
}
}
How can I exactly achieve the same behavior with an infinite scroll vertically? I want each of these titles to have collectionview that have the dynamic height each.
I did a crude implementation of this.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == colorsCollectionView {
let newContentOffSetX = scrollView.contentOffset.x
let distance = contentOffSetX + newContentOffSetX
// Scroll the text collection view proportinately
let titleScrollDistance = (distance/colorsCollectionView.frame.width * 75.0)
titlesCollectionView.contentOffset = CGPoint(x: titleScrollDistance, y: titlesCollectionView.contentOffset.y)
contentOffSetX = newContentOffSetX
}
}
contentOffSetX is a property of the class(ViewController) that I use to keep track of the scrolling distance of the bottom collection view. Initially that is set to 0. When the user scrolls the collection view at the bottom, the above delegate method is called. Then I use the contentOffSet to get the distance that was scrolled along the X-axis. I map that to the width of the title labels(hardcoded as 75.0) to calculate the distance that collection has to be scrolled. Hope this can be refined to serve your purpose, but I am sure that there are better methods out there :)

How can I detect when the user tried to swipe "past UIScrollView content offset"?

For example, I have a button that when the user tap on it, it will open a pop up scroll view from the bottom, which has a lengthy vertical content. If the user tried to swipe down when the scroll view content offset is at the top, I want to close the scroll view pop up by animating it flying to the bottom of the screen.
How can I detect that event? I know that I can get the event when scroll view did scroll using scrollViewDidScroll function. But it didn't give me any info of whether the user is scrolling up or down. So I can't check like "when the offset.y is 0 and user is still swiping .down then close the popup". Please help. Thanks.
Try this in scrollViewDidScroll(_ scrollView: UIScrollView) method
let scrollDiff = scrollView.contentOffset.y - self.previousScrollOffset
let absoluteTop: CGFloat = 0;
let absoluteBottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height;
let isScrollingDown = scrollDiff > 0 && scrollView.contentOffset.y > absoluteTop
let isScrollingUp = scrollDiff < 0 && scrollView.contentOffset.y < absoluteBottom
self.previousScrollOffset = scrollView.contentOffset.y
and set this as global variable var previousScrollOffset: CGFloat = 0;
you can print and check if this is scolling up or down
hope this will give you the direction

How to get the length of a scroll move

How can I get the length of scroll by dragging in a tableview, I want to execute a function inside scrollViewWillBeginDragging but only when the user scroll long, not when the user just touch the screen and the scroll bar moves by 1 mm, I need to know how match the scroll bar move so I can put a condition on that
scrollViewWillBeginDragging only fires once at the start of the gesture. scrollViewDidScroll continuously gets called as the user scroll. Just store the initial content offset of your tableview on scrollViewWillBeginDragging and check with the current offset in scrollViewDidScroll and perform you codes once there is enough movement.
var currentPosition : CGFloat!
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
currentPosition = tableView.contentOffset.y
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let amoutOfScroll = abs(tableView.contentOffset.y - currentPosition) // abs return absulote value, just to avoid getting nagative values
let scrollLenght = 25 // the length of scroling I need
if amoutOfScroll > scrollLenght {
// code
}
}

Add data dynamically to table when Floating Action Button is pressed

I have a scrollView (subclassed from custom UITableView()) which has number of rows and columns and scrolls in horizontal and vertical direction. I have to add data dynamically to the scrollView when floating action button is pressed.
UPDATED: I was able to display the dynamic data successfully in rows and columns. But I don't want to load my scrollView with all the dynamic data upon scroll, because data is too large to handle. Instead, I have to display only 5 columns initially and when user presses the floating action button, data should get added to rows and columns dynamically again with a count of 5 columns and 'x' rows.
I am stuck in adding dynamic data to scrollView when floating action button is pressed. Also how to show only 5 columns initially and add more 5*n columns when floating action button is pressed.
Any Suggestion or Help is much appreciated. Thank you :)
I suggest using a CollectionView which has build-in horizontal scrolling support instead of a custom TableView.
For the "floating action button" I would suggest to create a separate view that has a label that tells the user to release to load more items and moves in when the user reaches the end of the scrollView. In the following example the load more view has a width of 100px and suggest that user releases his finger after dragging the view 85px.
Swift 3
#IBOutlet fileprivate weak var loadMoreView: UIView!
#IBOutlet fileprivate weak var loadMoreLabel: UILabel!
#IBOutlet fileprivate weak var loadMoreConstraint: NSLayoutConstraint!
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x + scrollView.frame.size.width > scrollView.contentSize.width {
loadMoreView.isHidden = false
loadMoreLabel.text = "Load more"
if (scrollView.contentOffset.x + scrollView.frame.size.width) - scrollView.contentSize.width > 85 {
loadMoreLabel.text = "Release"
}
if (scrollView.contentOffset.x + scrollView.frame.size.width) - scrollView.contentSize.width < 100 {
loadMoreConstraint.constant = 100 - ((scrollView.contentOffset.x + scrollView.frame.size.width) - scrollView.contentSize.width)
}
} else {
loadMoreView.isHidden = true
loadMoreConstraint.constant = 100
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.contentOffset.x + scrollView.frame.size.width > scrollView.contentSize.width {
if (scrollView.contentOffset.x + scrollView.frame.size.width) - scrollView.contentSize.width > 85 {
// TODO: <Load more logic here>
}
}
}
The load more view has the following constraints, where the "Align Trailing" constraint is linked with the loadMoreConstraint IBOutlet in the code above.

Swift: how to make a ScrollView work with PageControl?

Strctly following this tutorial, in section Paging with UIScrollView, I have just implemented a ScrollView to use as a slideshow with downloaded photos from a previous UICollectionViewController. When scroll view is loaded, it does not work well because I see these ones:
Instead, when I slide back the images they are displayed in the correct way, one for each page. Or better, this problem disappears when I get the 4-th image in the slideshow, and only at that point all the following ones are correct, and so are the previous too. It is a problem which affects the first 2 or 3 images.
Moreover the slideshow does not even start from the UICollectionViewCell the user has just tapped, but always from the first. You can read all the code here:
import Foundation
import UIKit
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet var scrollView: UIScrollView!
#IBOutlet var pageControl: UIPageControl!
/* This will hold all the images to display – 1 per page.
It must be set from the previous view controller in prepareforsegue() method:
it will be the array of downloaded images for that photo gallery */
var pageImages:[UIImage]!
/* position in array images of the first to be showed, i.e. the one the user has just tapped */
var firstToShow:Int!
var currentImageViewForZoom:UIImageView?
/* This will hold instances of UIImageView to display each image on its respective page.
It’s an array of optionals, because you’ll be loading the pages lazily (i.e. as and when you need them)
so you need to be able to handle nil values from the array. */
var pageViews:[UIImageView?] = []
override func viewDidLoad() {
super.viewDidLoad()
self.scrollView.delegate = self
self.scrollView.maximumZoomScale = 1.0
self.scrollView.zoomScale = 10.0
self.pageControl.numberOfPages = self.pageImages.count
self.pageControl.currentPage = self.firstToShow
for _ in 0..<self.pageImages.count {
self.pageViews.append(nil)
}
/* The scroll view, as before, needs to know its content size.
Since you want a horizontal paging scroll view, you calculate the width to be the number of pages multiplied by the width of the scroll view.
The height of the content is the same as the height of the scroll view
*/
let pagesScrollViewSize = self.scrollView.frame.size
self.scrollView.contentSize = CGSize(width: pagesScrollViewSize.width * CGFloat(self.pageImages.count),
height: pagesScrollViewSize.height)
// You’re going to need some pages shown initially, so you call loadVisiblePages()
self.loadVisiblePages()
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return self.currentImageViewForZoom
}
func scrollViewDidScroll(scrollView: UIScrollView) {
self.loadVisiblePages()
}
/*
Remember each page is a UIImageView stored in an array of optionals.
When the view controller loads, the array is filled with nil.
This method will load the content of each page.
1 - If it's outside the range of what you have to display, then do nothing
2 - If pageView is nil, then you need to create a page. So first, work out the frame for this page.
It’s calculated as being the same size as the scroll view, positioned at zero y offset,
and then offset by the width of a page multiplied by the page number in the x (horizontal) direction.
3 - Finally, you replace the nil in the pageViews array with the view you’ve just created,
so that if this page was asked to load again, you would now not go into the if statement and instead do nothing,
since the view for the page has already been created
*/
func loadPage(page: Int) {
if page < 0 || page >= self.pageImages.count {
//1
return
}
//2
if let _ = self.pageViews[page] {/*Do nothing. The view is already loaded*/}
else {
// 2
var frame = self.scrollView.bounds
frame.origin.x = frame.size.width * CGFloat(page)
frame.origin.y = 0.0
let newPageView = UIImageView(image: self.pageImages[page])
newPageView.contentMode = .ScaleAspectFit
newPageView.frame = frame
self.scrollView.addSubview(newPageView)
// 3
self.pageViews[page] = newPageView
self.currentImageViewForZoom = newPageView
}
}
/*
This function purges a page that was previously created via loadPage().
It first checks that the object in the pageViews array for this page is not nil.
If it’s not, it removes the view from the scroll view and updates the pageViews array with nil again to indicate that this page is no longer there.
Why bother lazy loading and purging pages, you ask?
Well, in this example, it won’t matter too much if you load all the pages at the start, since there are only five and they won’t be large enough to eat up too much memory.
But imagine you had 100 pages and each image was 5MB in size. That would take up 500MB of memory if you loaded all the pages at once!
Your app would quickly exceed the amount of memory available and be killed by the operating system.
Lazy loading means that you’ll only have a certain number of pages in memory at any given time.
*/
func purgePage(page: Int) {
if page < 0 || page >= self.pageImages.count {
// If it's outside the range of what you have to display, then do nothing
return
}
// Remove a page from the scroll view and reset the container array
if let pageView = self.pageViews[page] {
pageView.removeFromSuperview()
self.pageViews[page] = nil
}
}
func loadVisiblePages() {
// First, determine which page is currently visible
let pageWidth = self.scrollView.frame.size.width
// floor() function will round a decimal number to the next lowest integer
let page = Int(floor((self.scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0))) /***/
// Update the page control
self.pageControl.currentPage = page
// Work out which pages you want to load
let firstPage = page - 1
let lastPage = page + 1
// Purge anything before the first page
for var index = 0; index < firstPage; ++index {
self.purgePage(index)
}
// Load pages in our range
for index in firstPage...lastPage {
self.loadPage(index)
}
// Purge anything after the last page
for var index = lastPage+1; index < self.pageImages.count; ++index {
self.purgePage(index)
}
}
}
I guess the problem could be the line with /***/ which is something I have not understood from the tutorial. Thank you for your attention
UPDATE
Looking here in SO for similar posts, someone advised to create subviews in viewDidLayoutSubviews(), so here's what I have just tried:
override func viewDidLayoutSubviews() {
self.loadVisiblePages()
}
and now the images are correctly displayed and there's no more that strange effect for the first 3 images. But why? I am a junior iOS developer and I still don't know all those methods to override and the order in which they work. Anyway, the other problem which persists is that images are showed always from the first, even if another image is tapped. For example, have a look at this:
always the first image (left up corner) is displayed even if another one is tapped. And finally, in my code I implemented the delegate method to have zoom but it does not work too.
UPDATE 2
Here's the code of prepareForSegue() from the previous UICollectionViewController when the user taps a cell:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "toSlideShow") {
let pagedScrollViewController:PagedScrollViewController = segue.destinationViewController as! PagedScrollViewController
pagedScrollViewController.pageImages = self.imagesDownloaded
pagedScrollViewController.firstToShow = self.collectionView?.indexPathsForSelectedItems()![0].row
}
}
You should update the scroll view's offset to the be equal to the offset of the image you want to be showing after you transition. self.scrollView.contentOffset = CGPoint(x: pagesScrollViewSize.width * CGFloat(self.firstToShow), y: 0.0) You can do this in viewDidLayoutSubviews, which would make it look like:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollView.delegate = self
self.scrollView.maximumZoomScale = 2.0
self.scrollView.zoomScale = 1.0
self.scrollView.minimumZoomScale = 0.5
self.pageControl.numberOfPages = self.pageImages.count
self.pageControl.currentPage = self.firstToShow
for _ in 0..<self.pageImages.count {
self.pageViews.append(nil)
}
/* The scroll view, as before, needs to know its content size.
Since you want a horizontal paging scroll view, you calculate the width to be the number of pages multiplied by the width of the scroll view.
The height of the content is the same as the height of the scroll view
*/
let pagesScrollViewSize = self.scrollView.frame.size
self.scrollView.contentSize = CGSize(width: pagesScrollViewSize.width * CGFloat(self.pageImages.count),
height: pagesScrollViewSize.height)
self.scrollView.contentOffset = CGPoint(x: pagesScrollViewSize.width * CGFloat(self.firstToShow), y: 0.0)
// You’re going to need some pages shown initially, so you call loadVisiblePages()
self.loadVisiblePages()
}
The line you highlighted is just rounding down to the closest image index to determine what page the scroll view is on. The problem is when your view controller is displayed no scrolling has been done so it will always show the first image. To get the image you want to show first, you can just calculate what the offset should be for the image set your scroll view's offset to that as shown above.

Resources