Swift: Updating UIPageControl from ScrollView - ios

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)

Related

Smooth custom paging for UIScrollView

I have two (possibly more) views in a UIScrollView and want to use paging with it. The problem arises when I try to use the default Paging option for UIScrollView, since the views have different widths it can not page properly.
So I have implemented a custom paging code which works. However, when the scrolls are slow, it does not function as expected. (It goes back to the original position without animation.)
Here is how I currently do the custom paging through the UIScrollViewDelegate
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if direction == 1{
targetContentOffset.pointee.x = 0
}else{
targetContentOffset.pointee.x = 100
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView.superview).x > 0 {
direction = 1
}
else {
direction = 0
}
}
What I want:
What I have:
try to below example for Custom UIScrollView Class
import UIKit
public class BaseScrollViewController: UIViewController, UIScrollViewDelegate {
public var leftVc: UIViewController!
public var middleVc: UIViewController!
public var rightVc: UIViewController!
public var initialContentOffset = CGPoint() // scrollView initial offset
public var maximumWidthFirstView : CGFloat = 0
public var scrollView: UIScrollView!
public class func containerViewWith(_ leftVC: UIViewController,
middleVC: UIViewController,
rightVC: UIViewController) -> BaseScrollViewViewController {
let container = BaseScrollViewViewController()
container.leftVc = leftVC
container.middleVc = middleVC
container.rightVc = rightVC
return container
}
override public func viewDidLoad() {
super.viewDidLoad()
setupHorizontalScrollView()
}
func setupHorizontalScrollView() {
scrollView = UIScrollView()
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.bounces = false
let view = (
x: self.view.bounds.origin.x,
y: self.view.bounds.origin.y,
width: self.view.bounds.width,
height: self.view.bounds.height
)
scrollView.frame = CGRect(x: view.x,
y: view.y,
width: view.width,
height: view.height
)
self.view.addSubview(scrollView)
let scrollWidth = 3 * view.width
let scrollHeight = view.height
scrollView.contentSize = CGSize(width: scrollWidth, height: scrollHeight)
leftVc.view.frame = CGRect(x: 0,
y: 0,
width: view.width,
height: view.height
)
middleVc.view.frame = CGRect(x: view.width,
y: 0,
width: view.width,
height: view.height
)
rightVc.view.frame = CGRect(x: 2 * view.width,
y: 0,
width: view.width,
height: view.height
)
addChildViewController(leftVc)
addChildViewController(middleVc)
addChildViewController(rightVc)
scrollView.addSubview(leftVc.view)
scrollView.addSubview(middleVc.view)
scrollView.addSubview(rightVc.view)
leftVc.didMove(toParentViewController: self)
middleVc.didMove(toParentViewController: self)
rightVc.didMove(toParentViewController: self)
scrollView.contentOffset.x = middleVc.view.frame.origin.x
scrollView.delegate = self
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.initialContentOffset = scrollView.contentOffset
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if maximumWidthFirstView != 0
{
if scrollView.contentOffset.x < maximumWidthFirstView
{
scrollView.isScrollEnabled = false
let newOffset = CGPoint(x: maximumWidthFirstView, y: self.initialContentOffset.y)
self.scrollView!.setContentOffset(newOffset, animated: false)
scrollView.isScrollEnabled = true
}
}
}
}
Use of BaseScrollViewController
let left = FirstController.init()
let middle = MiddleController()
let right = RightController.init()
let container = BaseScrollViewController.containerViewWith(left,middleVC: middle,rightVC: right)
container.maximumWidthFirstView = 150
Output:
GitHub gist Example code: https://gist.github.com/mspvirajpatel/58dac2fae0d3b4077a0cb6122def6570
I have previously written a short memo about this problem, and I'll copy/paste it since it is no longer accessible from anywhere. This may not be a specific answer and the codes are pretty old, but I hope this would help you in some degree.
If you have used a paging feature included in UIScrollView, you might also have tempted to customize the width of each page instead of a default, boring, frame width paging. It would be great if you can make the scroll stop at shorter or longer intervals than just multiples of its frame width. Surprisingly, there's no built-in way to configure the width of pages even in our latest iOS7 SDK. There are some ways to achieve custom paging, but none of them I would say are complete. As for now, you'll have to choose either of the following solutions.
1. Change the frame size of your UIScrollView
Alexander Repty has introduced a nice and easy solution to this problem and also included a sample code through his blog: http://blog.proculo.de/archives/180-Paging-enabled-UIScrollView-With-Previews.html
Basically, the instruction can be watered down to the following steps:
Create UIView subclass and override hitTest: withEvent:.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
if ([self.subviews count] == 0) return nil;
else return [self.subviews lastObject];
}
return nil;
}
Include UIScrollView as a subview of the above UIView subclass.
Adjust the frame size of your UIScrollView.
Set clipsToBound property of your scroll view to NO.
Set pagingEnabled property of your scroll view to YES.
As you can see, I've just assumed that there is only one subview (the scrollView!) to your UIView subclass. Since you are passing all the touch events occurred in the UIView subclass to your UIScrollView, you'll be able to scroll the content by panning on the UIView subclass, but the paging width will be decided by the width of UIScrollView's frame.
The best part of this approach is that you'll get the genuine feeling and responsiveness, as it is somewhat hard to mimic the paging by using UIScrollView delegate methods.
The only problem I found using this solution is that the width of all pages will have to be identical. You can't set different widths to different pages. If you tries to change your scrollView's frame size dynamically, you'll find there're a number of new emerging problems to deal with. Before trying to fix these glitches, you may want to check out other two solutions using UIScrollView delegates.
2. scrollViewWillEndDragging: withVelocity: targetContentOffset
scrollViewWillEndDragging: withVelocity: targetContentOffset is one of the latest UIScrollView delegate methods(iOS 5.0 or up) that gives you more information than the other old ones.
Since you get the velocity of the scrollView right after you lift the finger up from the screen, we can figure out the direction of the scrolled contents. The last argument, targetContentOffset, not only gives you the expected offset when the scrolling stops eventually, you can also assign CGPoint value in order to let the scrollView scrolls to the desired point.
targetContentOffset = CGPointMake(500, 0);
or
targetContentOffset->x = 500;
However, this will not work as you would think it should because you cannot set the speed of scrolling animation. It feels more like the scrollView happens to stop at the right point rather than it snaps to the spot. I also have to warn you that manually scrolling the contents with setContentOffset: animated: or just by using UIView animation inside the method will not work as expected.
If the velocity is 0, however, you may(and you have to) use manual scrolling to make it snap to the nearest paging point.
It could be the simplest and the most clean approach among all, but the major downside is that it does not provide the same experience that you always had with the real paging feature. To be more honest, it's not even similar to what we call paging. For the better result, we need to combine more delegate methods.
3. Use multiple UIScrollView delegate methods
From my shallow experience, an attempt to scroll your scrollView manually inside any UIScrollView delegate methods will only work when your scrollView has started to decelerate, or when it's not scrolling at all. Therefore, the best place I've found to perform the manual scrolling is scrollViewWillBeginDecelerating:.
Before looking inside the sample code, remember scrollViewEndDragging: withVelocity: targetContentOffset: method will always called prior to scrollViewWillBeginDecelerating:.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
_scrollVelocity = velocity.x;
if (_scrollVelocity == 0) {
// Find the nearest paging point and scroll.
}
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
if (_scrollVelocity < 0) {
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
scrollView.contentOffset = // Previous page offset
} completion:^(BOOL finished){}];
} else if (_scrollVelocity > 0) {
// Animate to the next page offset
}
}
_scrollVelocity is meant to be a global variable or a property, and I've assumed that you have your own ways to decide paging offsets for each page. Note that you'll have to handle the case of zero velocity inside the upper method because the latter method will not be called.
UIView animation with the duration 0.3 and the EaseOut curve option gave me the best result, but of course you should try other combinations to find what's the best for you.
This not the exact solution you might be looking for.
1) Check the offset of the scrollView when it reaches 0, You could show the VIEW you have above , You could animate while checking the scrollview movement so that it looks nice .But not completely
2) Now the VIEW is partially above your camera(you can decrease it alpha so that scrollview is still visible).
3) user can tap the view and you can show it completely.
You may want to consider calculating the most visible cell in your collection view after dragging ends and then programmatically scroll to – and center – that cell.
So something like:
First, implement the scrollViewDidEndDragging(_:willDecelerate:) method of your collection view's delegate. Then, in that method's body, determine which cell in collectionView.visibleCells is most visible by comparing each of their centers against your collection view's center. Once you find your collection view's most visible cell, scroll to it by calling scrollToItem(at:at:animated:).

