Swift: how to make a ScrollView work with PageControl? - ios

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.

Related

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

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
}

How to display a determined item in a paging UIScrollView instead of just loading it from 0

I want to recreate the same concept that the iOS built-in gallery app does in a basic level, that means having an array of images and being able to look at them individually and swiping right or left to see the other ones in the array. For testing purpose, I've set a small array of UIImages, and then added an individual subview for every image that there is in the array to a scrollView with paging enabled. Everything works as expected, however every time I load the app, the user starts from the first item (item #0 in the array) and I don't find a way to select manually the first image that needs to be shown, for example what if I want the scrollView to first show the third image in the array, and then be able to swipe left or right to page to the other images in the array? I can't find a solution to it.
I've tried to add other subviews but this doesn't work, the app works as expected but always starts from the first item in the array, I want to manually select the item so if I want to display the third image in the array, the collectionView does it like so. I have six images for testing purposes (each called menu_icon_(0...5))
#IBOutlet weak var scrollView: UIScrollView! {
didSet {
scrollView.delegate = self
}
}
var images:[ImagePreviewView] = [ImagePreviewView]()
override func viewDidLoad() {
super.viewDidLoad()
images = generateImages()
setupSlideScrollView(imagesPreviews: images)
}
func generateImages() -> [ImagePreviewView]{
var array:[ImagePreviewView] = []
for i in 0...5 {
let imagePreview = Bundle.main.loadNibNamed("ImagePreview", owner: self, options: nil)?.first as! ImagePreviewView
imagePreview.userSelectedImageView.image = UIImage(named: "menu_icon_\(i)")
array.append(imagePreview)
}
return array
}
func setupSlideScrollView(imagesPreviews : [ImagePreviewView]) {
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
scrollView.contentSize = CGSize(width: view.frame.width * CGFloat(imagesPreviews.count), height: view.frame.height)
scrollView.isPagingEnabled = true
for i in 0 ..< imagesPreviews.count {
imagesPreviews[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: view.frame.height)
scrollView.addSubview(imagesPreviews[i])
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex = round(scrollView.contentOffset.x/view.frame.width)
print("Page Index is \(pageIndex)")
}
I want to implement this code in such a way that the user adds images to a collection view, and when the user taps in an image, that indexPath.item is sent to this viewController and shows the image that the user selected, not the first image in the array, and then is able to swipe right or left. I'm trying to recreate how it would be only by the latter view controller logic.
So you will receive the index when this VC is created. Lets call it selectedIndex
var selectedIndex = 0 //I give 0 as initial value but it will be updated anyways.
Then in viewDidAppear try with following:
let width = view.frame.width
scrollView.setContentOffset(CGPoint(x: width * selectedIndex, y: 0), animated: false)
So when a user taps to an image you will receive the index number of that image in your VC, then setContentOffset of scrollView to width of the screen times selectedIndex, this should give you the result you want.

Creating swipe gesture in swift to scroll through image stack

I'm a swift newbie. Any help is appreciated.
I have a stack of 10 images. I want the user to be able to swipe up and down to update the the image view to new images.
For example: the Image View starts of by displaying 1.jpg. Then the user gestures down on the Image View and it updates the image to 2.jpg then 3.jpg... etc. depending on how far the gesture is held (kind of like an animation). I am able to update the image by one increment using the following code:
#IBAction func down(sender: UISwipeGestureRecognizer) {
image.image = UIImage(named: "2.jpg")
}
How can I make this a continuous gesture?
The end result should look something like this: https://figure1.com/sections/blog/wp-content/uploads/2014/08/resized.gif
So let's start off first things first.
Based off the idea that this is distance dragged
1) We need a UIScrollView()
2) Set the Scrollview Frame to the frame of the screen
3) Allow use of scrollview delegate, so add UIScrollViewDelegate to the list of subclasses (At the top where it says ClassName: list, of, subclasses, here, separated, by, commas)
4) In your class (If you are in your View Controller class), add the delegate to the scrollview
5) Let's set the contentSize of the scrollView
6) Let's add the images to your view, using a loop
7) Before step 6, make sure that your images are named something with numbers, and the same name, such as Image1 Image2 Image3 so that we can loop through them.
8) Declare a UIImage array
9) I was gonna keep typing steps but here you go :P
class ViewController:UIViewController, UIScrollViewDelegate {
let scrollView = UIScrollView()
let images:[UIImage] = []
let imageView:UIImageView!
override func viewDidLoad() {
//Setting up stuff
for index in 0 ..< 10 {
//It's either %t, %i or %d (I don't recall which is which lol)
images.append(UIImage(named: String(format: "Image %i", index)))
}
imageView = UIImageView(frame: view.bounds)
imageView.contentMode = .ScaleAspectFit
imageView.image = images[0]
imageView.backgroundColor = UIColor.clearColor()
self.view.addSubview(imageView)
scrollView.frame = view.bounds
scrollView.delegate = self
scrollView.backGroundColor = UIColor.clearColor()
//Let's say every time the user drags to 1/10 of the screen, the picture will change
scrollView.contentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height+(self.view.frame*(1/10)*CGFloat(images.count)))
self.view.addSubview(scrollView)
}
//Now let's add some UIScrollView delegates to be called whenever the user scrolls!
func scrollViewDidScroll(scrollView: UIScrollView) {
if(scrollView.contentOffSet.y > self.view.frame.height) {
//(1/10) because we are changing the scrollView every 1/10 of the screen
let pictureCount = scrollView.contentOffset.y/scrollView.frame.height*(1/10)
imageView.image = images[pictureCount]
}
}
}
Don't quote me off this, mainly because I pulled it out of the air, so if it has errors/bugs, I apologize, but feel free to comment and I will try to adjust what is needed.

