I have a collectionView Cell that is of the same size of my CollectionView i.e. one Cell at a time is displayed on the screen and I want minimum separation of 10 between cells, the problem is when I scroll the cell the cells aren't properly fitting the whole screen, and the shifting of cell is increased after every scroll. (Check screenshots for better understanding)
I assume you have set pagingEnabled for the collection view. It inherits this property from UIScrollView (because UICollectionView is a subclass of UIScrollView).
The problem is that the collection view uses its own width (320 points in your post) as the width of a page. Each of your cells is the same width as the collection view, but then you have a 10 point “gutter” between the cells. This means that the distance from the left edge of cell 0 to the left edge of cell 1 is 320 + 10 = 330 points. So when you scroll to show cell 1, the collection view stops scrolling at offset 320 (its own width), but cell 1 actually starts at offset 330.
The easiest fix is probably to turn off pagingEnabled and implement paging yourself by overriding scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) in your collection view delegate, like this:
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { return }
let pageWidth = scrollView.bounds.size.width + flowLayout.minimumInteritemSpacing
let currentPageNumber = round(scrollView.contentOffset.x / pageWidth)
let maxPageNumber = CGFloat(collectionView?.numberOfItems(inSection: 0) ?? 0)
// Don't turn more than one more page when decelerating, and don't go beyond the first or last page.
var pageNumber = round(targetContentOffset.pointee.x / pageWidth)
pageNumber = max(0, currentPageNumber - 1, pageNumber)
pageNumber = min(maxPageNumber, currentPageNumber + 1, pageNumber)
targetContentOffset.pointee.x = pageNumber * pageWidth
}
You'll also want to set the item size to match the device screen size, and set the deceleration rate to fast:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout, let collectionView = collectionView else { return }
flowLayout.itemSize = collectionView.bounds.size
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
Result:
The reason is that you have not taken the minimum separation of 10 while setting width of the cell(320). Hence this 10 is getting accumulated each time to scroll.
So you have to subtract 10 out of 320 while setting the width, so the width should be 310 IMO.
Related
I have a horizontal collectionView. There will always be 2 cells side by side, they both have the same width and height. The cells plays videos and when scrolling I want to load the video for the next cell that is 70% on screen and stop the video for the cell that is less than that. I found several similar questions but everything seems to reference 1 cell.
1- Cells 59 and 60 are both 100% on screen
2- Scrolling left, 59 is still more than 70%, 60 is 100%, and 61 is less than 30% on screen
3- Still scrolling left, 59 is less than 30% on screen, 60 is 100%, and 61 is more than 70%
Anything that is is already visible as in cell 59 in the first two photos and 60 in all three photos, I have logic to prevent the cell from loading again even though cell.loadNewVideo() will run for them.
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = UIScreen.main.bounds.width / 2
return CGSize(width: width, height: 150)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.visibleCells.forEach { cell in
guard let cell = cell as? VideoCell else { continue }
guard let indexPath = collectionView.indexPath(for: cell) else { return }
guard let layoutAttributes = collectionView.layoutAttributesForItem(at: indexPath) else { return }
let cellFrameInSuperview = collectionView.convert(layoutAttributes.frame, to: collectionView.superview)
guard let superView = collectionView.superview else { return }
let convertedRect = collectionView.convert(cellFrameInSuperview, to: superView)
let intersect = collectionView.frame.intersection(convertedRect)
if intersect.width > (UIScreen.main.bounds.width / 2) * 0.7 {
cell.loadNewVideo()
} else {
cell.destroyOldVideo()
}
}
}
I also tried the same code scrollViewWillEndDragging in but it also didn't work:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
}
You are way overthinking this. Forget about layoutAttributes and all that fancy-pants stuff and just do what you said you wanted to do: find out how much of the cell is onscreen.
The cell is a view. The screen (represented, let's say, by the window) is a view. Intersect them! Look at the intersection of the cell's frame with the window (or whatever), in appropriately converted coordinates, to get the part of the cell that is onscreen, and compare its size with the cell's frame's size. That is your percentage. Now do anything you like that percentage.
In this example, "anything you like" is just to display the percentage.
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let cells = self.collectionView.visibleCells
for cell in cells {
let f = cell.frame
let w = self.view.window!
let rect = w.convert(f, from: cell.superview!)
let inter = rect.intersection(w.bounds)
let ratio = (inter.width * inter.height) / (f.width * f.height)
let rep = (String(Int(ratio * 100)) + "%")
let label = cell.contentView.subviews.first as! UILabel
label.text = rep
}
}
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 :)
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
I have a UICollectionViewController and I added two UIViews as subviews. One is the purple UIView and above it is another white UIView, with the blue collection view getting scrolled up behind both.
Below those UIViews, I made the collectionView.contentInset from the top 300 (that's the total size of two UIViews' height). What I'm trying to accomplish is to scroll the collection view along with the two UIViews above. It's almost similar to the solution on this thread (Move a view when scrolling in UITableView), except when I override scrollViewDidScroll, the whole frame gets scrolled up and cells go behind the two Views. All I want is to scroll up the UIViews, and then scroll through the collection views. I feel like this might involve nested scroll views.
This was how I overrode scrollViewDidScroll:
var rect = self.view.frame
rect.origin.y = -scrollView.contentOffset.y - 300
self.view.frame = rect
EDIT: I posted a video that demonstrates what I want to do per iOS Tumblr app: https://youtu.be/elfxtzoiHQo
I have achieved the same requirement through some basic steps as below.
//Declare the view which is going to be added as header view
let requiredView = UIView()
//Add your required view as subview of uicollectionview backgroundView view like as
collectionView.backgroundView = UIView()
collectionView.backgroundView?.addSubview(requiredView)
//After that control the frame of requiredHeaderView in scrollViewDidScroll delegate method like
func scrollViewDidScroll(scrollView: UIScrollView) {
let per:CGFloat = 60 //percentage of required view to move on while moving collection view
let deductValue = CGFloat(per / 100 * requiredView.frame.size.height)
let offset = (-(per/100)) * (scrollView.contentOffset.y)
let value = offset - deductValue
let rect = requiredView.frame
self.requiredView.frame = CGRectMake(rect.origin.x, value, rect.size.width, rect.size.height)
}
It sounds like what you want is a header.
you can specify a class or nib for the header with either of these:
self.collectionView.registerClass(_:, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier:)
registerNib(_:, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: )
you should also specify a reference height if you are using a flow layout: self.flowLayout.headerReferenceHeight = ...
then you can provide the header via your UICollectionViewController in: collectionView(_:, viewForSupplementaryElementOfKind:, at:) by checking for the section header kind.
Here is a decent tutorial on this for reference: https://www.raywenderlich.com/78551/beginning-ios-collection-views-swift-part-2
You have a library CSStickyHeaderFlowLayout
From the ReadMe:
UICollectionView replacement of UITableView. Do even more like Parallax Header, Sticky Section Header. Made for iOS 7.
Try this.
headerViewYConstraint is header view's top y constraint.
Stores the last contact offset.
var lastContentOffset: CGFloat = 0
override func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView != contentScrollView {
if scrollView.dragging || scrollView.decelerating {
let newOffset = scrollView.contentOffset.y
let headerViewHeight = headerView.frame.width
if headerViewHeight > 0 && scrollView.contentSize.height > view.frame.height + headerViewHeight{
var topOffset = newOffset == 0 ? 0.0 : (headerViewYConstraint.constant + lastContentOffset - newOffset)
topOffset = min(0, max(topOffset, -headerViewHeight))
if headerViewYConstraint.constant > topOffset || newOffset < headerViewHeight || lastDirectionalContentOffset - newOffset > cellHeight(){
headerViewYConstraint.constant = topOffset
}
} else {
headerViewYConstraint.constant = 0
}
lastContentOffset = newOffset
}
}
}
I have a UICollectionView, a button that creates a new cell in collection view. And I want UICollectionView to adjust it's size according to it's content size (when there are one or two cells then UICollectionView is short, if there are a lot of cell UICollectionView is big enough).
I know how to get content size:
collectionView.collectionViewLayout.collectionViewContentSize
But I have no idea where to use this value. I would appreciate if somebody help me to figure out how to make UICollectionView auto adjust it's height.
UPD:
I published on GitHub a demo project that describes the problem: https://github.com/avokin/PostViewer
I don't think content size is what you're after. I think you're wanting to adjust the amount of screen real estate consumed by the collection view, right? That's going to require adjustment of the frame. The content size includes the off-screen (scrolling) area as well as the on screen view.
I don't know of anything that would prevent you from just changing the frame size on the fly:
collectionView.frame = CGRectMake (x,y,w,h);
[collectionView reloadData];
If I'm understanding you correctly.
Use a height constraint for the collection view and update its value with the content height when needed. See this answer: https://stackoverflow.com/a/20829728/3414722
Steps to change the UICollectionView frame:
Set the "translatesAutoresizingMaskIntoConstraints" property to YES for the collectioview's superview (If you are using AUTOLAYOUT)
Then update the collectioview's frame as :
collectionView.frame = CGRectMake (x,y,w,h);
[collectionView reloadData];
You need to constrain the collection view height to the height of your content:
I'm using SnapKit in the following code.
First constrain the collection view edges to its superview:
private func constrainViews() {
collectionView?.translatesAutoresizingMaskIntoConstraints = true
collectionView?.snp.makeConstraints { make in
make.edges.equalToSuperview()
heightConstraint = make.height.equalTo(0).constraint
}
}
Next calculate the height and set the height to the height constraint offset. I'm letting the flow layout do the work, and then calculating the height based on the bottom edge of the last layout attribute:
override func viewDidLayoutSubviews() {
guard
let collectionView = collectionView,
let layout = collectionViewLayout as? UICollectionViewFlowLayout
else {
return
}
let sectionInset = layout.sectionInset
let contentInset = collectionView.contentInset
let indexPath = IndexPath(item: tags.count, section: 0)
guard let attr = collectionViewLayout.layoutAttributesForItem(at: indexPath) else {
return
}
// Note sectionInset.top is already included in the frame's origin
let totalHeight = attr.frame.origin.y + attr.frame.size.height
+ contentInset.top + contentInset.bottom
+ sectionInset.bottom
heightConstraint?.update(offset: totalHeight)
}
Note that in the example, I always have one special tag not included in my items tags count, so the line:
let indexPath = IndexPath(item: tags.count, section: 0)
would need to be something like if items.count > 0 ... let indexPath = IndexPath(item: tags.count - 1, section: 0) in most other code.