Detect when y offset changes in scrollView with paging enabled

I would like to find out which page is visible to the user and also do a certain action when a change in the page is detected. To do this I am using the function scrollViewDidScroll. As I didn't get the result I wanted (nothing happened), I looked for answers on stackoverflow and found the following question which was answered: Detecting UIScrollView page change, to be more precise I used the answer by Michael Waterfall and converted the Objective-C code to swift code.
Below you will find the code I used to create the scrollView in my class HomeController: UIViewController, UIScrollViewDelegate {. Moreover the function by Michael Waterfall is declared and called in the function initiateScrollView(). However, the function doesn't print anything. Do you know where I am doing something wrong? I can't see where I made a mistake. Nothing happens when I scroll (e.g. from page 1 to page 2 (homeView to view2)). The function only prints before: 0 once and after that nothing happens.
Code
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
initiateScrollView()
}
func initiateScrollView() {
//create scrollView with paging enabled
let scrollView = UIScrollView(frame: view.bounds)
scrollView.isPagingEnabled = true
view.addSubview(scrollView)
//get page size
let pageSize = view.bounds.size
//individual views
let homeView = UIView()
homeView.backgroundColor = .green
let view2 = UIView()
view2.backgroundColor = .blue
let view3 = UIView()
view3.backgroundColor = .red
//array with individual views
let pagesViews = [homeView, view2, view3]
//amount of views
let numberOfPages = pagesViews.count
print(numberOfPages) //prints '3'
//add subviews (pages)
for (pageIndex, page) in pagesViews.enumerated(){
//add individual pages to scrollView
page.frame = CGRect(origin: CGPoint(x:0 , y: CGFloat(pageIndex) * pageSize.height), size: pageSize)
scrollView.addSubview(page)
}
func scrollViewDidScroll(scrollView: UIScrollView) {
var previousPage = 0
let fractionalPage: Float = Float(scrollView.contentOffset.y / pageSize.height)
let page = lround(Double(fractionalPage))
print("before:", page) //prints 'before: 0' once
if previousPage != page {
// Page has changed, do your thing!
// ...
// Finally, update previous page
print("before:", page)
previousPage = page
print("after:", page)
} //never prints anything
}
scrollViewDidScroll(scrollView: scrollView)
//define size of scrollView
scrollView.contentSize = CGSize(width: pageSize.width, height: pageSize.height * CGFloat(numberOfPages))
}
You forgot to set the viewController as the delegate for the scroll view, as following:
func initiateScrollView() {
//create scrollView with paging enabled
let scrollView = UIScrollView(frame: view.bounds)
scrollView.isPagingEnabled = true
scrollView.delegate = self
view.addSubview(scrollView)
(...) }
And I recommend that you use the scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) function, instead of scrollViewDidScroll(...)
This is because you haven't set the delegate property of your scrollview.
Add scrollView.delegate = self in your initiateScrollView method.

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.

