I have currently created an array of images [1 - 8]. I have also created a collectionview that pulls images out of the imageArray and puts images in the imageview which is in the cell. Please note that the cell has one imageView in it and it takes up the whole screen, horizontal scrolling and paging are both enabled.
Right now with the current code I have, is very odd what is currently happening so I will tell you what is currently going on. So what currently is happening (I'm going by image 1 which index 0) if the image is on 2 (index 1) and then you swipe next to 3 (index 2), it skips image 3 (index 2) and 4 (index 3) and sits on image 5 (index 4), so when I mean skips, I mean it slides past the image you just swiped to and one more.
(Oddly it deletes image 1 and 2) once on 5. I believe this is due to the it is updating the index or setting it to 2 over again because it just deleted 0. I know this might be hard to get but just think of scrollview with paging and when you swipe to the third image, it skips the one your on and one more and slides you to image 5 where it stays place.
So far thanks to some of you, I have came up with this code below, but I am hoping someone will be able to solve this awful mess.
var currentImage = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let x = floor(myCollectionView.contentOffset.x / view.frame.width)
if Int(x) != currentImage {
currentImage = Int(x)
}
if currentImage > 1 {
for collectionCell in myCollectionView.visibleCells as [UICollectionViewCell] {
let visibleCells = myCollectionView.visibleCells
if visibleCells.first != nil {
if let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell) {
let indexPathOfLastItem = (indexPath.item) - 1
let indexPathOfItemToDelete = IndexPath(item: (indexPathOfLastItem), section: 0)
imageArray.remove(at: (indexPath.item) - 1)
myCollectionView.deleteItems(at: [indexPathOfItemToDelete])
}
}
}
}
}
These codes will delete a specific item using the item index. This is working great!
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var visibleRect = CGRect()
visibleRect.origin = myCollectionView.contentOffset
visibleRect.size = myCollectionView.bounds.size
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
let visibleIndexPath: IndexPath = myCollectionView.indexPathForItem(at: visiblePoint)!
print(visibleIndexPath)
fruitArray.remove(at: visibleIndexPath.row )
myCollectionView.deleteItems(at: [visibleIndexPath])
myCollectionView.scrollToItem(at: IndexPath.init(row: visibleIndexPath.row-1, section: 0), at: UICollectionViewScrollPosition.centeredHorizontally, animated: false)
}
Use scrollViewDidEndDragging to detect when user stops scrolling and delete the cell,..
Objective C
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// if decelerating, let scrollViewDidEndDecelerating: handle it
if (decelerate == NO) {
[self deleteCell];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self deleteCell];
}
- (void)deleteCell {
NSIndexPath *pathForCenterCell = [self.tableView indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.tableView.bounds), CGRectGetMidY(self.tableView.bounds))]; // if you want middle cell
NSIndexPath *firstVisibleIndexPath = [[self.tableView indexPathsForVisibleRows] objectAtIndex:0]; // if you want first visible cell
NSIndexPath *lastVisibleIndexPath = [[self.tableView indexPathsForVisibleRows] objectAtIndex:[self.tableView indexPathsForVisibleRows].count]; // last cell in visible cells
myCollectionView.beginUpdates() //if you are performing more than one operation use this
yourImage.removeObjectAtIndex(myNSIndexPath.row)
myCollectionView.deleteItems(at: [lastVisibleIndexPath])
myCollectionView.endUpdates()
}
Swift 3.0
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// if decelerating, let scrollViewDidEndDecelerating: handle it
if decelerate == false {
deleteCell()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
deleteCell()
}
func deleteCell() {
var pathForCenterCell: IndexPath? = tableView.indexPathForRow(at: CGPoint(x: CGFloat(tableView.bounds.midX), y: CGFloat(tableView.bounds.midY)))
// if you want middle cell
var firstVisibleIndexPath: IndexPath? = (tableView.indexPathsForVisibleRows?[0] as? IndexPath)
// if you want first visible cell
var lastVisibleIndexPath: IndexPath? = (tableView.indexPathsForVisibleRows?[tableView.indexPathsForVisibleRows?.count] as? IndexPath)
// last cell in visible cells
myCollectionView.beginUpdates()()
//if you are performing more than one operation use this
yourImage.removeObjectAtIndex(myNSIndexPath.row)
myCollectionView.deleteItems
}
Happy coding
Related
I have currently trying to create a way to delete an item in a collection view at the indexPath of 1 back. So far i have used some help to create a function with scrollview did scroll to create a way to count which image the user is on by the current image method. I now need a way to count which cell the user is on. Here is my code>
var currentImage = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let x = floor(myCollectionView.contentOffset.x / view.frame.width)
if Int(x) != currentImage {
currentImage = Int(x)
//print(currentImage)
}
if currentImage > 0 {
for collectionCell in myCollectionView.visibleCells as [UICollectionViewCell] {
let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell)!
let indexPathOfLastItem = (indexPath?.item)! - 1
let indexPathOfItemToDelete = IndexPath(item: (indexPathOfLastItem), section: 0)
imageArray.remove(at: 0)
myCollectionView.deleteItems(at: [indexPathOfItemToDelete])
currentImage = 1
Based more on the comments than your actual question, what you seem to want is to get the first visible cell's index path so you can use that path to delete the cell.
let visibleCells = myCollectionView.visibleCells
if let firstCell = visibleCells.first() {
if let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell) {
// use indexPath to delete the cell
}
}
None of this should be used or done in the scrollViewDidScroll delegate method.
[Updated for Swift 5.2] Here's a slightly more succinct way than #rmaddy's answer (but that one works just fine):
guard let indexPath = collectionView.indexPathsForVisibleItems.first else {
return
}
// Do whatever with the index path here.
I'm trying to implement a smoother way for a use to add new collectionViewCells for myCollectionView (which only shows one cell at a time). I want it to be something like when the user swipes left and if the user is on the last cell myCollectionView inserts a new cell when the user is swiping so that the user swipes left "into" the cell. And also I only allow the user to scroll one cell at a time.
EDIT:
So I think it is a bit hard to describe it in words so here is a gif to show what I mean
So in the past few weeks I have been trying to implement this in a number of different ways, and the one that I have found the most success with is by using the scrollViewWillEndDragging delegate method and I have implemented it like this:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Getting the size of the cells
let flowLayout = myCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let cellWidth = flowLayout.itemSize.width
let cellPadding = 10.0 as! CGFloat
// Calculating which page "card" we should be on
let currentOffset = scrollView.contentOffset.x - cellWidth/2
print("currentOffset is \(currentOffset)")
let cardWidth = cellWidth + cellPadding
var page = Int(round((currentOffset)/(cardWidth) + 1))
print("current page number is: \(page)")
if (velocity.x < 0) {
page -= 1
}
if (velocity.x > 0) {
page += 1
}
print("Updated page number is: \(page)")
print("Previous page number is: \(self.previousPage)")
// Only allowing the user to scroll for one page!
if(page > self.previousPage) {
page = self.previousPage + 1
self.previousPage = page
}
else if (page == self.previousPage) {
page = self.previousPage
}
else {
page = self.previousPage - 1
self.previousPage = page
}
print("new page number is: " + String(page))
print("addedCards.count + 1 is: " + String(addedCards.count + 1))
if (page == addedCards.count) {
print("reloading data")
// Update data source
addedCards.append("card")
// Method 1
cardCollectionView.reloadData()
// Method 2
// let newIndexPath = NSIndexPath(forItem: addedCards.count - 1, inSection: 0)
// cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
}
// print("Centering on new cell")
// Center the cardCollectionView on the new page
let newOffset = CGFloat(page * Int((cellWidth + cellPadding)))
print("newOffset is: \(newOffset)")
targetContentOffset.memory.x = newOffset
}
Although I think I have nearly got the desired result there are still some concerns and also bugs that I have found.
My main concern is the fact that instead of inserting a single cell at the end of myCollectionView I'm reloading the whole table. The reason that I do this is because if I didn't then myCollectionView.contentOffset wouldn't be changed and as a result when a new cell is created, myCollectionView isn't centered on the newly created cell.
1. If the user scrolls very slowly and then stops, the new cell gets created but then myCollectionView gets stuck in between two cells it doesn't center in on the newly created cell.
2. When myCollectionView is in between the second last cell and the last cell as a result of 1., the next time the user swipes right, instead of creating one single cell, two cells are created.
I've also used different ways to implement this behaviour such as using scrollViewDidScroll, and various other but to no avail. Can anyone point me in the right direction as I am kind of lost.
Here is a link to download my project if you want to see the interaction:
My Example Project
These the old methods if you're interested:
To do this I have tried 2 ways,
The first being:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// page is the current cell that the user is on
// addedCards is the array that the data source works with
if (page == addedCards.count + 1) {
let placeholderFlashCardProxy = FlashCardProxy(phrase: nil, pronunciation: nil, definition: nil)
addedCards.append(placeholderFlashCardProxy)
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
cardCollectionView.reloadData()
}
}
The problem with using this method is that:
Sometimes I will get a crash as a result of: NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0.
When a new collectionCell is added it will sometimes display the input that the user has written from the previous cell (this might have sometimes to do with the dequeuing and re-use of cell, although again, I'm not sure and I would be grateful if someone answered it)
The inserting isn't smooth, I want the user to be able to swipe left at the last cell and "into" a new cell. As in if I am currently on the last cell, swiping left would put me automatically at the new cell, because right now when I swipe left a new cell is created by it doesn't center on the newly created cell
The second method that I am using is:
let swipeLeftGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipedLeftOnCell:")
swipeLeftGestureRecognizer.direction = .Left
myCollectionView.addGestureRecognizer(swipeLeftGestureRecognizer)
swipeLeftGestureRecognizer.delegate = self
Although the swipe gesture very rarely responds, but if I use a tap gesture, myCollectionView always responds which is very weird (I know again this is a question on its own)
My question is which is the better way to implement what I have described above? And if none are good what should I work with to create the desired results, I've been trying to do this for two days now and I was wondering if someone could point me in the right direction. Thanks!
I hope this helps out in some way :)
UPDATE
I updated the code to fix the issue scrolling in either direction.
The updated gist can be found here
New Updated Gist
Old Gist
First I'm gonna define some model to for a Card
class Card {
var someCardData : String?
}
Next, create a collection view cell, with a card view inside that we will apply the transform to
class CollectionViewCell : UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(cardView)
self.addSubview(cardlabel)
}
override func prepareForReuse() {
super.prepareForReuse()
cardView.alpha = 1.0
cardView.layer.transform = CATransform3DIdentity
}
override func layoutSubviews() {
super.layoutSubviews()
cardView.frame = CGRectMake(contentPadding,
contentPadding,
contentView.bounds.width - (contentPadding * 2.0),
contentView.bounds.height - (contentPadding * 2.0))
cardlabel.frame = cardView.frame
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
lazy var cardView : UIView = {
[unowned self] in
var view = UIView(frame: CGRectZero)
view.backgroundColor = UIColor.whiteColor()
return view
}()
lazy var cardlabel : UILabel = {
[unowned self] in
var label = UILabel(frame: CGRectZero)
label.backgroundColor = UIColor.whiteColor()
label.textAlignment = .Center
return label
}()
}
Next setup the view controller with a collection view. As you will see there is a CustomCollectionView class, which I will define near the end.
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var cards = [Card(), Card()]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.frame = CGRectMake(0, 0, self.view.bounds.width, tableViewHeight)
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, contentPadding)
}
lazy var collectionView : CollectionView = {
[unowned self] in
// MARK: Custom Flow Layout defined below
var layout = CustomCollectionViewFlowLayout()
layout.contentDelegate = self
var collectionView = CollectionView(frame: CGRectZero, collectionViewLayout : layout)
collectionView.clipsToBounds = true
collectionView.showsVerticalScrollIndicator = false
collectionView.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
}()
// MARK: UICollectionViewDelegate, UICollectionViewDataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cards.count
}
func collectionView(collectionView : UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAtIndexPath indexPath:NSIndexPath) -> CGSize {
return CGSizeMake(collectionView.bounds.width, tableViewHeight)
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cellIdentifier = "CollectionViewCell"
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! CollectionViewCell
cell.contentView.backgroundColor = UIColor.blueColor()
// UPDATE If the cell is not the initial index, and is equal the to animating index
// Prepare it's initial state
if flowLayout.animatingIndex == indexPath.row && indexPath.row != 0{
cell.cardView.alpha = 0.0
cell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, 0.0, 0.0, 0.0)
}
return cell
}
}
UPDATED - Now For the really tricky part. I'm gonna define the CustomCollectionViewFlowLayout. The protocol callback returns the next insert index calculated by the flow layout
protocol CollectionViewFlowLayoutDelegate : class {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath)
}
/**
* Custom FlowLayout
* Tracks the currently visible index and updates the proposed content offset
*/
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
weak var contentDelegate: CollectionViewFlowLayoutDelegate?
// Tracks the card to be animated
// TODO: - Adjusted if cards are deleted by one if cards are deleted
private var animatingIndex : Int = 0
// Tracks thje currently visible index
private var visibleIndex : Int = 0 {
didSet {
if visibleIndex > oldValue {
if visibleIndex > animatingIndex {
// Only increment the animating index forward
animatingIndex = visibleIndex
}
if visibleIndex + 1 > self.collectionView!.numberOfItemsInSection(0) - 1 {
let currentEntryIndex = NSIndexPath(forRow: visibleIndex + 1, inSection: 0)
contentDelegate?.flowLayout(self, insertIndex: currentEntryIndex)
}
} else if visibleIndex < oldValue && animatingIndex == oldValue {
// if we start panning to the left, and the animating index is the old value
// let set the animating index to the last card.
animatingIndex = oldValue + 1
}
}
}
override init() {
super.init()
self.minimumInteritemSpacing = 0.0
self.minimumLineSpacing = 0.0
self.scrollDirection = .Horizontal
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// The width offset threshold percentage from 0 - 1
let thresholdOffsetPrecentage : CGFloat = 0.5
// This is the flick velocity threshold
let velocityThreshold : CGFloat = 0.4
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5))
let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5))
let currentHorizontalOffset = collectionView!.contentOffset.x
// If you either traverse far enought in either direction,
// or flicked the scrollview over the horizontal velocity in either direction,
// adjust the visible index accordingly
if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold {
visibleIndex = max(0 , (visibleIndex - 1))
} else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold {
visibleIndex += 1
}
var _proposedContentOffset = proposedContentOffset
_proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex)
return _proposedContentOffset
}
}
And define the delegate methods in your view controller to insert a new card when the delegate tell it that it needs a new index
extension ViewController : CollectionViewFlowLayoutDelegate {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath) {
cards.append(Card())
collectionView.performBatchUpdates({
self.collectionView.insertItemsAtIndexPaths([index])
}) { (complete) in
}
}
And below is the custom Collection view that applies the animation while scrolling accordingly :)
class CollectionView : UICollectionView {
override var contentOffset: CGPoint {
didSet {
if self.tracking {
// When you are tracking the CustomCollectionViewFlowLayout does not update it's visible index until you let go
// So you should be adjusting the second to last cell on the screen
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 1, inSection: 0))
} else {
// Once the CollectionView is not tracking, the CustomCollectionViewFlowLayout calls
// targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:), and updates the visible index
// by adding 1, thus we need to continue the trasition on the second the last cell
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 2, inSection: 0))
}
}
}
/**
This method applies the transform accordingly to the cell at a specified index
- parameter atIndex: index of the cell to adjust
*/
func adjustTransitionForOffset(atIndex : NSIndexPath) {
if let lastCell = self.cellForItemAtIndexPath(atIndex) as? CollectionViewCell {
let progress = 1.0 - (lastCell.frame.minX - self.contentOffset.x) / lastCell.frame.width
lastCell.cardView.alpha = progress
lastCell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, progress, progress, 0.0)
}
}
}
I think u missed one point here in
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
the indexPath should be
NSIndexPath(forItem : addedCards.count - 1, inSection : 0) not addedCards.count
Thats why you were getting the error
NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0
I'm working on a chat screen now(UITableView + input TextField).
I want my UITableView reload more chat messages staying current scroll position when I scroll to top(If I load older 20 messages more, I still seeing a 21st message.) KakaoTalk and Line Apps is doing this. In that apps, I can scroll up infinitely because the scroll position is staying.(I mean I'm still seeing the same chat message)
I'm checking the row index in tableView:willDisplayCell:forRowAtIndexPath:, so I fetch more chat messages when the index is 0. I finished the logic fetching more messages from DB, but I didn't finish polishing UI.
This is the code.
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row == 0 && !isTableViewLoading {
if photo.chatMessages.count != chatMessages.count {
var loadingChatItems: Array<ChatMessage>
let start = photo.chatMessages.count - chatMessages.count - Metric.ChatMessageFetchAmount
if start > 0 {
loadingChatItems = Database.loadChatMessages(photoId, startIndex: start, amount: Metric.ChatMessageFetchAmount)
} else {
loadingChatItems = Database.loadChatMessages(photoId, startIndex: 0, amount: Metric.ChatMessageFetchAmount + start)
}
chatMessages = loadingChatItems + chatMessages
isTableViewLoading = true
var indexPaths = Array<NSIndexPath>()
for row in 0..<loadingChatItems.count {
let indexPath = NSIndexPath(forRow: row, inSection: 0)
indexPaths.append(indexPath)
}
tableView.scrollEnabled = false
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
}
} else {
isTableViewLoading = false
tableView.scrollEnabled = true
}
}
Any suggestions?
Swift 3
This post is a bit old, but here a solution:
1) Detect when you are scrolling near to the top in scrollViewDidScroll,
2) Load the new messages
3) Save the contentSize, reload the tableView
4) Set the contentOffset.y of the tableView to (newContentSizeHeight - oldContentSizeHeight) which is exactly the current point
and here the code:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == tableView {
if tableView.contentOffset.y < 50 {
loadMessages()
let oldContentSizeHeight = tableView.contentSize.height
tableView.reloadData()
let newContentSizeHeight = tableView.contentSize.height
tableView.contentOffset = CGPoint(x:tableView.contentOffset.x,y:newContentSizeHeight - oldContentSizeHeight)
}
}
}
In viewDidLoad:
self.refreshControl?.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
Then add the function:
func refresh(refreshControl: UIRefreshControl) {
//your code here
refreshControl.endRefreshing()
}
This will allow the user to scroll up to refresh the UITableView.
I have implemented another solution, it uses the scroll listener from the delegate, and the response of the asynchronous service.
So, it scrolls without animation to the position where the loading of more messages started, because messages are being appended to my array (It causes the transition to be barely affected).
I decided to do it in this way because the first solution has a problem, and it is that when you let your row height to be set automatically, then the new content height will not be accurate.
Here's my code.
- (void) scrollViewDidScroll:(UIScrollView*)scrollView {
if (!topReached && scrollView == self.table) {
if (!isLoadingTop && self.table.contentOffset.y < 40) {
isLoadingTop = YES;
[self loadChat];
}
}
}
// method used by the response of the async service
- (void)fillMessages: (NSArray*) messageArray {
if ([messageArray count] == 0) {
topReached = YES;
} else {
if ([messageArray count] < limit) {
topReached = YES;
}
for (int i=0; i<messageArray.count; i++) {
NSDictionary* chat = [messageArray objectAtIndex:i];
[messages insertObject:[[MessageModel alloc] initWithDictionary:chat] atIndex:i];
}
[table reloadData];
if (!isLoadingTop)
[self scrollToBottom];
else {
isLoadingTop = NO;
[self.table
scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[messageArray count] inSection:0]
atScrollPosition:UITableViewScrollPositionTop animated:false];
}
offset += limit;
}
}
As an adicional information, I like to block the access of my async services consumptions in a view controller, so one service can be called just once in the same view.
I have one tableView in my storyBoard where I added 4 static cell into it and my storyBoard look like:
I don't have any dataSource for this tableView because my cells are static.
And I use below code to drag a cell and it is working fine till I scroll a table.
import UIKit
class TableViewController: UITableViewController {
var sourceIndexPath: NSIndexPath = NSIndexPath()
var snapshot: UIView = UIView()
let longPress: UILongPressGestureRecognizer = {
let recognizer = UILongPressGestureRecognizer()
return recognizer
}()
override func viewDidLoad() {
super.viewDidLoad()
longPress.addTarget(self, action: "longPressGestureRecognized:")
self.tableView.addGestureRecognizer(longPress)
self.tableView.allowsSelection = false
}
override func viewWillAppear(animated: Bool) {
self.tableView.reloadData()
}
// MARK: UIGestureRecognizer
func longPressGestureRecognized(gesture: UILongPressGestureRecognizer){
let state: UIGestureRecognizerState = gesture.state
let location:CGPoint = gesture.locationInView(self.tableView)
if let indexPath: NSIndexPath = self.tableView.indexPathForRowAtPoint(location){
switch(state){
case UIGestureRecognizerState.Began:
sourceIndexPath = indexPath
let cell: UITableViewCell = self.tableView .cellForRowAtIndexPath(indexPath)!
//take a snapshot of the selected row using helper method
snapshot = customSnapshotFromView(cell)
//add snapshot as subview, centered at cell's center
var center: CGPoint = cell.center
snapshot.center = center
snapshot.alpha = 0.0
self.tableView.addSubview(snapshot)
UIView.animateWithDuration(0.25, animations: { () -> Void in
center.y = location.y
self.snapshot.center = center
self.snapshot.transform = CGAffineTransformMakeScale(1.05, 1.05)
self.snapshot.alpha = 0.98
cell.alpha = 0.0
}, completion: { (finished) in
cell.hidden = true
})
case UIGestureRecognizerState.Changed:
let cell: UITableViewCell = self.tableView.cellForRowAtIndexPath(indexPath)!
var center: CGPoint = snapshot.center
center.y = location.y
snapshot.center = center
print("location \(location.y)")
//is destination valid and is it different form source?
if indexPath != sourceIndexPath{
//update data source
//I have commented this part because I am not using any dataSource.
// self.customArray.exchangeObjectAtIndex(indexPath.row, withObjectAtIndex: sourceIndexPath.row)
//move the row
self.tableView.moveRowAtIndexPath(sourceIndexPath, toIndexPath: indexPath)
//and update source so it is in sync with UI changes
sourceIndexPath = indexPath
}
if (location.y < 68) || (location.y > 450) {
print("cancelled")
self.snapshot.alpha = 0.0
cell.hidden = false
UIView.animateWithDuration(0.10, animations: { () -> Void in
self.snapshot.center = cell.center
self.snapshot.transform = CGAffineTransformIdentity
self.snapshot.alpha = 0.0
//undo fade out
cell.alpha = 1.0
}, completion: { (finished) in
self.snapshot.removeFromSuperview()
})
}
case UIGestureRecognizerState.Ended:
//clean up
print("ended")
let cell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
cell.hidden = false
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.snapshot.center = cell.center
self.snapshot.transform = CGAffineTransformIdentity
self.snapshot.alpha = 0.0
//undo fade out
cell.alpha = 1.0
}, completion: { (finished) in
self.snapshot.removeFromSuperview()
})
break
default:
break
}
}else{
gesture.cancelsTouchesInView = true
}
}
func customSnapshotFromView(inputView: UIView) -> UIView {
// Make an image from the input view.
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0)
inputView.layer.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
// Create an image view.
let snapshot = UIImageView(image: image)
snapshot.layer.masksToBounds = false
snapshot.layer.cornerRadius = 0.0
snapshot.layer.shadowOffset = CGSize(width: -5.0, height: 0.0)
snapshot.layer.shadowRadius = 5.0
snapshot.layer.shadowOpacity = 0.4
return snapshot
}
}
When I scroll after dragging it looks like:
As you can see cell is not appearing again. I want to drag and drop static cell and I want to save it's position so I will not rearrange again when I scroll.
Sample project for more Info.
This is just a demo project But I have added many elements into my cell and every cell have different UI.
There is a library that does exactly what you are looking to do with a very similar approach. It's called FMMoveTableView but it's for cells with a datasource.
I think that what is causing your problem is that when you move the cells around and then you scroll the datasource from the storyboard is no longer in sync with the table and therefore your cell object can't be redrawn.
I think you should implement your table this way:
Make your 4 cells custom cells.
Subclass each one.
Create an Array with numbers 1 to 4
Reorder the array on long drag
Override cellForRowAtIndexPath to show the right cell for the right number
You can drag uitableview cell from uitableview delegates .......
1) set the table view editing style to none in its delegate.
2) implement table view delegate to enable dragging of cell i.e canMoveRowAtIndexPath methods...
You can create multiple dynamic cells.
You'll just have to dequeue cells with correct identifier.
Are you doing this for layout purposes only, maybe a UICollectionView or a custom made UIScrollView could do the job?
Never the less, I have a solution:
Create a IBOutlet collection holding all your static UITableViewCells
Create a index list to simulate a "data source"
Override the cellForRowAtIndexPath to draw using your own index list
When updating the list order, update the indexList so that the view "remembers" this change
This Table view controller explains it all:
class TableViewController: UITableViewController {
#IBOutlet var outletCells: [UITableViewCell]!
var indexList = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
// Prepare a index list.
// We will move positions in this list instead
// of trying to move the view's postions.
for (index, _) in outletCells.enumerate() {
indexList.append(index)
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Use dynamic count, not needed I guess but
// feels better this way.
return outletCells.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Use the index path to get the true index and return
// the view on that index in our IBOutlet collection
let realIndexForPos = indexList[indexPath.row]
return outletCells[realIndexForPos]
}
#IBAction func onTap(sender: AnyObject) {
// Simulating your drag n drop stuff here... :)
let swapThis = 1
let swapThat = 2
tableView.moveRowAtIndexPath(NSIndexPath(forItem: swapThis, inSection: 0), toIndexPath: NSIndexPath(forItem: swapThat, inSection: 0))
// Update the indexList as this is the "data source"
// Do no use moveRowAtIndexPath as this is never triggred
// This one line works: swap(&indexList[swapThis], &indexList[swapThat])
// But bellow is easier to understand
let tmpVal = indexList[swapThis]
indexList[swapThis] = indexList[swapThat]
indexList[swapThat] = tmpVal
}
}
To create the IBOutlet use the Interface Builder.
Use the Referencing Outlet Collection on each Table View Cell and drag, for each, to the same #IBOutlet in your controller code.
I have a UICollectionView with alot of UICollectionViewCells inside it.
Searched alot, but couldn't find anything near this issue.
My goal is to drag one of the Pink squares (UICollectionViewCell) to the Gray view (UIView).
You can think of it like dragging a folder on your desktop to your trash can except that I dont want the Pink square to be deleted. Or dragging a icon from your dock (it snaps back).
When the pink area hits the gray area it should just go back to its original position and the same when dropping out anywhere else using some kind of easy statement.
Can anyone help me out or redirect me to some references or samples?
Much appreciated, if you would like more info. Let me know.
If i managed to understand you correctly, i did something very similar once before, i found its much better to use a fake image of the cell, instead of the cell itself.
So for example, cell number 2 is starting to be dragged, you immediately call "SetHidden=YES", and draw at the exact same place a fake one with a simple draw method:
UIGraphicsBeginImageContext(self.cell.bounds.size);
[self.cell.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
This one you can move as you like, and when it should go back, you do it the same way.
Hope this helps you..
this worked for me in swift 3:
class DragAndDropGestureManager: NSObject {
var originalPosition: CGPoint?
var draggedView: UIView?
var viewSize: CGSize?
var originalZIndex: CGFloat = 0
#objc func handleDrag(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
if let view = recognizer.view {
draggedView = view
originalPosition = view.frame.origin
viewSize = view.frame.size
originalZIndex = view.layer.zPosition
view.layer.zPosition = 1
}
case .changed:
if let view = recognizer.view {
let currentPosition = recognizer.location(in: view.superview!)
let newPosition = CGPoint(x: currentPosition.x - (viewSize!.width / 2.0), y: currentPosition.y - (viewSize!.height / 2.0))
view.frame.origin = newPosition
} else {
self.finishDrag()
}
case .ended:
finishDrag()
default:
print("drag default \(recognizer.state.rawValue)")
}
}
func finishDrag() {
self.draggedView?.frame.origin = originalPosition!
self.draggedView?.layer.zPosition = originalZIndex
originalZIndex = 0
originalPosition = nil
draggedView = nil
}
}
i added a gesture recognizer in the function public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
like this:
if self.dragManager != nil && cell.gestureRecognizers == nil {
cell.addGestureRecognizer(UIPanGestureRecognizer(target: dragManager!, action: #selector(DragAndDropGestureManager.handleDrag)))
}
the if, is for not adding gesutre recognition more than once because the cell are being reused
don't forget to add collectioView.clipToBounds = false otherwise the dragged cell will be clipped when trying to move out of the collection view's bounds