Suspect I am doing something fundamentally wrong below... I have a horizontal collectionview and after dragging I want to snap the closest cell to the center. But my results are unpredictable... what am I doing wrong here?
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// Find collectionview cell nearest to the center of collectionView
// Arbitrarily start with the last cell (as a default)
var closestCell : UICollectionViewCell = collectionView.visibleCells()[0];
for cell in collectionView!.visibleCells() as [UICollectionViewCell] {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPathForCell(closestCell)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
Turns out my original code was missing accounting for the collecitonview content offset. In addition, I've moved the centering into the scrollViewDidEndDecelerating callback. I've modified the original code and included it below.
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
// Find collectionview cell nearest to the center of collectionView
// Arbitrarily start with the last cell (as a default)
var closestCell : UICollectionViewCell = collectionView.visibleCells()[0];
for cell in collectionView!.visibleCells() as [UICollectionViewCell] {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPathForCell(closestCell)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
Try this:
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:self.collectionView.center];
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
SWIFT:
let indexPath = self.collectionView.indexPathForItemAtPoint(self.collectionView.center)
self.collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
None of the solutions reliably worked for me, so I wrote this and it works 100% of the time. Very simple. Even the built in indexPathForItemAtPoint was not working 100%.
extension UICollectionView {
var centerMostCell:UICollectionViewCell? {
guard let superview = superview else { return nil }
let centerInWindow = superview.convert(center, to: nil)
guard visibleCells.count > 0 else { return nil }
var closestCell:UICollectionViewCell?
for cell in visibleCells {
guard let sv = cell.superview else { continue }
let cellFrameInWindow = sv.convert(cell.frame, to: nil)
if cellFrameInWindow.contains(centerInWindow) {
closestCell = cell
break
}
}
return closestCell
}
}
Updated version for Swift 4
var closestCell = collectionView.visibleCells[0]
for cell in collectionView.visibleCells {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPath(for: closestCell)
collectionView.scrollToItem(at: indexPath!, at: .centeredHorizontally, animated: true)
When stop dragging, just pick the center and use it to choose the indexpath
swift 4.2
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let indexPath = photoCollectionView.indexPathForItem(at: photoCollectionView.center)
photoCollectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: .centeredHorizontally, animated: true)
}
var centerUICollectionViewCell: UICollectionViewCell? {
get {
// 1
guard let center = collectionView.superview?.convert(collectionView.center, to: collectionView) else { return nil }
// 2
guard let centerIndexPath = collectionView.indexPathForItem(at: center) else { return nil }
// 3
return collectionView.cellForItem(at: centerIndexPath)
}
}
1: Need to make sure we are converting the point from superview coordinate space to collectionView's space
2: Use the collectionView's local space point to find the indexPath
3: Return the cell for indexPath
public func closestIndexPathToCenter() -> IndexPath? {
guard let cv = collectionView else { return nil }
let visibleCenterPositionOfScrollView = Float(cv.contentOffset.x + (cv.bounds.size.width / 2))
var closestCellIndex = -1
var closestDistance: Float = .greatestFiniteMagnitude
for i in 0..<cv.visibleCells.count {
let cell = cv.visibleCells[i]
let cellWidth = cell.bounds.size.width
let cellCenter = Float(cell.frame.origin.x + cellWidth / 2)
// Now calculate closest cell
let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
if distance < closestDistance {
closestDistance = distance
closestCellIndex = cv.indexPath(for: cell)!.row
}
}
if closestCellIndex != -1 {
return IndexPath(row: closestCellIndex, section: 0)
}else {
return nil
}
}
Related
I have a custom UICollectionViewFlowLayout which lays out items with a left-aligned format.
LAYOUT
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var newAttributesArray = [UICollectionViewLayoutAttributes]()
let superAttributesArray = super.layoutAttributesForElements(in: rect)!
guard let attributesToReturn = superAttributesArray.map( { $0.copy() }) as? [UICollectionViewLayoutAttributes] else {
return nil
}
for (index, attributes) in attributesToReturn.enumerated() {
if index == 0 || attributesToReturn[index - 1].frame.origin.y != attributes.frame.origin.y {
attributes.frame.origin.x = sectionInset.left
} else {
let previousAttributes = attributesToReturn[index - 1]
let previousFrameRight = previousAttributes.frame.origin.x + previousAttributes.frame.width
attributes.frame.origin.x = previousFrameRight + minimumInteritemSpacing
}
newAttributesArray.append(attributes)
}
return attributesToReturn
}
When I reload a cell that is not the first in the horizontal line, the cell tried performs peculiarly is in the below illustration. I believe this is a layout issue. Reloading a cell that's first does not act this way.
RELOAD METHOD
func selectedInterest(for cell: InterestCell) {
guard let indexPath = mainView.collectionView.indexPath(for: cell),
let documentID = cell.interest?.documentID,
let isSaved = cell.interest?.isSaved else { return }
var interest = self.interests[indexPath.item]
interest.isSaved = !isSaved
self.interests[indexPath.item] = interest
self.mainView.collectionView.collectionViewLayout.invalidateLayout()
self.mainView.collectionView.reloadItems(at: [indexPath])
}
I have tried to invalidate the layout before reloading however this has no effect.
I have a tableView where when a user taps on that cell, I want the cell to expand and fill the entire screen with that color. Currently, it does scale to fill the entire screen but the cells above it will also show. The colors come from an API and each cell changes color when the user taps the "Generate" button. Due to how the API's JSON file is structured, I only want 5 colors on the screen at a time.
I can't figure out why the cells above it don't move upwards or how I can move the clicked cell to the top of the screen. I've tried offsetting the coordinates of the tapped cell and changing its frame size to be the height of the tableView tappedCell.bounds.size = CGSize(width: UIScreen.main.bounds.width, height: self.paletteTableView.bounds.height) but when I click the 1st or 2nd cells from the top, the screen doesn't fill all the way to the bottom.
I've also tried animating from a UIView (called colorDetailsView here) but wasn't able to successfully get it to animate from the center of each cell that's tapped on, only from the center of the screen.
The animation happens here:
#objc func userTap(sender: UITapGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.ended {
let tapLocation = sender.location(in: self.paletteTableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {
UIView.animate(withDuration: 0.5, animations: {
switch self.currentAnimation {
case 0:
tappedCell.transform = CGAffineTransform(scaleX: 1, y: 50)
//...
case 1:
tappedCell.transform = .identity
default: break
}
} )
currentAnimation += 1
if currentAnimation > 1 {
currentAnimation = 0
} } } } }
Full code:
class PaletteController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
var colorPalette = [Color]()
var currentAnimation = 0
let colorDetailsView: UIView = {
let view = UIView()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.isScrollEnabled = false
\\...
view.addSubview(colorDetailsView)
}
//Color cell expands to fill the screen of that color when user taps on the cell
#objc func userTap(sender: UITapGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.ended {
let tapLocation = sender.location(in: self.tableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {
UIView.animate(withDuration: 0.5, animations: {
//When user taps on a cell, it is scaled to fill the screen with that color
switch self.currentAnimation {
case 0:
tappedCell.transform = CGAffineTransform(scaleX: 1, y: 50)
for i in 0..<self.colorPalette.count {
if tapIndexPath.row == i {
self.colorDetailsView.backgroundColor = UIColor(hexString: self.colorPalette[i])
}
}
case 1:
tappedCell.transform = .identity
default: break
}
} )
currentAnimation += 1
if currentAnimation > 1 {
currentAnimation = 0
}
}
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:"ColorCell", for: indexPath)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(userTap(sender:)))
cell.addGestureRecognizer(tapGesture)
cell.isUserInteractionEnabled = true
//Assigns a new color to each row. Colors are converted from HEX to RGB
for i in 0..<colorPalette.count {
if tapIndexPath.row == i {
cell.backgroundColor = UIColor(hexString: colorPalette[i])
}
}
return cell
}
}
I know the cell is scaling when I set tableView.isScrollEnabled to true, as shown below. Each cell should end up filling the whole screen and not get covered by any other cells, like the top-most cell. Ideally, I'd like the tableView to not scroll, but I'm not sure if the top cells will "move up" if scroll is disable.
Any help is appreciated!
The cells are after all sub-views of the table view. You need to bring the tapped cell at the front of the sub-view list.
I would suggest adding
tableView.bringSubviewToFront(tappedCell)
just after the line
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {.
See if this works!
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
}
}
}
I am able to reorder my collectionView like so:
However, instead of all cells shifting horizontally, I would just like to swap with the following behavior (i.e. with less shuffling of cells):
I have been playing with the following delegate method:
func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath
however, I am unsure how I can achieve custom reordering behavior.
I managed to achieve this by creating a subclass of UICollectionView and adding custom handling to interactive movement. While looking at possible hints on how to solve your issue, I've found this tutorial : http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/.
The most important part there was that interactive reordering can be done not only on UICollectionViewController. The relevant code looks like this :
var longPressGesture : UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
// rest of setup
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.handleLongGesture(_:)))
self.collectionView?.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView?.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView?.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView?.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView?.endInteractiveMovement()
default:
collectionView?.cancelInteractiveMovement()
}
}
This needs to be inside your view controller in which your collection view is placed. I don't know if this will work with UICollectionViewController, some additional tinkering may be needed. What led me to subclassing UICollectionView was realisation that all other related classes/delegate methods are informed only about the first and last index paths (i.e. the source and destination), and there is no information about all the other cells that got rearranged, so It needed to be stopped at the core.
SwappingCollectionView.swift :
import UIKit
extension UIView {
func snapshot() -> UIImage {
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
extension CGPoint {
func distanceToPoint(p:CGPoint) -> CGFloat {
return sqrt(pow((p.x - x), 2) + pow((p.y - y), 2))
}
}
struct SwapDescription : Hashable {
var firstItem : Int
var secondItem : Int
var hashValue: Int {
get {
return (firstItem * 10) + secondItem
}
}
}
func ==(lhs: SwapDescription, rhs: SwapDescription) -> Bool {
return lhs.firstItem == rhs.firstItem && lhs.secondItem == rhs.secondItem
}
class SwappingCollectionView: UICollectionView {
var interactiveIndexPath : NSIndexPath?
var interactiveView : UIView?
var interactiveCell : UICollectionViewCell?
var swapSet : Set<SwapDescription> = Set()
var previousPoint : CGPoint?
static let distanceDelta:CGFloat = 2 // adjust as needed
override func beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool {
self.interactiveIndexPath = indexPath
self.interactiveCell = self.cellForItemAtIndexPath(indexPath)
self.interactiveView = UIImageView(image: self.interactiveCell?.snapshot())
self.interactiveView?.frame = self.interactiveCell!.frame
self.addSubview(self.interactiveView!)
self.bringSubviewToFront(self.interactiveView!)
self.interactiveCell?.hidden = true
return true
}
override func updateInteractiveMovementTargetPosition(targetPosition: CGPoint) {
if (self.shouldSwap(targetPosition)) {
if let hoverIndexPath = self.indexPathForItemAtPoint(targetPosition), let interactiveIndexPath = self.interactiveIndexPath {
let swapDescription = SwapDescription(firstItem: interactiveIndexPath.item, secondItem: hoverIndexPath.item)
if (!self.swapSet.contains(swapDescription)) {
self.swapSet.insert(swapDescription)
self.performBatchUpdates({
self.moveItemAtIndexPath(interactiveIndexPath, toIndexPath: hoverIndexPath)
self.moveItemAtIndexPath(hoverIndexPath, toIndexPath: interactiveIndexPath)
}, completion: {(finished) in
self.swapSet.remove(swapDescription)
self.dataSource?.collectionView(self, moveItemAtIndexPath: interactiveIndexPath, toIndexPath: hoverIndexPath)
self.interactiveIndexPath = hoverIndexPath
})
}
}
}
self.interactiveView?.center = targetPosition
self.previousPoint = targetPosition
}
override func endInteractiveMovement() {
self.cleanup()
}
override func cancelInteractiveMovement() {
self.cleanup()
}
func cleanup() {
self.interactiveCell?.hidden = false
self.interactiveView?.removeFromSuperview()
self.interactiveView = nil
self.interactiveCell = nil
self.interactiveIndexPath = nil
self.previousPoint = nil
self.swapSet.removeAll()
}
func shouldSwap(newPoint: CGPoint) -> Bool {
if let previousPoint = self.previousPoint {
let distance = previousPoint.distanceToPoint(newPoint)
return distance < SwappingCollectionView.distanceDelta
}
return false
}
}
I do realize that there is a lot going on there, but I hope everything will be clear in a minute.
Extension on UIView with helper method to get a snapshot of a cell.
Extension on CGPoint with helper method to calculate distance between two points.
SwapDescription helper structure - it is needed to prevent multiple swaps of the same pair of items, which resulted in glitchy animations. Its hashValue method could be improved, but was good enough for the sake of this proof of concept.
beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool - the method called when the movement begins. Everything gets setup here. We get a snapshot of our cell and add it as a subview - this snapshot will be what the user actually drags on screen. The cell itself gets hidden. If you return false from this method, the interactive movement will not begin.
updateInteractiveMovementTargetPosition(targetPosition: CGPoint) - method called after each user movement, which is a lot. We check if the distance from previous point is small enough to swap items - this prevents issue when the user would drag fast across screen and multiple items would get swapped with non-obvious results. If the swap can happen, we check if it is already happening, and if not we swap two items.
endInteractiveMovement(), cancelInteractiveMovement(), cleanup() - after the movement ends, we need to restore our helpers to their default state.
shouldSwap(newPoint: CGPoint) -> Bool - helper method to check if the movement was small enough so we can swap cells.
This is the result it gives :
Let me know if this is what you needed and/or if you need me to clarify something.
Here is a demo project.
Swift 5 solution of #Losiowaty solution:
var longPressGesture : UILongPressGestureRecognizer!
override func viewDidLoad()
{
super.viewDidLoad()
// rest of setup
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongGesture))
self.collectionView?.addGestureRecognizer(longPressGesture)
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer)
{
switch(gesture.state)
{
case UIGestureRecognizerState.began:
guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
break
}
collectionView?.beginInteractiveMovementForItem(at: selectedIndexPath)
case UIGestureRecognizerState.changed:
collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case UIGestureRecognizerState.ended:
collectionView?.endInteractiveMovement()
default:
collectionView?.cancelInteractiveMovement()
}
}
import UIKit
extension UIView {
func snapshot() -> UIImage {
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
extension CGPoint {
func distanceToPoint(p:CGPoint) -> CGFloat {
return sqrt(pow((p.x - x), 2) + pow((p.y - y), 2))
}
}
struct SwapDescription : Hashable {
var firstItem : Int
var secondItem : Int
var hashValue: Int {
get {
return (firstItem * 10) + secondItem
}
}
}
func ==(lhs: SwapDescription, rhs: SwapDescription) -> Bool {
return lhs.firstItem == rhs.firstItem && lhs.secondItem == rhs.secondItem
}
class SwappingCollectionView: UICollectionView {
var interactiveIndexPath : IndexPath?
var interactiveView : UIView?
var interactiveCell : UICollectionViewCell?
var swapSet : Set<SwapDescription> = Set()
var previousPoint : CGPoint?
static let distanceDelta:CGFloat = 2 // adjust as needed
override func beginInteractiveMovementForItem(at indexPath: IndexPath) -> Bool
{
self.interactiveIndexPath = indexPath
self.interactiveCell = self.cellForItem(at: indexPath)
self.interactiveView = UIImageView(image: self.interactiveCell?.snapshot())
self.interactiveView?.frame = self.interactiveCell!.frame
self.addSubview(self.interactiveView!)
self.bringSubviewToFront(self.interactiveView!)
self.interactiveCell?.isHidden = true
return true
}
override func updateInteractiveMovementTargetPosition(_ targetPosition: CGPoint) {
if (self.shouldSwap(newPoint: targetPosition))
{
if let hoverIndexPath = self.indexPathForItem(at: targetPosition), let interactiveIndexPath = self.interactiveIndexPath {
let swapDescription = SwapDescription(firstItem: interactiveIndexPath.item, secondItem: hoverIndexPath.item)
if (!self.swapSet.contains(swapDescription)) {
self.swapSet.insert(swapDescription)
self.performBatchUpdates({
self.moveItem(at: interactiveIndexPath as IndexPath, to: hoverIndexPath)
self.moveItem(at: hoverIndexPath, to: interactiveIndexPath)
}, completion: {(finished) in
self.swapSet.remove(swapDescription)
self.dataSource?.collectionView?(self, moveItemAt: interactiveIndexPath, to: hoverIndexPath)
self.interactiveIndexPath = hoverIndexPath
})
}
}
}
self.interactiveView?.center = targetPosition
self.previousPoint = targetPosition
}
override func endInteractiveMovement() {
self.cleanup()
}
override func cancelInteractiveMovement() {
self.cleanup()
}
func cleanup() {
self.interactiveCell?.isHidden = false
self.interactiveView?.removeFromSuperview()
self.interactiveView = nil
self.interactiveCell = nil
self.interactiveIndexPath = nil
self.previousPoint = nil
self.swapSet.removeAll()
}
func shouldSwap(newPoint: CGPoint) -> Bool {
if let previousPoint = self.previousPoint {
let distance = previousPoint.distanceToPoint(p: newPoint)
return distance < SwappingCollectionView.distanceDelta
}
return false
}
}
if you want to track numbers of cells being dragged like this behaviour:
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let targetIndexPath = reorderCollectionView.indexPathForItem(at: gesture.location(in: reorderCollectionView)) else {
return
}
reorderCollectionView.beginInteractiveMovementForItem(at: targetIndexPath)
case .changed:
reorderCollectionView.updateInteractiveMovementTargetPosition(gesture.location(in: reorderCollectionView))
reorderCollectionView.performBatchUpdates {
self.reorderCollectionView.visibleCells.compactMap { $0 as? ReorderCell}
.enumerated()
.forEach { (index, cell) in
cell.countLabel.text = "\(index + 1)"
}
}
case .ended:
reorderCollectionView.endInteractiveMovement()
default:
reorderCollectionView.cancelInteractiveMovement()
}
}
I have a UITableView with more cells than fit on screen. When I get a notification from my data model I want to jump to a specific row and show a very basic animation.
My code is:
func animateBackgroundColor(indexPath: NSIndexPath) {
dispatch_async(dispatch_get_main_queue()) {
NSLog("table should be at the right position")
if let cell = self.tableView.cellForRowAtIndexPath(indexPath) as? BasicCardCell {
var actColor = cell.backgroundColor
self.manager.vibrate()
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = UIColor.redColor() }, completion: {
_ in
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = actColor }, completion: { _ in
self.readNotificationCount--
if self.readNotificationCount >= 0 {
var legicCard = self.legicCards[indexPath.section]
legicCard.wasRead = false
self.reloadTableViewData()
} else {
self.animateBackgroundColor(indexPath)
}
})
})
}
}
}
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int {
dispatch_sync(dispatch_get_main_queue()){
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: index), atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
self.animateBackgroundColor(NSIndexPath(forRow: 0, inSection: index))
}
}
I hoped that the dispatch_sync part would delay the execution of my animateBackgroundColor method until the scrolling is done. Unfortunately that is not the case so that animateBackgroundColor gets called when the row is not visible yet -> cellForRowAtIndexPath returns nil and my animation won't happen. If no scrolling is needed the animation works without problem.
Can anyone tell my how to delay the execution of my animateBackgroundColor function until the scrolling is done?
Thank you very much and kind regards
Delaying animation does not seem to be a good solution for this since scrollToRowAtIndexPath animation duration is set based on distance from current list item to specified item. To solve this you need to execute animateBackgroudColor after scrollToRowAtIndexPath animation is completed by implementing scrollViewDidEndScrollingAnimation UITableViewDelegate method. The tricky part here is to get indexPath at which tableview did scroll. A possible workaround:
var indexPath:NSIndexpath?
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int{
dispatch_sync(dispatch_get_main_queue()){
self.indexPath = NSIndexPath(forRow: 0, inSection: index)
self.tableView.scrollToRowAtIndexPath(self.indexPath, atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
}
}
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
self.animateBackgroundColor(self.indexPath)
indexPath = nil
}
Here are my solution
1) Create a .swift file, and copy code below into it:
typealias SwagScrollCallback = (_ finish: Bool) -> Void
class UICollectionViewBase: NSObject, UICollectionViewDelegate {
static var shared = UICollectionViewBase()
var tempDelegate: UIScrollViewDelegate?
var callback: SwagScrollCallback?
func startCheckScrollAnimation(scroll: UIScrollView, callback: SwagScrollCallback?){
if let dele = scroll.delegate {
self.tempDelegate = dele
}
self.callback = callback
scroll.delegate = self
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
callback?(true)
if let dele = self.tempDelegate {
scrollView.delegate = dele
tempDelegate = nil
}
}
}
extension UICollectionView {
func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionView.ScrollPosition, _ callback: SwagScrollCallback?){
UICollectionViewBase.shared.startCheckScrollAnimation(scroll: self, callback: callback)
self.scrollToItem(at: indexPath, at: scrollPosition, animated: true)
}
}
2) Example:
#IBAction func onBtnShow(){
let index = IndexPath.init(item: 58, section: 0)
self.clv.scrollToItem(at: index, at: .centeredVertically) { [weak self] (finish) in
guard let `self` = self else { return }
// Change color temporarily
if let cell = self.clv.cellForItem(at: index) as? MyCell {
cell.backgroundColor = .yellow
cell.lbl.textColor = .red
}
// Reset
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.clv.reloadData()
}
}
}
3) My github code example: github here
I has similar problem. I just do this.
UIView.animateWithDuration(0.2,delay:0.0,options: nil,animations:{
self.tableView.scrollToRowAtIndexPath(self.indexPath!, atScrollPosition: .Middle, animated: true)},
completion: { finished in UIView.animateWithDuration(0.5, animations:{
self.animateBackgroundColor(self.indexPath)})})}