I have a UICollectionView called dayPicker that scrolls horizontally, and lets you select the day of the month. When the user stops scrolling (scrollViewDidEndDecelerating), I want the app to do something with that day, accessible from the cell's label. All of the answers I have seen online are similar to this: https://stackoverflow.com/a/33178797/9036092
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var someCell : UICollectionViewCell = collectionView.visibleCells()[0];
// Other code follows...
}
When I try to access collectionView from inside the scrollViewDidEndDecelerating function, I get a Ambiguous use of collectionView error. When I substitute the actual name of my UICollectionView (dayPicker), it errors out with "Unexpectedly found nil while implicitly unwrapping an Optional value".
My question is: how do you get to collectionView from inside the scrollViewDidSomething function? Currently my scrollViewDidEndDecelerating function is inside a UICollectionViewDelegate in my view controller, and I have also tried putting it in a UIScrollViewDelegate extension.
Current code:
extension PageOneViewController: UICollectionViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let centerPoint = CGPoint(x: UIScreen.main.bounds.midX, y: scrollView.frame.midY)
print(centerPoint) // Successfully prints the same center point every time scrolling stops
let indexPath = collectionView.indexPathForItem(at: centerPoint) // Ambiguous error
let indexPath = dayPicker.indexPathForItem(at: centerPoint) // Fatal error
}
}
Screenshot of scrollable UICollectionView in question:
I also have another method of when the user taps on the day, and it is working flawlessly. Trying to complete the experience with the scrolling ending.
Xcode 11.4.1/Swift 5
I figured it out, for those who come across this thread looking for the same answer. Big thanks to this answer here for a different issue: https://stackoverflow.com/a/45385718/9036092
The missing ingredient was to cast scrollView as a UICollectionView so that you can access the collectionView's cell properties.
Working code:
extension PageOneViewController: UICollectionViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let collectionView = scrollView as! UICollectionView // This is crucial
let centerPoint = CGPoint(x: UIScreen.main.bounds.midX, y: scrollView.frame.midY)
let indexPath = collectionView.indexPathForItem(at: centerPoint)
let centerCell = collectionView.cellForItem(at: indexPath!) as! MyCustomCell
let selectedDay = centerCell.dayLabel.text //
print(selectedDay) // Prints the value of the day in the center of the collectionView, as a string
}
}
Related
I want to implement a Refresh Control on my horizontal UICollectionView, but I can't figure out how to implement it. I essentially want to be able to swipe right one the first cell (cells take up the whole screen) and refresh. I've been searching online and found a cocoa pod that is too outdated, and found a way to add one vertically (not what I want), but nothing what I described. Is it even possible to do something like this?
Here's an attempt on the implementation:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset
let inset = scrollView.contentInset
let y: CGFloat = offset.x - inset.left
let reload_distance: CGFloat = -80
if y < reload_distance{
shouldReload = true
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let _ = scrollView as? UICollectionView {
currentlyScrolling = false
if shouldReload {
reload()
}
}
}
func reload(){
print("RELOADING")
}
the third party library mentioned ended up working. Downloading via cocoa pods was giving errors but downloading the code and adding the .h files to my bridging header allowed it to work. Here's the repo: https://github.com/hoang-tran/HTPullToRefresh
Hi I'm trying to add feedback when scrolling through the collectionview Items. Where should I add code for feedback in collectionview delegates. If I add in willDisplay then add cell that will be displayed initially will call feedback which is not good. I need to provide feedback only when the user scrolls and selects a item.
Assuming that you only scroll in one direction (like vertically) and that all rows of items have the same height, you can use scrollViewDidScroll(_:) to detect selections like UIPickerView.
class ViewController {
var lastOffsetWithSound: CGFloat = 0
}
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let flowLayout = ((scrollView as? UICollectionView)?.collectionViewLayout as? UICollectionViewFlowLayout) {
let lineHeight = flowLayout.itemSize.height + flowLayout.minimumLineSpacing
let offset = scrollView.contentOffset.y
let roundedOffset = offset - offset.truncatingRemainder(dividingBy: lineHeight)
if abs(lastOffsetWithSound - roundedOffset) > lineHeight {
lastOffsetWithSound = roundedOffset
print("play sound feedback here")
}
}
}
}
Remember that UICollectionViewDelegateFlowLayout inherits UICollectionViewDelegate, which itself inherits UIScrollViewDelegate, so you can declare scrollViewDidScroll in any of them.
You can add it in view controller methods
touchesBegan(_:with:)
touchesMoved(_:with:)
So that whenever the user interacts with your view controller anywhere you can provide a feedback and it will only be limited to user interaction and not when you add a cell programetically or call update on your table view.
If you have other UI components as well in your controller and you want to limit the feedback to your collection view and not other components then you can check the view in these methods.
let touch: UITouch = touches.first as! UITouch
if (touch.view == collectionView){
println("This is your CollectionView")
}else{
println("This is not your CollectionView")
}
Don't forget to call super to give system a chance to react to the methods.
Hope this helps.
I have a custom collection view layout which seems to produce some bugs. Or maybe I'm missing something. Layout looks like this and it's from here:
My problem is I can't pass the indexPath of the centered cell to pink button, call of centerCellIndexPath produces nil. I also tried to preselect the cell in the layout itself, like so:
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// item's setup ......
// ...................
let finalPoint = CGPoint(x: newOffsetX, y: proposedContentOffset.y)
// Select cell in collectionView by Default
let updatePoint = CGPoint(x: newOffsetX, y: 180)
let indexPath = collectionView!.indexPathForItem(at: updatePoint)
self.collectionView!.selectItem(at: indexPath, animated: false, scrollPosition: [])
return finalPoint
}
But it doesn't work. I have to manually select the cell by swiping up, which is pretty unclear for user. How should I select the cell without tapping and swiping?
Any advice will be appreciated
UPD: After a night without a sleep finally managed it thanks to Vollan
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let position = 2 * scrollView.contentOffset.x / collectionView.superview!.frame.width
selectedItem = Int(round(position))
}
UPD: But the best just got better. Converted answer from linked to Swift version
This would be better code because it's cleaner and easier to read, all the content offset calculation is superfluous:
NSIndexPath *centerCellIndexPath =
[self.collectionView indexPathForItemAtPoint:
[self.view convertPoint:[self.view center] toView:self.collectionView]];
This would also be the correct representation of what you're actually
trying to do:
1. Taking the center point of your viewcontroller's view - aka visual center point
2. convert it to the coordinate space of the view you're interested in - which is the collection view
3. Get the indexpath that exist at the location given.
So it takes 2 lines:
let centerPoint = self.view.convert(view.center, to: collectionView)
let indexPath = collectionView.indexPathForItem(at: centerPoint)
-> save media[indexPath.row]
And that was basically what Sachin Kishore suggested, but I didn't understand him at first
I kind of like the idea with dynamic indexPath but it takes some rounding, which is not reliable. So I think convert and indexPathForItem is the best here
If you were to put the logic into the scrollViewDidScroll you can update an indexPath dynamically.
let position = scrollView.contentOffset.x/(cellSize+spacing)
currenIndexPath.item = position
This should give you the current cells indexPath at all times.
func viewIndexPathInCollectionView(_ cellItem: UIView, collView: UICollectionView) -> IndexPath? {
let pointInTable: CGPoint = cellItem.convert(cellItem.bounds.origin, to: collView)
if let cellIndexPath = collView.indexPathForItem(at: pointInTable){
return cellIndexPath
}
return nil
}
call this function on button action and pass the button name , collection view name on this function
Hi I am trying to make a home feed like facebook using UICollectionView But in each cell i want to put another collectionView that have 3 cells.
you can clone the project here
I have two bugs the first is when i scroll on the inner collection View the bounce do not bring back the cell to center. when i created the collection view i enabled the paging and set the minimumLineSpacing to 0
i could not understand why this is happening. when i tried to debug I noticed that this bug stops when i remove this line
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
but removing that line brings me this error
The behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values
because my cell have a dynamic Height
here is an example
my second problem is the text on each inner cell dosent display the good text i have to scroll until the last cell of the inner collection view to see the good text displayed here is an example
You first issue will be solved by setting the minimumInteritemSpacing for the innerCollectionView in the OuterCell. So the definition for innerCollectionView becomes this:
let innerCollectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let cv = UICollectionView(frame :.zero , collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .orange
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
cv.isPagingEnabled = true
cv.showsHorizontalScrollIndicator = false
return cv
}()
The second issue is solved by adding calls to reloadData and layoutIfNeeded in the didSet of the post property of OuterCell like this:
var post: Post? {
didSet {
if let numLikes = post?.numLikes {
likesLabel.text = "\(numLikes) Likes"
}
if let numComments = post?.numComments {
commentsLabel.text = "\(numComments) Comments"
}
innerCollectionView.reloadData()
self.layoutIfNeeded()
}
}
What you are seeing is related to cell reuse. You can see this in effect if you scroll to the yellow bordered text on the first item and then scroll down. You will see others are also on the yellow bordered text (although at least with the correct text now).
EDIT
As a bonus here is one method to remember the state of the cells.
First you need to track when the position changes so in OuterCell.swft add a new protocol like this:
protocol OuterCellProtocol: class {
func changed(toPosition position: Int, cell: OutterCell)
}
then add an instance variable for a delegate of that protocol to the OuterCell class like this:
public weak var delegate: OuterCellProtocol?
then finally you need to add the following method which is called when the scrolling finishes, calculates the new position and calls the delegate method to let it know. Like this:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let index = self.innerCollectionView.indexPathForItem(at: CGPoint(x: self.innerCollectionView.contentOffset.x + 1, y: self.innerCollectionView.contentOffset.y + 1)) {
self.delegate?.changed(toPosition: index.row, cell: self)
}
}
So that's each cell detecting when the collection view cell changes and informing a delegate. Let's see how to use that information.
The OutterCellCollectionViewController is going to need to keep track the position for each cell in it's collection view and update them when they become visible.
So first make the OutterCellCollectionViewController conform to the OuterCellProtocol so it is informed when one of its
class OutterCellCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, OuterCellProtocol {
then add a class instance variable to record the cell positions to OuterCellCollectionViewController like this:
var positionForCell: [Int: Int] = [:]
then add the required OuterCellProtocol method to record the cell position changes like this:
func changed(toPosition position: Int, cell: OutterCell) {
if let index = self.collectionView?.indexPath(for: cell) {
self.positionForCell[index.row] = position
}
}
and finally update the cellForItemAt method to set the delegate for a cell and to use the new cell positions like this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OutterCardCell", for: indexPath) as! OutterCell
cell.post = posts[indexPath.row]
cell.delegate = self
let cellPosition = self.positionForCell[indexPath.row] ?? 0
cell.innerCollectionView.scrollToItem(at: IndexPath(row: cellPosition, section: 0), at: .left, animated: false)
print (cellPosition)
return cell
}
If you managed to get that all setup correctly it should track the positions when you scroll up and down the list.
I have a requirement to show border around user's avatar image but only for the user cell at center of table. Also, the border width is proportional to its distance from center.
So I've my cell update logic in UIScrollView delegate method scrollViewDidScroll API and then get the middle cell using UITableView contentOffset.y. Once I know the center point of table I ask the table to give me cell at that point and then updating its value.
However, the call to tableView.cellForRow(at:) randomly throws an exception and crashes. I do guard for nil value but don't know how to handle this crash due to exception.
Also, note centerLevelCellIndexPath value looks good to me when that exception is raised - section 0 row 3 ( My UITableView always have 1 section and about 5-6 rows)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView is UITableView {
let tableViewCenterContentOffset = CGPoint(x: tableView.center.x, y: tableView.contentOffset.y + tableView.bounds.height/2)
guard let centerLevelCellIndexPath = tableView.indexPathForRow(at: tableViewCenterContentOffset) else {
return
}
guard let cell = tableView.cellForRow(at: centerLevelCellIndexPath), // <--- Crashes here
let centerLevelCell = cell as? MyCustomCell,
let collectionView = centerLevelCell.collectionView else {
return
}
Here is the screenshot of exception call stack -