How to programmatically scroll through a collection view? - ios

I have a collection view in my view. In the cells are image views and it has one section.
I now want to scroll programmatically through the images. I want the animation to happen endless so it starts with item one after reaching item 10.
It also would be helpful if you provide some method where to put animations like the cells getting bigger after getting programmatically swiped in from right.

There are two methods that could help you achieve this:
func scrollToItemAtIndexPath(indexPath: NSIndexPath,
atScrollPosition scrollPosition: UICollectionViewScrollPosition,
animated animated: Bool)
or
func setContentOffset(contentOffset: CGPoint,
animated animated: Bool)
Both are on UICollectionView so you can use whatever seems more convenient. To create a custom animation for this however is more difficult. A quick solution depending on what you need could be this answer.
Swift 4, iOS 11:
// UICollectionView method
func scrollToItem(at indexPath: IndexPath,
at scrollPosition: UICollectionViewScrollPosition,
animated: Bool)
// UIScrollView method
func setContentOffset(_ contentOffset: CGPoint, animated: Bool)

Swift 5
Based on Mr.Bean's answer, here is an elegant way using UICollectionView extension:
extension UICollectionView {
func scrollToNextItem() {
let contentOffset = CGFloat(floor(self.contentOffset.x + self.bounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func scrollToPreviousItem() {
let contentOffset = CGFloat(floor(self.contentOffset.x - self.bounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func moveToFrame(contentOffset : CGFloat) {
self.setContentOffset(CGPoint(x: contentOffset, y: self.contentOffset.y), animated: true)
}
}
Now you can use it wherever you want:
collectionView.scrollToNextItem()
collectionView.scrollToPreviousItem()

By far this is the best approach i have encountered, the trick is to scroll to the frame which is containing next objects. Please refer the code
/* -------------- display previous friends action ----------------*/
#IBAction func actionPreviousFriends(_ sender: Any) {
let collectionBounds = self.collectionView.bounds
let contentOffset = CGFloat(floor(self.collectionView.contentOffset.x - collectionBounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
/* -------------- display next friends action ----------------*/
#IBAction func actionNextFriends(_ sender: Any) {
let collectionBounds = self.collectionView.bounds
let contentOffset = CGFloat(floor(self.collectionView.contentOffset.x + collectionBounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func moveToFrame(contentOffset : CGFloat) {
let frame: CGRect = CGRect(x : contentOffset ,y : self.collectionView.contentOffset.y ,width : self.collectionView.frame.width,height : self.collectionView.frame.height)
self.collectionView.scrollRectToVisible(frame, animated: true)
}

You can use this UICollectionView extension (Swift 4.2)
extension UICollectionView {
func scrollToNextItem() {
let scrollOffset = CGFloat(floor(self.contentOffset.x + self.bounds.size.width))
self.scrollToFrame(scrollOffset: scrollOffset)
}
func scrollToPreviousItem() {
let scrollOffset = CGFloat(floor(self.contentOffset.x - self.bounds.size.width))
self.scrollToFrame(scrollOffset: scrollOffset)
}
func scrollToFrame(scrollOffset : CGFloat) {
guard scrollOffset <= self.contentSize.width - self.bounds.size.width else { return }
guard scrollOffset >= 0 else { return }
self.setContentOffset(CGPoint(x: scrollOffset, y: self.contentOffset.y), animated: true)
}
}
And the usage will be like
yourCollectionView.scrollToNextItem()
yourCollectionView.scrollToPreviousItem()

Related

UICollectionView jumps/scrolls when a nested UITextView begins editing

My first question on SO so bear with me. I have created a UICollectionViewController which has a header and 1 cell. Inside the cell is a tableview, inside the table view there are multiple static cells. One of those has a horizontal UICollectionView with cells which have UITextViews.
Problem: When tapping on a UITextView the collection view scrolls/jumps
Problem Illustration
On the right you can see the y offset values. On first tap it changes to 267 -- the header hight. On a consecutive tap it goes down to 400 -- the very bottom. This occurs no matter what I tried to do.
Note: Throughout my app I'am using IQKeyboardManager
What have I tried:
Disabling IQKeyboardManager completely and
Taping on text view
Replacing it with a custom keyboard management methods based on old SO answers
Set collectionView.shouldIgnoreScrollingAdjustment = true for:
all scrollable views in VC
Individuals scrollable views
Note: this property originates from the IQKeyboardManager Library and as far as I understand it is supposed to disable scroll adjustment offset.
Tried disabling scroll completely in viewDidLoad() as well as all other places within this VC. I used:
collectionView.isScrollEnabled = false
collectionView.alwaysBounceVertical = false
Notably, I have tried disabling scroll in text viewDidBeginEditing as well as the custom keyboard management methods.
My Code:
The main UICollectionView and its one cell are created in the storyboard. Everything else is done programatically. Here is the flow layout function that dictates the size of the one and only cell:
extension CardBuilderCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:
UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let height = view.frame.size.height
let width = view.frame.size.width
return CGSize(width: width * cellWidthScale, height: height * cellHeigthScale)
}
}
Additionally, collectionView.contentInsetAdjustmentBehavior = .never
The TableView within the subclass of that one cell is created like so:
let tableView: UITableView = {
let table = UITableView()
table.estimatedRowHeight = 300
table.rowHeight = UITableView.automaticDimension
return table
}()
and:
override func awakeFromNib() {
super.awakeFromNib()
dataProvider = DataProvider(delegate: delegate)
addSubview(tableView)
tableView.fillSuperview() // Anchors to 4 corners of superview
registerCells()
tableView.delegate = dataProvider
tableView.dataSource = dataProvider
}
The cells inside the table view are all subclasses of class GeneralTableViewCell, which contains the following methods which determine the cells height:
var cellHeightScale: CGFloat = 0.2 {
didSet {
setContraints()
}
}
private func setContraints() {
let screen = UIScreen.main.bounds.height
let heightConstraint = heightAnchor.constraint(equalToConstant: screen*cellHeightScale)
heightConstraint.priority = UILayoutPriority(999)
heightConstraint.isActive = true
}
The height of the nested cells (with TextView) residing in the table view is determined using the same method as the one and only cell in the main View.
Lastly the header is created using a custom FlowLayout:
class StretchyHeaderLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
layoutAttributes?.forEach({ (attribute) in
if attribute.representedElementKind == UICollectionView.elementKindSectionHeader && attribute.indexPath.section == 0 {
guard let collectionView = collectionView else { return }
attribute.zIndex = -1
let width = collectionView.frame.width
let contentOffsetY = collectionView.contentOffset.y
print(contentOffsetY)
if contentOffsetY > 0 { return }
let height = attribute.frame.height - contentOffsetY
attribute.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)
}
})
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
This is my first time designing a complex layout with mostly a programatic approach. Hence it is possible that I missed something obvious. However, despite browsing numerous old questions I was not able to find a solution. Any solutions or guidance is appreciated.
Edit:
As per request here are the custom keyboard methods:
In viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
Then:
var scrollOffset : CGFloat = 0
var distance : CGFloat = 0
var activeTextFeild: UITextView?
var safeArea: CGRect?
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
var safeArea = self.view.frame
safeArea.size.height += collectionView.contentOffset.y
safeArea.size.height -= keyboardSize.height + (UIScreen.main.bounds.height*0.04)
self.safeArea = safeArea
}
}
private func configureScrollView() {
if let activeField = activeTextFeild {
if safeArea!.contains(CGPoint(x: 0, y: activeField.frame.maxY)) {
print("No need to Scroll")
return
} else {
distance = activeField.frame.maxY - safeArea!.size.height
scrollOffset = collectionView.contentOffset.y
self.collectionView.setContentOffset(CGPoint(x: 0, y: scrollOffset + distance), animated: true)
}
}
// prevent scrolling while typing
collectionView.isScrollEnabled = false
collectionView.alwaysBounceVertical = false
}
#objc func keyboardWillHide(notification: NSNotification) {
if distance == 0 {
return
}
// return to origin scrollOffset
self.collectionView.setContentOffset(CGPoint(x: 0, y: scrollOffset), animated: true)
scrollOffset = 0
distance = 0
collectionView.isScrollEnabled = true
}
Finaly:
//MARK: - TextViewDelegate
extension CardBuilderCollectionViewController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
self.activeTextFeild = textView
configureScrollView()
}
}
The problem that I can see is you calling configureScrollView() when your textView is focused in textViewDidBeginEditing .
distance = activeField.frame.maxY - safeArea!.size.height
scrollOffset = collectionView.contentOffset.y
self.collectionView.setContentOffset(CGPoint(x: 0, y: scrollOffset + distance), animated: true)
You're calling collectionView.setContentOffset --> so that's why your collection view jumping.
Please check your distance calculated correctly or not. Also, your safeArea was modified when keyboardWillShow.
Try to disable setCOntentOffset?

How to go to next view controller if i am at last page of uicollection view. its horizontal transition and paging is enable

#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.isPagingEnabled = true
}
#IBAction func onBackButtonTap(_ sender: Any) {
// if collection view is on first page i have to pop
// self.navigationController?.popViewController(animated: true)
let collectionBounds = self.collectionView.bounds
let contentOffset = CGFloat(floor(self.collectionView.contentOffset.x - collectionBounds.size.width))
self.moveCollectionToFrame(contentOffset: contentOffset)
}
#IBAction func onNextButtonTap(_ sender: Any) {
// if collection view is on last page i have to push to another view controller
let collectionBounds = self.collectionView.bounds
let contentOffset = CGFloat(floor(self.collectionView.contentOffset.x + collectionBounds.size.width))
self.moveCollectionToFrame(contentOffset: contentOffset)
}
func moveCollectionToFrame(contentOffset : CGFloat) {
let frame: CGRect = CGRect(x : contentOffset ,y : self.collectionView.contentOffset.y ,width : self.collectionView.frame.width,height : self.collectionView.frame.height)
self.collectionView.scrollRectToVisible(frame, animated: true)
}
How to go to next view controller if I am at last page of UICollectionView? Its horizontal transition and paging is enabled.
You can check for the current visible index and check if it is the last or first cell. And based on that you can take your action of pushing or popping the view controller
var currentIndexPath:IndexPath = collectionView.indexPathsForVisibleItems.last!
if currentIndexPath.row == yourArray.count - 1 {
print("last row, we'll push")
}
if currentIndexPath.row == 0 {
print("first row, we'll pop")
}

