I am implementing a UICollectionViewCompositionalLayout and using
section.orthogonalScrollingBehavior = .groupPagingCentered
to scroll a section horizontally.
This lets a user scroll from page to page in a section horizontally.
I use
section.visibleItemsInvalidationHandler = {...}
to get the page a user scrolls to.
How can I scroll to a page in this section programmatically?
I actually got it working. This code works XCode 12 Beta 2 / Target iOS14 / Swift 5.1.
In this piece of code, pay special attention to spacing, insets and orthogonalScrollingBehavior !!!
fileprivate func configureCollectionViewLayout() {
func createLayout() -> UICollectionViewLayout {
self.collectionView.canCancelContentTouches = false
let layout = UICollectionViewCompositionalLayout {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionKind = Section(rawValue: sectionIndex) else { return nil }
let spacing = CGFloat(10)
switch sectionKind {
case .someOtherSection:
// configure the layout for this section
case .yetAnotherSection:
// configure the layout for this section
case .sectionWithOrthogonalScrollingBehaviour:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(1.0*9.0/16.0))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
group.interItemSpacing = .fixed(0) // should (probably) be 0 otherwise there is an offset when scrollingToIndex
section.interGroupSpacing = 0 // should (probably) be 0 otherwise there is an offset when scrollingToIndex
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0) // leading and trailing should (certainly) be 0 otherwise the pages are offsetted in relation to the view
section.orthogonalScrollingBehavior = .continuous // should be continous, otherwise it won't work
return section
}
}
return layout
}
collectionView.collectionViewLayout = createLayout()
}
To scroll programatically, I implemented a segmented control. When user taps on a segment, the orthogonally scrolling section scrolls automatically to the cell corresponding with the selected segment.
#IBAction func segmentedControlValueChanged(_ sender: UISegmentedControl) {
// determine which Item belongs to the selected segment. In my case Items in a Section are represented by an enum called 'Item'
let dataSourceIndexPath = self.dataSource.indexPath(for: .itemCase)
let pressentationIndexPath = self.collectionView.presentationIndexPath(forDataSourceIndexPath: dataSourceIndexPath)! // This step is necessary, otherwise the next line does not have have wanted behaviour (I'm force unwrapping here to simplify. Don't do that in your final code)
self.collectionView.scrollToItem(at: pressentationIndexPath, at: .centeredHorizontally, animated: true) // make sure you use .centeredHorizontally
}
I'm not sure this is how Apple intended this to be used. It might brake again in next Xcode beta versions.
Sidenote: In case you were wondering why I need/want a segmented
control to scroll a collection view: It's because the content of the
cells in the orthogonally scrolling section need to consume the
horizontal pan gestures (the cells are all charts that you can move your finger
along and then display the chart value at the touched location).
Yes... I know this can also be done in other ways, but I wanted to use
the compositional layout because it fits together nicely with the
other sections, and I wanted to keep this sweet built in animation of
the orthogonalscrollbehavior (even though this is now partially lost because orthogonalScrollingBehavior needs to be .continues for it to work) and I just wanted to see if it can be done :)
Related
I have a collectionView with compositional layout which scrolls vertically, with each section inside it scrolling horizontally.
I would need to disable bouncing on horizontal scroll, which does not seem to be possible (or maybe I'm doing something wrong?). Disabling vertical bouncing works perfectly.
What I have tried so far:
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.bounces = false
collectionView.alwaysBounceHorizontal = false
collectionView.alwaysBounceVertical = false
collectionView.isDirectionalLockEnabled = true
This does not seem to work for horizontal bouncing - it is still enabled.
My compositional layout is created in this way:
func createCompositionalLayout() {
let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(UIScreen.main.bounds.width), heightDimension: .absolute(UIScreen.main.bounds.height))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
group.interItemSpacing = .fixed(0)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
section.orthogonalScrollingBehavior = .paging
return section
}
collectionView.collectionViewLayout = layout
}
If anyone might know how to solve the issue, any help would be much appreciated!
After spending almost 2 days with the Apple's example class OrthogonalScrollingViewController, it does look weird that the bouncing (on/off) works only for the vertical scrolling.
collectionView.bounces = false
collectionView.alwaysBounceHorizontal = false
collectionView.alwaysBounceVertical = false
This won't work because:
Short answer
Because the collection view's section (compositional layout's section) is not affected by the scroll view at all
Long answer
If you conform to UIScrollViewDelegate (UICollectionViewDelegate has it already, hence no need for that, if you have conformed to the latter), the method doesn't even get called
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
//something should happen
}
and that's because our collection view is vertical -> scroll view direction is vertical and our sections are not affected by it (ever?! really?!)
A discussion that helped me:
UICollectionView CompositionalLayout not calling UIScrollDelegate
I have collectionview with multiple section and vertical scrolling.
Each have only one item.
Screen flow is like below:
I call an api which provides me feedId and url for to fetch section data.
When user scrolls collectionview api gets called for section whichever section is currently visible on screen.
Inside each collectionview cell (or section) there is another collectionview with grid layout which renders grid image like below
Outer Collectionview
I am using Diffable datasource with compositional layout and while applying the snapshot the code is like below.
if #available(iOS 15.0, *) {
updateSnapshot.reconfigureItems([item])
} else {
updateSnapshot.reloadItems([item])
// Fallback on earlier versions
}
if #available(iOS 15.0, *) {
dataSource.applySnapshotUsingReloadData(updateSnapshot)
} else {
dataSource.apply(updateSnapshot, animatingDifferences: false, completion: nil)
// Fallback on earlier versions
}
Inner collectionview code is like below
var dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, GlobalGalleryViewModel>> {[] (ds, cv, indexPath, vm) -> UICollectionViewCell in
let cell = cv.dequeueReusableCell(withReuseIdentifier: GlobalGalleryCell.className, for: indexPath) as! GlobalGalleryCell
cell.configureCell(vm)
return cell
}
Issue
When I scroll the outer collectionview there is lots of glitches and flickering when inner gallery renders on outer collectionview cell.
Layout code for Outer collectionview
let parentItemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension:feedInterface.isHaveActualFeed ? .estimated(height) : .fractionalHeight(1)
)
let largeItem = NSCollectionLayoutItem(layoutSize: parentItemSize)
largeItem.contentInsets = NSDirectionalEdgeInsets(top: parentInset, leading: parentInset, bottom: parentInset, trailing: parentInset)
// Outer Group
let outerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension:.estimated(height))
let outerGroup = NSCollectionLayoutGroup.vertical(layoutSize: outerGroupSize, subitems: [largeItem])
Also added the cell prefetching like this
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
self.updateVisibleCellRemotely(indexPaths)
if let lastIndexPath = indexPaths.last,
lastIndexPath.section == (self.feeds.count - 1),
self.feeds.count > 0 {
self.viewModel.fetchBottomFeedData()
}///load more vertical or main feed or parent feed
}
Any solution will be appreciated.
I have the following layout for my compositional layout
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let dayItemSize = NSCollectionLayoutSize(widthDimension: .absolute(74), heightDimension: .fractionalHeight(1))
// 2. Setup media group
let dayItem = NSCollectionLayoutItem(layoutSize: dayItemSize)
let dayGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),heightDimension: .fractionalHeight(1.0))
let dayGroup = NSCollectionLayoutGroup.horizontal(layoutSize: dayGroupSize, subitems: [dayItem])
let interitemSpacing = CGFloat(10)
dayGroup.interItemSpacing = .fixed(interitemSpacing)
let section = NSCollectionLayoutSection(group: dayGroup)
section.orthogonalScrollingBehavior = .continuous
return section
}
return layout
}
Then there are 30 "day" items in my collectionView.
fileprivate func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { (collectionView, indexPath, YearMonthDay) ->
UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TimelineDayCell.identifier, for: indexPath) as? TimelineDayCell
cell?.configure(with: YearMonthDay)
cell?.dayLabel.text = String(indexPath.section)+","+String(indexPath.row)
return cell
})
return dataSource
}
func configureDataSource() {
self.collectionView!.register(TimelineDayCell.nib, forCellWithReuseIdentifier: TimelineDayCell.identifier)
}
func applySnapshot(animatingDifferences: Bool = true) {
// 2
var snapshot = DataSourceSnapshot()
snapshot.appendSections([.main])
snapshot.appendItems(days) # 30 of these
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
My UIcollectionViewController covers the entire width of the screen. My cells in the screen look like this:
The text on the cells is their index path. You can see that between 0-4, 5-9 the spacing is uniform, then on the edges 4-5 and 9-10, its different. I believe this is because its the "edge" between screens. But what can I do about it?
I've also tried using no orthogonal scroll behavior and just setting the layout config's scroll direction
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .horizontal
layout.configuration = config
return layout
It seems to achieve the same thing. Any suggestions here?
EDIT: It seems that this behavior is based on the sizing of the group. For instance when I use let dayGroup = NSCollectionLayoutGroup.horizontal(layoutSize: dayGroupSize, subitem: dayItem, count: 30), it makes all of the items lie next to each other like so:
Now there are 30 separate items. Notice how it has auto-spaced the distance between them so that it can fit 30 into 1 screen width. Thus it is still trying to force all of the items in 1 group into a single screen which is the behavior I don't want. I want the group to be the width of the entire content size and to be scrollable.
Ok I solved it. It has to do with the group sizing. I had let dayGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),heightDimension: .fractionalHeight(1.0))
The inter-item spacing is between the group items. And the group size was the size of my window. So I need to make the group size the size of the entire content (bigger than the window for instance.
I am using a UICompositionalLayout to set the layout of my collection view. For the header of each section I am using a UICollectionReusableView, which contains three buttons at the top and two textviews below them. The UI is set up in the following way
The user should be able to write some notes in both uitextviews and those views should resize their height according to their intrinsic heights, I have disabled scrolling for both textfields but whenever the user writes more than the width of the collection view, that part of the text disappears instead of the textview to resize.
The following is the code I am using for the collection view compositional layout
static func setupCollectionViewCompositionalLayout() -> UICollectionViewCompositionalLayout{
let compositionalLayout = UICollectionViewCompositionalLayout { (sectionNumber, env) -> NSCollectionLayoutSection? in
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)))
item.contentInsets.trailing = 16
item.contentInsets.leading = 16
let group = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(200))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: TestTableCollectonViewTopHeaderCollectionReusableView.reuseIdentifier, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
}
return compositionalLayout
}
The reusable view comes from a xib with very simple auto layout constraints, the buttons have fixed width and heights, all subviews have 8 points from leading and trailing and there is a spacing of 16 between subviews.
According to this constraints the layout should resize according to the content but the text keeps disappearing and the header doesn't resize as it should be, even though I'm setting the height to be calculated according to the auto layout of the view.
If someone could point me in the right way, it would be really appreciated
Thanks in advance
In my collection view using UICollectionViewCompositionalLayout, I would like the item size to be 85x85 when the view width is small, otherwise 100x100. But this is specified when creating the layout so how do you change the layout size when the screen width changes, for example rotating the iPhone or changing the split view configuration on iPad?
collectionView.collectionViewLayout = {
let size = CGFloat(view.bounds.width < 375 ? 85 : 100)
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(size), heightDimension: .absolute(size))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(size))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
return UICollectionViewCompositionalLayout(section: section)
}()
Instead of calling UICollectionViewCompositionalLayout(section:), which is a static way of setting a section, use the other initializer, init(sectionProvider:), where you pass a section provider function. Your function now receives the environment as a parameter, including the collection view size, so now you can make your decision in real time depending on the environment. And you get called again if the environment changes, so presto, you can deal with the app changing size (as in an iPad that goes into split screen mode).