Scrolling bad practice for UICollectionViews - ios

I made a horizontal scrolling UICollectionView and I want the cell that's in the middle to have a white font while the rest is black.
If I just use scrollViewDidEndDecelerating highlighting of the middle cell seems to jump around more than if I use both scrollViewWillBeginDecelerating and scrollViewDidEndDecelerating to highlight the middle cell. Is this bad practice?
extension CurrencySelectorTableViewCell: UIScrollViewDelegate{
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
self.findCenterIndex()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.findCenterIndex()
}
}
This code btw still doesn't animate perfectly like I want it to so I'm open to any advice how to make this scrolling mechanism as smooth as possible.
When the UICollectionView thus starts scrolling this function is triggered:
func findCenterIndex() {
let center = self.convert(self.collectionView.center, to: self.collectionView)
let index = collectionView!.indexPathForItem(at: center)
if let selectedIndex = index {
self.selectedCell = selectedIndex.item
self.collectionView.reloadData()
}
}
Upon reloading the UICollectionView the label in the cell that is located in the middle will look different from the rest:
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CurrencySelectorCollectionViewCell", for: indexPath) as! CurrencySelectorCollectionViewCell
if (indexPath.item == self.selectedCell) {
cell.currencyLabel.textColor = UIColor.white
cell.currencyLabel.font = cell.currencyLabel.font.withSize(22)
} else {
cell.currencyLabel.textColor = UIColor.black
cell.currencyLabel.font = cell.currencyLabel.font.withSize(15)
}
cell.currencyLabel.text = currencies[indexPath.item]
return cell
}
Right now it jumps around a little bit because it will only change the label when the scrolling has just started or just stopped. I would like this effect on the UITextLabel to happen continuously throughout the scrolling process.

Try to add removeAllAnimations() of your UILabel layer before you fire off a new animation:
[view.layer removeAllAnimations];
EDIT:
Based on your edit in the question, you are not running any animation. You are calling reloadData on your UICollectionView, which is really bad practice.
You should just simple either:
1: (Bad option)
Reload the Cell only with performBatchUpdates(_:completion:)
2: Good option
Access the cell as a variable in your cell findCenterIndexwith cellForItem(at:) and simply just do your update to the label.
You can also deselect the other cells by getting an array of the visibleCells and simply just do as same described above, but you fire your "deselection" code instead. You could actually do this before you run your selection code. Or do everything in one action by Simply run a for loop on the visible cells and "deselect" them in your loop, and select the one in your CGPoint center.
This way, you never even have to reload your UICollectionView and is the best practice. And you also avoid flickers and animations.

Related

Buttons in a UICollectionView don't receive touch events

I've been looking around for a while now but can't seem to work out how to stop the collection view cells from consuming the touch events.
I need the touch events to be passed down into the respective cells so that the buttons within the cells can be pressed. I was thinking i might need to work out how to disable the UICollectionView's didSelectCellAtIndexFunction?
I've also seen this as a potential solution: collectionView.cancelsTouchesInView = false
Also this link might help someone answer my question: How to add tap gesture to UICollectionView , while maintaining cell selection?
Thanks in advance!
Edit:
Also I should add: my buttons are added to a view that is in turn added to the cell's contentView. My code is done all programatically and so I am not using interface Builder at all.
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return false // all cell items you do not want to be selectable
}
Assuming all buttons are connected to the same selector, you need a way to differentiate which cell's button has been clicked. One of the ways for finding out the button's cell's index is:
func buttonPressed(button: UIButton) {
let touchPoint = collectionView.convertPoint(.zero, fromView: button)
if let indexPath = collectionView.indexPathForItemAtPoint(touchPoint) {
// now you know indexPath. You can get data or cell from here.
}
}
Try on your cell.
self.contentView.isUserInteractionEnabled = false

iOS: knowing which UITableView Cell was tapped