When I Scroll the tableView, how can I know which cell is touch the Green Area

The Screen In My Prototype
My question is based onThe image in the link . Because my reputation is not enough, I can't post any image here
We assume that Green Area in the image is fixed.
And, my requirement is that When a cell contains the GA, that cell'saudioPlayer will speak the word in the cell, like AirPod
OR, you can regard my requirement as When a cell contains the GA, the text of that cell's label changes to "Touch the Green"
My question is that when I Scroll the tableView, how can I get which one(Cell) is containing the GA?
But I can’t find a way to get that(some position/index information about That Cell)
could anyone help me ? ObjectiveC solution is OK, Swift solution is better for me, Thank you so much
In this code, I am using GreenArea as in Center of UIView. Some modification from Ruslan's Answer.
#IBOutlet weak var greenAreaVw: UIView!
var contHeight : CGFloat = 0.0
var eachRowHeight : CGFloat = 45
var topSpaceTableView : CGFloat = 62
var GreenAreaOriginY : CGFloat = 0.0
// Give UITableView Edge Insets in ViewDidLoad
contHeight = ((self.view.frame.size.height / 2) - eachRowHeight / 2 - topSpaceTableView)
userTblVw.contentInset = UIEdgeInsets(top: contHeight, left: 0, bottom: contHeight, right: 0)
userTblVw.contentOffset.y = -contHeight
GreenAreaOriginY = greenAreaVw.frame.origin.y
/*------------------- -----------------------*/
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
checkCells()
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
checkCells()
}
func checkCells() {
userTblVw.visibleCells.forEach { cell in
if let indexPath = userTblVw.indexPathForCell(cell) {
let rect = userTblVw.rectForRowAtIndexPath(indexPath)
let convertedRect = self.userTblVw.convertRect(rect, toView: self.view)
if convertedRect.origin.y >= GreenAreaOriginY && convertedRect.origin.y < (GreenAreaOriginY + eachRowHeight)
{
let contFloat : CGFloat = (eachRowHeight * CGFloat(indexPath.row)) - contHeight
userTblVw.setContentOffset(CGPoint(x: 0, y: contFloat), animated: true)
}
}
}
}
Find below Screenshots:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// We check cells here to set the state of whether it contains the green or not before the scrolling
checkCells()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// And we are continuously checking cells while scrolling
checkCells()
}
func checkCells() {
tableView.visibleCells.forEach { cell in
if let indexPath = tableView.indexPath(for: cell) {
let rect = tableView.rectForRow(at: indexPath)
// This is the rect in your VC's coordinate system (and not the table view's one)
let convertedRect = self.view.convert(rect, from: tableView)
if convertedRect.contains(greenArea.frame) {
cell.textLabel?.text = "Touch the Green"
} else {
cell.textLabel?.text = "Does not touch the Green"
}
}
}
}
How about something like:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
[0, 1, 2].forEach {
let rect = tableView.rectForRow(at: IndexPath(row: $0, section: 0))
if rect.contain(GAView.frame) {
// play sound here
}
}
}