How to show "pull-to-refresh" element at the bottom of the UITableView in Swift

I need to show UIRefreshControl at the bottom of the UITableView like in the following Objective-C libraries:
https://github.com/emenegro/bottom-pull-to-refresh
https://github.com/vlasov/CCBottomRefreshControl
but I'm using Swift and noticed some problems when using "Bridging-Header.h" file with these libraries.
What is the alternative and the easiest way to achieve such behavior?
Thanks in advance.
I got into the same problem in my project, and found this answer. You can add a UIActivityIndicatorView instance on the bottom of the table, initially hidden, and when it enters the if condition above, unhide it and start its animation.
You may need to change the table's bottom offset, add "1 cell" height to it while it's loading and place it back when it finishes inside the if condition as well.
In Swift 3:
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// Use this 'canLoadFromBottom' variable only if you want to load from bottom iff content > table size
let contentSize = scrollView.contentSize.height
let tableSize = scrollView.frame.size.height - scrollView.contentInset.top - scrollView.contentInset.bottom
let canLoadFromBottom = contentSize > tableSize
// Offset
let currentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
let difference = maximumOffset - currentOffset
// Difference threshold as you like. -120.0 means pulling the cell up 120 points
if canLoadFromBottom, difference <= -120.0 {
// Save the current bottom inset
let previousScrollViewBottomInset = scrollView.contentInset.bottom
// Add 50 points to bottom inset, avoiding it from laying over the refresh control.
scrollView.contentInset.bottom = previousScrollViewBottomInset + 50
// loadMoreData function call
loadMoreDataFunction(){ result in
// Reset the bottom inset to its original value
scrollView.contentInset.bottom = previousScrollViewBottomInset
}
}
}
var refreshControl: UIRefreshControl!
//Pull to refresh
self.refreshControl = UIRefreshControl()
self.refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh")
self.refreshControl.addTarget(self, action: "pullToRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.messageTableView.addSubview(refreshControl)
//Refresh the table view (pull to refresh)
func pullToRefresh(sender:AnyObject)
{
print("pull to refresh...")
}

UIScrollView and Page Control, Page Control Disrupting View

So, I know its been done many times before, but I am building a ImageViewer with a UIScrollView and a UIPageControl. I am building it in Swift. Yet I have come across a issue which I can't seem to diagnose. I have a interesting setup to minimize long pages of code by splitting a view into containers. One for a tableview, the other for paging images. Here is a image of the storyboard. http://imgur.com/ZqvNgjy So in the first container view, it is a simple UIViewController with a UIScrollView and a UIPageControl. I can confirm that the UIPageControl is not in the view hierarchy of the UIScrollView. Now here is the code I used to setup everything.
import UIKit
//Set Image Array
var imagelist: String[] = ["Home Image 1.png", "info.png"]
//Set Page Numbers
var numpages = imagelist.count
class HomeImageView: UIViewController, UIScrollViewDelegate {
//IBOutlets & Propertys
#IBOutlet var scrollView: UIScrollView
#IBOutlet var pageControl: UIPageControl
override func viewDidLoad() {
super.viewDidLoad()
//Setup Page Control
pageControl.numberOfPages = numpages
pageControl.currentPage = 0
//Setup Scroll View
scrollView.pagingEnabled = true;
scrollView.scrollEnabled = true
scrollView.showsHorizontalScrollIndicator = false;
scrollView.showsVerticalScrollIndicator = false;
scrollView.bounces = true;
scrollView.scrollsToTop = false
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * CGFloat(numpages), scrollView.frame.size.height)
}
override func viewWillAppear(animated: Bool) {
//Setup UIViews with Images
for var z = 0; z < numpages; z++ {
let mainFrame: CGRect = CGRectMake(scrollView.frame.size.width * CGFloat(z), 0, scrollView.frame.size.width, scrollView.frame.size.height)
var image: UIImageView = UIImageView(frame: mainFrame)
image.image = UIImage(named: imagelist[z])
image.clipsToBounds = true
image.contentMode = UIViewContentMode.ScaleAspectFit
scrollView.addSubview(image)
}
func scrollViewDidScroll(scrollview: UIScrollView) {
var pageSize: Float = Float(scrollView.bounds.size.width)
var page: Float = floorf((Float(scrollview.contentOffset.x) - pageSize / 2.0) / pageSize) + 1
// Bound page limits
if (page >= Float(numpages)) {
page = Float(numpages) - 1;
} else if (page < 0) {
page = 0;
}
pageControl.currentPage = Int(page)
println(pageControl.currentPage)
}
}
}
So with that out of the way, here is the problem. When I run it, it does not do exactly what I expected. The simulator starts like this http://imgur.com/RVlKxdf and I can move the view around outside the bounds of the image.. Yet when I change the view hierarchy so that UIPageControl is behind the UIScrollView (Basically Removing it). The problem disappears completely. Aligned and allowing movement only in the bounds like I want. I don't know why the UIScrollView would react to the UIPageControl when it is outside its view hierarchy. Thanks for any assistance.

Resources