Collapse a view on scroll up (iOS)

I have a ViewController, which has a tableView, and has two views on top of the table view. I need to collapse a view (outlined in red in the image), when the user scrolls up, and move another view (orange button), up with the table. I'm planning on using the scrollViewDidScroll delegate method to detect the offset of the table, and handle the collapsing with that.
How do I go about implementing this?
//Custom delegate method, which returns offset of scrollViewDidScroll
func didScrollScrollView(offset: CGFloat){
print(offset)
//I have the offset, which is -30 on load. How do I implement collapsing of the view highlighted in red?
}
you need to access the frame of the view or better its heightConstraint.
If you have a reference to the heightConstrain you can do:
var previousOffset: CGFloat = -30
func didScrollScrollView(offset: CGFloat){
let diff = previousOffset - offset
previousOffset = offset
var newHeight = heightConstraint.constant + diff
if newHeight < 0 {
newHeight = 0
} else if newHeight > 60 { // or whatever
newHeight = 60
}
heightConstraint.constant = newHeight
}
If you don't use Autolayout you need to update the frames manualy.
Also set clipsSubviews=true (clippedToBounds) in interface builder or in code

Swift: Updating UIPageControl from ScrollView

I seem to be having some issues getting the UIPageControl to work.
I have a ViewController that holds a ScrollView. This ScrollView loads nib files that can be swiped. See image:
Here is the code that loads these:
self.addChildViewController(vc0)
self.displayReportView.addSubview(vc0.view)
vc0.didMoveToParentViewController(self)
var frame1 = vc1.view.frame
frame1.origin.x = self.view.frame.size.width
vc1.view.frame = frame1
self.addChildViewController(vc1)
self.displayReportView.addSubview(vc1.view)
vc1.didMoveToParentViewController(self)
// And so on
...
This works fine as in they scroll correctly etc..
Now, on the ViewController (one holding the scrollview) I added the delegate:
UIScrollViewDelegate
created some variables:
var frame: CGRect = CGRectMake(0, 0, 0, 0)
var colors:[UIColor] = [UIColor.redColor(), UIColor.blueColor(), UIColor.greenColor(), UIColor.yellowColor()]
var pageControl : UIPageControl = UIPageControl(frame: CGRectMake(50, 300, 200, 20))
I added some functions that are needed:
func configurePageControl() {
// The total number of pages that are available is based on how many available colors we have.
self.pageControl.numberOfPages = 4
self.pageControl.currentPage = 0
self.pageControl.tintColor = UIColor.redColor()
self.pageControl.pageIndicatorTintColor = UIColor.blackColor()
self.pageControl.currentPageIndicatorTintColor = UIColor.greenColor()
self.view.addSubview(pageControl)
}
// MARK : TO CHANGE WHILE CLICKING ON PAGE CONTROL
func changePage(sender: AnyObject) -> () {
let x = CGFloat(pageControl.currentPage) * displayReportView.frame.size.width
displayReportView.setContentOffset(CGPointMake(x, 0), animated: true)
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
}
Now, When I run the app the scrollview dots show, but when I swipe they do not update.
Question
How do I update the dots to reflect what view is showing?
let me know if you need anything else from my code to see functionality.
You can certainly do what you're describing, if you have a paging scroll view; I have an example of it that uses this code:
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let x = scrollView.contentOffset.x
let w = scrollView.bounds.size.width
pageControl.currentPage = Int(x/w)
}
Except for your round, that looks a lot your code, which makes me think that your code should work. That makes me think that something else is just misconfigured. Is this a paging scroll view? Did you remember to make this object your scroll view's delegate? Use logging or a breakpoint to be certain that your scrollViewDidEndDecelerating is even being called in the first place.
However, I would just like to point out that the configuration you are describing is effectively what UIPageViewController gives you for free — a scroll view with view controller views, plus a page control — so you might want to use that instead.
I would replace the scroll view with a UICollectionView. This way you get paging for free, and it will be better, because paging will work out of the box, without you having to calculate the frame offsets.
Be sure to set collectionView.pagingEnabled = true
To get the current page number, do collectionView.indexPathsForVisibleItems().first?.item
To change the page:
collectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: CenteredHorizontally, animated: true)

Resources