Swift - Use button to manually scroll UITextView

I want to use a button to scroll down or up in UITextView,
but I ran into a problem.
Using textView.setContentOffset(CGPoint(x: 0, y: MyTextView.contentOffset.y + 100), animated: true) can make my textview scroll down, but it still scrolls when I click the button in the end of text...
like this..
e
f
g
===================
But I want is this..
a
b
c
d
e
f
g
===================
My code is
#IBAction func down(_ sender: UIButton) {
MyTextView.setContentOffset(CGPoint(x: 0, y: MyTextView.contentOffset.y - 100), animated: true)
}
#IBAction func up(_ sender: UIButton) {
MyTextView.setContentOffset(CGPoint(x: 0, y: MyTextView.contentOffset.y - 100), animated: true)
}
please help me!!!
Try this one
#IBAction func down(_ sender: UIButton) {
if (textView.contentSize.height>(textView.frame.size.height+textView.contentOffset.y+100)){
textView.setContentOffset(CGPoint(x:0,y:textView.contentOffset.y + 100), animated: true);
}
else{
textView.setContentOffset(CGPoint(x:0,y:(textView.contentSize.height - textView.frame.size.height)), animated: true)
}
}
I have tested and it works. Check this out
#IBOutlet weak var textView: UITextView!
#IBAction func up(_ sender: UIButton) {
if textView.contentOffset.y < 100{
textView.setContentOffset(CGPoint.zero, animated: true)
}else{
textView.setContentOffset(CGPoint(x: 0, y: textView.contentOffset.y-100), animated: true)
}
}
#IBAction func down(_ sender: UIButton) {
if textView.contentOffset.y > textView.contentSize.width - textView.frame.size.height{
textView.setContentOffset(CGPoint(x: 0, y: textView.contentSize.height-textView.frame.size.height), animated: true)
}else{
textView.setContentOffset(CGPoint(x: 0, y: textView.contentOffset.y+100), animated: true)
}
}
You probably want to check what the current offset is and only move the amount you need to. For example for scrolling down, try something like below. Then you should be able to adjust it and do the opposite for scrolling up.
struct Constants {
static let preferredScrollAmount: CGFloat = 100.0
}
#IBAction func downButtonTapped(_ sender: UIButton) {
// Get the current offset, content height, text view height and work out the scrollable distance for the text view
let currentOffset: CGFloat = textView.contentOffset.y
let contentHeight: CGFloat = textView.contentSize.height
let textViewHeight: CGFloat = textView.frame.size.height
let scrollableDistance = contentHeight - textViewHeight
// Check that the current offset isn't beyond the scrollable area otherwise return (no need to scroll)
guard currentOffset < scrollableDistance else { return }
// Work out how far we can move
let distanceWeCanMove = scrollableDistance - currentOffset
// Get the distance we should move (the smaller value so it doesn't go past the end)
let distanceToScroll = min(distanceWeCanMove, Constants.preferredScrollAmount)
// Do the scrolling
textView.setContentOffset(CGPoint(x: 0, y: currentOffset + distanceToScroll), animated: true)
}

Get scroll position of UIPageViewController

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

Resources