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) {
}
Related
I have UITableViewCell which looks like CardView. Inside cell, I should display some dynamic content. That is why I have stackview inside my cell that will have my dynamic content. But the problem is that cellForRowAt method is called every time while scrolling and stackview starts having extra elements.
How should I solve this problem?
How I handled problem by myself?
I found prepareForReuse method of UITableViewCell which is called before re-configuring by cell. In that method, I clean my stackView. And now, my stackview will not have extra views. But, it is too bad for performance. The next way I tried is holding some flag inside my cell that tells me was stackview already configured. But, this approach didn't help (yes, it will not add extra elements, but content inside cells gets incorrect placement) as checking stackview length.
Here is my pseudo-code:
/// called in cellForRowAt
func configure(item: Item) {
item.forEach {
stackView.addArrangedSubview(ItemView(item))
}
}
func prepareForReuse() {
stackView.arrangedSubviews.forEach {
$0.removeFromSuperView()
}
}
If the prepareForReuse and dequeuing methods leads to exceeding 0.0167 sec (60 frames per second) then maybe in your edge case it will be better to create a cell instead of dequeuing it.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.configure(item: item)
return cell
}
I am working on a project and one of the table views the text for it is a UITextView, but on the other one is a UILabel. The UILabel detects the click from the user as a click on the table cell, but the UITextView doesn't. Why is this happening? Is there any way to fix it?
Try that
yourTextView.addTarget(self, action: #selector(myTargetFunction), for: .touchDown)
#objc func myTargetFunction(textField: UITextView) {
print("myTargetFunction")
}
Ensure the isSelectable property of your textView is true.
I figure it out, the cell view and text view are both scrow views and always will have conflict, so I need to uncheck the UserInteractions and Multiple Touch of the text view, booth set to false
I'm not sure from your question whether you're talking about selection on the UITableView row or the UITextView/UILabel itself.
If you're trying to set up a gesture recognizer on the UILabel or UITextView, I'm wondering if that's necessary or if you could just use didSelectRowAtIndexPath in your view controller.
e.g.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
yourMethodHere(for: cell)
}
Then you'd define in yourMethodHere() what behavior you want to happen when the cell is selected.
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.
I have some UITableViewCells in which I can perform click actions.
When a click action starts the cell will expand and a UIPickerView, UITextView or other data will be displayed. (See example images)
When you click the TableCell again the cell will collapse and the original state will be displayed.
Currently every cell expands when I click in a UITableViewCell with an expand action. When I click on a cell I want every other cell to be collapsed and only expand the clicked one.
Question:
How can I give my table cell, which is expanded, a state in which it will receive all touch events. (Become the first responder for the whole screen) and close it first and after the close action send the click event to the corresponding UITableViewCell.
I have made the UITextView first responder and that will close the keyboard after it's poped up, but I want the table cell to be the handler of the click events.
Example code
func togglePicker() {
//This function is called when the UITableCell is clicked.
canBecomeFirstResponder()
// Some code here which adds UIPickerView, UITextView or other data.
setNeedsLayout()
}
I tried this code, but this cell only receives touch events which are triggered in this cell and not outside its boundaries.
Example images
Orginal cell state
First cell is expanded
I implemented the following solution:
class CustomCell: UITableViewCell {
var delegate: CustomCellDelegate?
var focussed: Bool = false
function becomeFocussed() {
//Put some code here to change the design
setNeedsLayout()
focussed = true
delegate?.isFocussed(self)
}
function becomeNormaleState() {
//Put some code here to change the design to the original state.
focussed = false
setNeedsLayout()
}
}
Every Cell has two functions: becomeFocussed() and becomeNormaleState(). When the cell is clicked the function becomeFocussed() should be called.
This functions tells the delegate that the cell is selected.
In the TableViewController the function isFocussed(cell : UITableViewCell), loops through al cells of the current tableView en calls "becomeNormaleState()" for every cells which is focussed.
okay, by looking at the images u are changing the frame of the cell, one solution for When I click on a cell I want every other cell to be collapsed and only expand the clicked one. as u asked in the question, u can store the index path of the clicked cell and use this collapse other cell if that cell's picker view is showing for example,
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if showIndexPath?.row == indexPath.row
{
return 330 //expanded height for example
}
else {
return 100 //normal state height for example
}
}
//in this method u can decide which one to expand or collapse
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//if user selected the same cell again close it
if showIndexPath?.row == indexPath.row
{
//already expanded, collapse it
showIndexPath = nil
tableView .reloadRowsAtIndexPaths([showIndexPath!], withRowAnimation: .Fade)
}
else
{
//expand this cell's index path
showIndexPath = indexPath
tableView .reloadRowsAtIndexPaths([showIndexPath!], withRowAnimation: .Fade)
}
}
I have a UITableView with a custom cell, which has a few labels in it that dynamically decide the height of the cell. When I tap on one cell and segue to a new view controller, upon returning all the formatting for the cells is completely messed up, and I can't figure out what is causing it.
Here is what the cells normally look like:
And I have some pretty basic constraints set on them. The top label is pinned to the top and left margins, and must always be >= 20 from the right. The other labels are aligned to the left of this first label, with vertical spacing set between all of them. The middle label has a right spacing constraint to the margin, and the bottom labels are aligned to the baseline of the first and have horizontal spacing between all of them.
When I segue back to this table view it looks like this however:
I can't figure out what is causing it to layout differently than when I left. If I scroll around it seems to "reset" them back to what they should be, but on initial load they're really messed up. I can attach the project if desired, but there's really not much outside of the Storyboard.
cellForRowAtIndexPath:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as CustomTableViewCell
let object = objects[indexPath.row]
cell.title1.text = object.name
cell.title2.text = object.color
cell.title3.text = object.roar
return cell
}
Sample project: http://cl.ly/040L2z0q0V2d
It appears that the table view cells aren't resizing based on the contents when returning from the segue. Using the sample project, I threw a reload data in the viewWillAppear and that seemed to fix the issue.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.tableView.reloadData()
}
There are actually couple of issues with your project.
Data Loading and AutoLayout.
The first one is causing a strange behavior at the time of drawing the cells with data. When unwinding from the segue you'll see those additional cells on top of your table caused by ambiguous layout calculation.
Solution: Move the data into override func viewWillAppear(animated: Bool) { and perform a tableView.reloadData() (as correctly suggested by #rFessler).
On the other hand, Autolayout is a kind of fiery beast. Tamable. It's worth investigating the topic further. I wasn't able to make your layout work with autosizing cell height but I'll leave few references and the project for you.
References:
http://www.appcoda.com/self-sizing-cells/
http://captechconsulting.com/blog/tyler-tillage/ios-8-tutorial-series-auto-sizing-table-cells
Project:
http://cl.ly/3z3a2Z3a3U2K
I've had a similar problem myself. I downloaded your project and it seems I've solved it by removing and tweaking some constraints. This is how my constraints look now:
Also I've added this to viewDidLoad:
self.tableView.estimatedRowHeight = 120
self.tableView.rowHeight = UITableViewAutomaticDimension
I also added this to test delete:
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete
{
self.objects.removeAtIndex(indexPath.row)
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
}
Now you can even rotate the device and remove rows and it's all working splendid!
However, there's still problem if you push this view on a Navigation Controller (Which is what my problem was about in the beginning). See my storyboard below to get some funky labels:
To solve this, it seems we actually have to do a hack! (Damn you apple, what is going on with this?!)
var firstAppearance=true
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if firstAppearance
{
if let indexPaths = self.tableView.indexPathsForVisibleRows()
{
self.tableView.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
self.firstAppearance = false
}
}
}
At the moment, I think this is as good as it gets.
I played with this and find a simple solution, add this seems to fix the problem.
override func viewWillDisappear(animated:Bool) {
super.viewWillDisappear(animated)
self.tableView.estimatedRowHeight = 166.0
}
Since the the method tableView:estimatedHeightForRowAtIndexPath will be called every time you segue to a new MVC, and change the autolayout, you can just do
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
to reuse the autolayout