I have looked around for this issue, and have not found an answer that works for my case. Basically, I have a tableView where each cell contains a collectionView. What I would like to do is to refresh the specific TableViewCell where the collectionView was scrolled, in order to update a label under the CollectionView, within that TableViewCell.
So when the user scrolls on the collectionView, I need to know in which cell that collectionView is. I have tried using didSelectRowAtIndexPath, however, it only is called when the non-CollectionView part of the cell is tapped. When the collectionView is tapped or scrolled, it isn't called. I am coding in Swift.
Any ideas on how I can do this?
Thanks!
This seems like a architecture issue. Sure it can be done the way you want, but it'd be much easier if your rearranged some things. There is a fundamental problem how you want to do this. You want to manage all of the cells and their collection views directly from your view controller. But this poses the problem of needing to know where messages are coming from and directing messages back to the correct cells and collection views. This will create a lot of bloat that can be fixed with a simple UITabelViewCell subclass. It also is a step in contracting Massive View Controller syndrome. Instead, you should make the individual cells responsible for managing their own collection views.
First off, make the UITableViewCell own and be the delegate and data source of the UICollectionView. This centralizes the data and more closely models the tree of data that you actually see on screen.
class CollectionTableViewCell: UITableViewCell {
#IBOutlet var collectionView: UICollectionView! {
didSet(newCollectionView) {
newCollectionView.delegate = self;
newCollectionView.dataSource = self;
}
}
var model: NSArray?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
extension CollectionTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return model?.count ?? 0
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
// configure cell
return cell
}
}
Now you wanted to have a state refresh when your collection view scrolls. So we're going to add a label to the cell in the prototype cell (Nib or Storyboard.)
#IBOutlet var indicatorLabel: UILabel!
And you want to update it when the collection view is scrolled. We can do that with the scrollViewDidScroll method off the UIScrollViewDelegate protocol. Because UICollectionViewDelegate implements the UIScrollViewDelegate, it's available for us to use since we implement the UICollectionViewDelegate in our extension.
// in the CollectionTableViewCell extension...
func scrollViewDidScroll(scrollView: UIScrollView) {
let scrollViewOffset = scrollView.contentOffset.x
let scrollViewWidth = CGRectGetWidth(scrollView.frame)
let completionString = String(format: "%# / %#", scrollViewOffset, scrollViewWidth)
self.indicatorLabel.text = completionString
}
So by making the individual cells responsible for their own respected collection views, we make managing them easier. It allows us organize our code to be more compact, understandable, and keeps us from getting Massive View Controller syndrome. The more code you can move out of your view controller, the better.
Some good talks to hear on this would be:
WWDC 2014 – Advanced iOS Application Architecture and Patterns
Let's Play: Refactor the Mega Controller!
You can use tag property of UITableViewCell. Set tag to row number and when cell is tapped, fetch tag property to find out the tapped cell index.
Your CollectionView will be contained within some kind of cell. Once you have found this cell, you can ask the table for the index. Navigate from your CollectionView up the view hierarchy to find the cell. For example:
CollectionView* collectionView = // your collectionView;
UITableViewCell* cell = (UITableViewCell*)[collectionView superview];
UITableView* table = (UITableView *)[cell superview];
NSIndexPath* pathOfTheCell = [table indexPathForCell:cell];
NSInteger rowOfTheCell = [pathOfTheCell row];
NSLog(#"rowofthecell %d", rowOfTheCell);

Change label height of custom cell within didSelectRowAtIndexPath

I'm trying to resize a label's height when my custom cell is selected (to allow it to expand so more text is visible).
I'm obviously missing something fundamental though because the frame is the exact same after I try to draw the new CGRect.
Here's the relevant code:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow()
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! BasicCell
UIView.animateWithDuration(0.3) {
tableView.beginUpdates()
currentCell.subtitleLabel!.frame = CGRectMake(currentCell.subtitleLabel!.frame.origin.x, currentCell.subtitleLabel!.frame.origin.y, currentCell.subtitleLabel!.frame.size.width, 100)
currentCell.subtitleLabel!.numberOfLines = 0
tableView.endUpdates()
}
}
Any help would be appreciated.
You should use
-reloadRowsAtIndexPaths:withRowAnimation:
inside beginUpdate and endUpdate pair. Simply setting the frame is not enough for table view to know which cell needs to be updated.
Also there's no need to put your update code inside animation block. It's animated inherently.

SelectedBackgroundView of UICollectionViewCell visible when it should not be

