Swift CollectionView set where to scroll - ios

So I have a collection view horizontal and I want when the user scroll to set the scroll where I want.
#IBOutlet var joke_cards: UICollectionView!
extension ViewController: UIScrollViewDelegate{
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
joke_cards.contentOffset.x = scrollView.contentOffset.x + 1000
joke_cards.reloadData()
}
}
But this doesn't work, it scrolls normally, I want to specify how much to scroll, any suggestions?
So I think I need to be a bit more clear, what I want is to flip through some cards horizontally thats why I need when the user stars to swipe to show the next cell in the middle

If you want to manually scroll the user to a certain area you need first to define the area you need to scroll into view. This will depend a little bit on where exactly you are trying to scroll, but if the goal is just to scroll 1000 points to the right you can define the rect and scrolling like so:
let destinationRect = CGRect(x: scrollView.contentOffset.x + 1000, y: scrollView.contentOffset.y, width: 1, height: 1)
scrollView.scrollRectToVisible(destinationRect, animated: true)
Please note that scrolling will stop as soon as any part of the rect is visible, so if you want contentOffset.x + 1000 to be in the center you will need to do some more math to create the destinationRect.
The other option, since you are using a UICollectionView is to figure out which cell is at the point you want to scroll to, and scroll that cell to a certain position. In this example I safely unwrap the optional indexPath at the point you specified, and scroll that cell to be centered horizontally in the collectionView:
if let indexPath = self.joke_cards.indexPathForItem(at: CGPoint(x: scrollView.contentOffset.x, y: scrollView.frame.midY)) {
self.joke_cards.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}

Related

UIScrollView change contentInset while scrolling

I’m trying to implement a custom top bar that behaves similarly to the iOS 11+ large title navigation bar, where the large title section of the bar collapses when scrolling down the content:
The difference is that my bar needs a custom height and also a bottom section that doesn’t collapse when scrolled. I managed to get that part working:
The bar is implemented using a UIStackView & with some non-required layout constraints, but I believe its internal implementation is not relevant. The most important thing is that the height of the bar is tied to scrollview's top contentInset. These are driven by scrollview's contentOffset in UIScrollViewDelegate.scrollViewDidScroll method:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let topInset = (-scrollView.contentOffset.y).limitedBy(topBarHeightRange)
// changes both contentInset and scrollIndicatorInsets
adjustTopContentInset(topInset)
// changes top bar height
heightConstraint?.constant = topInset
adjustSmallTitleAlpha()
}
topBarHeightRange stores the minimum and maximum bar height
One thing that I'm having a problem with is that when the user stops scrolling the scrollview, it's possible that the bar will end up in a semi-collapsed state. Again, let's look at the desired behavior:
Content offset is snapped to either the compact or expanded height, whichever is "closer". I'm trying to achieve the same in UIScrollViewDelegate.scrollViewWillEndDragging method:
func scrollViewWillEndDragging(_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let targetY = targetContentOffset.pointee.y
// snaps to a "closer" value
let snappedTargetY = targetY.snappedTo([topBarHeightRange.lowerBound, topBarHeightRange.upperBound].map(-))
targetContentOffset.pointee.y = snappedTargetY
print("Snapped: \(targetY) -> \(snappedTargetY)")
}
The effect is far from perfect:
When I look at the printout it shows that the targetContentOffset is modified correctly. However, visually in the app the content offset is snapped only to the compact height but not to the expanded height (you can observe that the large "Title" label ends up being cut in half instead of back to the "expanded" position.
I suspect this issue has something to do with changing the contentInset.top while the user is scrolling, but I can't figure out how to fix this behavior.
It's a bit hard to explain the problem, so I hope the GIFs help. Here's the repo: https://github.com/AleksanderMaj/ScrollView
Any ideas how to make the scrollview/bar combo snap to compact/expanded height properly?
I took a look at your project and liked your implementation.
I came up with a solution in your scrollViewWillEndDragging method by adding the following code at the end of method:
if abs(targetY) < abs(snappedTargetY) {
scrollView.setContentOffset(CGPoint(x: 0, y: snappedTargetY), animated: true)
}
Basically, if the scroll down amount is not worth hiding the large title (it happens if targetY is less than snappedTargetY) then just scroll to value of snappedTargetY to show the large title back.
Seems to be working for now, but let me know if you encounter any bugs or find a way to improve.
Whole scrollViewWillEndDragging method:
func scrollViewWillEndDragging(_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let targetY = targetContentOffset.pointee.y
// snaps to a "closer" value
let snappedTargetY = targetY.snappedTo([topBarHeightRange.lowerBound, topBarHeightRange.upperBound].map(-))
targetContentOffset.pointee.y = snappedTargetY
if abs(targetY) < abs(snappedTargetY) {
scrollView.setContentOffset(CGPoint(x: 0, y: snappedTargetY), animated: true)
}
print("Snapped: \(targetY) -> \(snappedTargetY)")
}

Peek previous/next cells in UICollectionView with native paging enabled?

I want a collection view to page through cells and centered, but display a portion of the previous and next cells like this:
There are tons of hacks out there, but I'd like to achieve this with the native paging property of the UICollectionView. Making the cell the full width of the collection view doesn't show previous/next cells, and making the cell width smaller doesn't snap to center when paging.
Is is possible to make the collection view 80% of the screen width for example, and let the previous/next cells bleed outside the bounds (no clip to bounds)?
Or any other ideas to achieve this using the native paging?
iOS Swift 4
Use the below two methods to meek the previous and next screens.
private func calculateSectionInset() -> CGFloat {
let deviceIsIpad = UIDevice.current.userInterfaceIdiom == .pad
let deviceOrientationIsLandscape = UIDevice.current.orientation.isLandscape
let cellBodyViewIsExpended = deviceIsIpad || deviceOrientationIsLandscape
let cellBodyWidth: CGFloat = 236 + (cellBodyViewIsExpended ? 174 : 0)
let buttonWidth: CGFloat = 50
let inset = (collectionFlowLayout.collectionView!.frame.width - cellBodyWidth + buttonWidth) / 4
return inset
}
private func configureCollectionViewLayoutItemSize() {
let inset: CGFloat = calculateSectionInset() // This inset calculation is some magic so the next and the previous cells will peek from the sides. Don't worry about it
collectionFlowLayout.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
collectionFlowLayout.itemSize = CGSize(width: collectionFlowLayout.collectionView!.frame.size.width - inset * 2, height: collectionFlowLayout.collectionView!.frame.size.height)
}
Don't forget to invoke configureCollectionViewLayoutItemSize() method in viewDidLayoutSubviews() of your UIViewController.
For more detailed reference Click Here
I don't think there's an easy way to do this with the native paging enabled.
But you can easily do a custom paging by utilising scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) to set the destination you want. By adding some logic to that you can create the pagination.
You can find examples here and here

Swift: how to deal with tableView when tableView width is wider than screen size?

The scene I need to implement the attached picture.
When I try to implement the bottom form, I meet some problems.
The bottom form has following requirements:
top 2 lines should stick at the head, and not allow to scroll up/down. I use table section head to implement.
all the other rows could scroll up/down and clickable. I use a table view to implement
since the app support bigger text, when text is bigger, the screen size could not display all the tableView, so in this case, the whole tableview should support horizontally scroll.
when tableview row data is different from the "currentBitting", which has a value of "33342431", the individual label should show background colour.
each column should be aligned for the table
Following is the idea how I implement:
To achieve all these requirements, I use tableView to implement. Each character in the array item use label to display.
I use the max width and regular width to define the width of each column
When text font size changes, I try to set the bottom tableView
contentSize = current new content size in "viewWillLayoutSubViews()"
I add a background colour for the section head, otherwise, when bottom tableview scroll up, it will overlap the section head and offscreen text background colour does not render. So I add section head background colour to avoid this problem.
Currently, the problem is the horizontal scroll does not work. Very strange.
To be honest, I tried quite a lot of ways:
bottom form first uses a parent scrollView, then put a sub tableView. Problem: top header is hard to stick or vertically scroll not that smooth. I pasted my scrollViewDidScroll part. It works. But vertically scroll not that smooth. If anyone could help to improve it, I will be very appreciated.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let recognizer = scrollView.panGestureRecognizer
let distance = recognizer.translation(in: scrollView)
let deltaHeight:CGFloat = parentScrollView.contentSize.height - parentScrollView.frame.height
let goingUp = distance.y < 0 //going up
if distance.y != 0 {
if goingUp {
parentScrollView.contentOffset.y = 0
switch recognizer.state {
case .began,.changed:
if bottomTableview.contentOffset.y < deltaHeight {
bottomTableview.contentOffset.y -= distance.y
recognizer.setTranslation(.zero, in: scrollView)
}
case .ended:
if bottomTableview.contentOffset.y > deltaHeight{
bottomTableview.contentOffset.y = deltaHeight
UIView.animate(withDuration: 0.1, animations: {
scrollView.layoutIfNeeded()
})
}
default:
break
}
}
else{
switch recognizer.state {
case .began,.changed:
if bottomTableview.contentOffset.y > 0 {
bottomTableview.contentOffset.y -= distance.y
recognizer.setTranslation(.zero, in: scrollView)
}
case .ended:
if bottomTableview.contentOffset.y < 0 {
bottomTableview.contentOffset.y = 0
UIView.animate(withDuration: 0.1, animations: {
scrollView.layoutIfNeeded()
})
}
default:
break
}
}
}
}
collection view to implementing. But I don't know how to select a whole line to get the data and how to stick the top header
Does anyone have some better idea to implement this case? Very appreciated for your help.
Data to use in the table view is:
let extensionCodeArray = ["Position", "Current Bitting", "11407", "10253", "10492", "14195", "11665", "10547", "12332", "12536", "10211", "11034", "10725", "11036", "12928"]
let extensionResultBitting = ["12345678", "33342431", "33133244", "33243134", "32343134", "33134243", "31343234", "33342134", "31344233", "34342133", "31334342", "33134342", "34343132", "34231343", "32431343"]
I solved the problem. The idea is below:
define a collection view, set its numberOfItemsInSection = 1
for the collectionView, set its
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize --> return CGSize(width: contentSize.width, height: collectionView.frame.size.height)
The cotentSize is calculated in advance for the final form
Customerize the UICollectionViewCell, inside which implementing the tableView data source and delegate. All the datasource for the tableView get from a protocol defined in the UICollectionViewCell. Outerside collectionView is the delegate for the protocol
Done.
For first solution you tried.
UITableView is a subclass of UIScrollView, which means UITableView also have scrollViewDidScroll(_ scrollView: UIScrollView) event.
To solve this, you could simply do the following. Set a tag to tableView or scrollView to identify it.
1. For example, to identify the scrollView, add the following line after where you initialized the scrollView.
scrollView.tag = 101
In
func scrollViewDidScroll(_ scrollView: UIScrollView)
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
if scrollView.tag == 101 {
// Triggered by scrollView
} else {
// Triggered by tableView
}
}