I've got a UICollectionView. With some cells inside with a white background color. I've set the selectedBackgroundView to a basic purple view.
My CollectionView has a constraint with a height of 0 and when I hit a button I update the constraint to 80. When I'm doing that, during the animation i can see the purple background on the screen until the end on the animation and i cannot understand why or how prevent this ?
Everything else working fine, it's just a "visual" bug.
Any suggestion about how to fix this ?
Gif of the bug where you can see the purple appearing during the animation
Here is my cell construction if it can be of any help :
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("WidgetMenuCellIdentifier", forIndexPath: indexPath) as UICollectionViewCell
cell.removeSubviews()
// some code setup
cell.selectedBackgroundView = UIView()
cell.selectedBackgroundView.backgroundColor = UIColor.purpleColor()
return cell
}
Subclass your UICollectionViewCell
Do
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// your code
cell.selectedBackgroundView.hidden = true
return cell
}
Then in your subclass :
override var selected:Bool {
willSet {
self.selectedBackgroundView.hidden = false
}
}
It should work.
It seems like this code is being executed within an animation, causing unexpected behavior at times based on how various properties animate. Another complicating factor is that, because cells are reused, it won't reproduce if a reused cell is already configured correctly (i.e. there is nothing to animate). Adding the following after styling the selectedBackgroundView was the least hacky solution I could think of.
[cell.selectedBackgroundView.layer removeAllAnimations];
Depending on what your cells are like you may also want to consider removing animations on other layers as well. For example:
[cell.backgroundView.layer removeAllAnimations];

Reordering UITableView without reorder control

I need the user to be able to reorder a UITableView by this way: he touches a cell for a predetermined period (e.g. 1 second), then he can drag and drop it over the other cells.
I know how to implement the 'long touch' detection using a gesture recognizer, but what is the best way to implement the drag and drop ability without using a reorder control (the user should drag the cell from anywhere in the cell, not only from the reorder control)?
This is an old question, but here's a solution that's tested and working with iOS 8 through 11.
In your UITableViewCell subclass try this:
class MyTableViewCell: UITableViewCell {
weak var reorderControl: UIView?
override func layoutSubviews() {
super.layoutSubviews()
// Make the cell's `contentView` as big as the entire cell.
contentView.frame = bounds
// Make the reorder control as big as the entire cell
// so you can drag from everywhere inside the cell.
reorderControl?.frame = bounds
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: false)
if !editing || reorderControl != nil {
return
}
// Find the reorder control in the cell's subviews.
for view in subviews {
let className = String(describing: type(of:view))
if className == "UITableViewCellReorderControl" {
// Remove its subviews so that they don't mess up
// your own content's appearance.
for subview in view.subviews {
subview.removeFromSuperview()
}
// Keep a weak reference to it for `layoutSubviews()`.
reorderControl = view
break
}
}
}
}
It's close to Senseful's first suggestion but the article he references no longer seems to work.
What you do, is make the reorder control and the cell's content view as big as the whole cell when it's being edited. That way you can drag from anywhere within the cell and your content takes up the entire space, as if the cell was not being edited at all.
The most important downside to this, is that you are altering the system's cell view-structure and referencing a private class (UITableViewCellReorderControl). It seems to be working properly for all latest iOS versions, but you have to make sure it's still valid every time a new OS comes out.
I solved the question of the following steps:
Attach gesture recognizer to UITableView.
Detect which cell was tapped by "long touch". At this moment create a snapshot of selected cell, put it to UIImageView and place it on the UITableView. UIImageView's coordinates should math selected cell relative to UITableView (snapshot of selected cell should overlay selected cell).
Store index of selected cell, delete selected cell and reload UITableView.
Disable scrolling for UITableView. Now you need to change frame of snapshot UIImageView when you will drag cell. You can do it in touchesMoved method.
Create new cell and reload UITableView (you already have stored index) when the user finger leaves screen.
Remove the snapshot UIImageView.
But it was not easy to do it.
The article Reordering a UITableViewCell from any touch point discusses this exact scenario.
Essentially you do the following:
Find the UITableViewCellReorderControl (a private class).
Expand it so it spans the entire cell.
Hide it.
The user will now be able to drag the cell from anywhere.
Another solution, Cookbook: Moving Table View Cells with a Long Press Gesture, achieves the same effect by doing the following:
Add a long press gesture recognizer on the table view.
Create a snapshot of the cell when the cell is dragged.
As the cell is dragged, move the snapshot around, and call the -[UITableView moveRowAtIndexPath:toIndexPath:].
When the gesture ends, hide the cell snapshot.
For future reference...
I had the same problem, I found another question(Swift - Drag And Drop TableViewCell with Long Gesture Recognizer) about it and someone suggested this tutorial: https://www.freshconsulting.com/create-drag-and-drop-uitableview-swift/
worked just perfectly for me
I know this is a question about UITableView. But I ended with a solution of using UICollectionView rather than UITableView to implement longtap reorder. Its easy and simple.
tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
tableView.dropDelegate = self
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { }
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession,
at: indexPath: IndexPath) -> [UIDragItem] {
return [UIDragItem(itemProvider: NSItemProvider())]
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session:
UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if session.localDragSession != nil {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UITableViewDropProposal(operation: .cancel, intent: .unspecified)
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
}

Resources