Determine Destination IndexPath when scrollView flicks (ends dragging)

So, I have a infinite scrolling tableView of same size cells. Each cell has just a single image and what I'm trying to achieve is that as soon as a scroll flick happens, I'm trying to determine at what indexPath it will arrive at and preload Images for that index only.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let contentOffset = CGPoint(x: targetContentOffset.pointee.x, y: targetContentOffset.pointee.y)
let totalRowsVisible = Int(tableView.bounds.height / rowHeight) + 1
let rowNumber = tableView.indexPathForRow(at: contentOffset)?.row ?? Int((contentOffset.y + offSetParameter) / rowHeight)
}
Right now this is what I'm using to determine the indexPath of tableview as soon as dragging ends but this is giving me incorrect results many times (especially when I scroll really fast).
Also when I add more rows to the tableView (at the bottom), the contentOffset changes weirdly and targetContentOffset Parameter is loaded somewhere else.
I have not found any solution anywhere.
EDIT: So, in the comments people have asked my specific requirement. The requirement exactly is as stated, that whenever a scroll begins and dragging ends, I have to determine what destination the scroll will end at and begin fetching content for that row before reaching the destination. So no matter how fast or slow the scroll occurs I just download images for the indexPath

correct scrollViewWillEndDragging code?

How do I get the UIScrollView to stop dragging at the end of the view, and snap it into a correct position? I've attempted using the following code, but while it correctly decelerates, and snaps into the perfect position at the top of the view, it does not allow me to scroll down:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let layout = self.collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
let itemHeightIncludingSpacing = self.view.frame.height + layout.minimumInteritemSpacing
var offset = targetContentOffset.memory
let index = (offset.x + scrollView.contentInset.bottom) / itemHeightIncludingSpacing
let roundedIndex = round(index)
offset = CGPoint(x: roundedIndex * itemHeightIncludingSpacing - scrollView.contentInset.bottom, y: -scrollView.contentInset.top)
targetContentOffset.memory = offset
}
here is the top of my scroll view:
here is the the bottom, its not in the position its supposed to be as you can see. it should be flush with the bottom offset value just like the top:
how do i get the scroll to stop flush just like the top?
Did you make an extension of UIScrollView that has that method? It doesn't exist but scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) does. If this is your custom method in the extension, make sure it is called somewhere in your code (like the delegate method I mentioned). Also, resetting the parameter is doing nothing to the scrollView. I would suggest a different approach.
I'm not sure what you're asking here either. Are you not wanting the content to be pulled past the edge of the screen before it bounces back in place?
If so, you don't need that method. Just add self.myScrollView.bounces = false to viewDidLoad()
Edit:
Ok try this
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, 0, 0, 0)
}

Resources