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)
}
}
Related
I currently have a table view controller which consist of not only the cells but also a UIView. Now, within that UIView there's a label which might have more than 1 line of text with a See More button. When I pressed that button, the button itself will disappear and the text.numberOfLines is set to 0 so the View should expand to show all text. Which doesn't seems to work in my case, The button does disappear though and the text just continues to the edge of the screen, truncated instead of extending down.
But when this whole UIView is outside the table view controller the functions above work just as expected, but not after I've moved them to within the table View right above the prototype cells. Any Ideas?
When you are setting up the tableviewcell, you have to specify a fixed height for the row with the delegate function
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) {
if expandedArray[indexPath.row] {
return 60
} else {
return UITableViewAutomaticDimension
}
}
Then keep an array of bools, to specify if the cell is expanded or not. Primarily set the bools to false indicating "not expanded".
Then when the use presses the "see more" button, make the bool in the array with the right index true, indicating that the cell is expanded. and call the below code updating the cell.
tableView.beginUpdates()
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
tableView.endUpdates()
That should do the trick.
P.S: Don't forget to properly add constraints inside tableview cell.
Use UITableViewAutomaticDimension and reload your cell on click of see more button.
I'm creating a form and whenever a user forgets to fill out a form field, I have a mechanism to highlight the required text labels for any of the cells in which the user did not select a value in. After the color change is reflected and the tableView is reloaded, I want to animate the highlighted label(s) also. What is the proper way to achieve this? The tableView can load a mixture of custom tableViewCells, so I want to know this can be done across all the custom cells.
Please note: Labels are highlighted in red if missing on "Submit" button click. As the missing fields are filled out, the labels are no longer highlighted as soon as they have a value assigned to that field. So now I need a way to animate the individual labels on the cells that are of different custom cell classes only when the user attempts to click the submit button with missing fields
Protocols
You can create a protocol that your custom cells will implement, so in each custom cell you can make your own implementation of a function that you will call in the delegate method from the table view:
tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
guard let customCellThatImplementsFormCell = cell as? FormCell else { return }
/// check if has errors and call either function
}
depending on if your cell has an error or not
this is a very simple idea but it could look like this
protocol FormCell {
func showError()
func hideError()
}
class FormTextInputCell: UITableViewCell {
}
extension FormTextInputCell: FormCell {
func showError() {
// Style the cell and animate what you want
}
func hideError() {
// Style the cell and animate what you want
}
}
I currently have a TableView in my project, which is set up to turn a cell green when it is pressed, and back to clear if it is pressed a second time. However, if I scroll down to the bottom of the table view, and scroll back up, all my cells have been reset to their default clear colour.
I'm not sure how to go about fixing this issue, as anything I can find referring to it is in Objective-C rather than Swift. Any help and advice as to how to go about this would be great, thanks.
Everytime a UITableViewCell goes out of the screen, any function that you've written in the tableViewController/ViewController runs again.
for example in cellForRowAtIndexPath if you have a cell.setUpCell() or something similar, it will rerun and reset your values to the original values.
if you have a
var name = testName in your MainVC
and you update something in your cell, you should change the name in your mainVc too.
Every time you scroll or call tableView.reloadData() UITableView cells will reload. So, every time you select UITableViewCell, add selected index (indexPath.row) to an array(ex: selectedIndexArray) in your didSelectRowAt indexPath: delegate. If the cell you selected is already selected one, then remove the cell from selectedIndexArray.
And in your cellForRowAt indexPath: manage the cells using selectedIndexArray.
var selectedIndexArray:[Int] = [] //to save selected tableViewCells
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let isSelected = false
for each in selectedIndexArray
{
if each == indexPath.row
{
isSelected = true
}
}
if isSelected == true
{
//set selected cell color
}
else
{
//set default cell color
}
}
You need to write the logic of adding and removing cell indexes in your didSelectRowAt indexPath:.
I am trying to make a '''Static Table''' with some cells that are expandable and collapsing . I override the ''': WithAnimation:)''' which should provide me some control on the animation transition. Unfortunately, the cells returned are blank, I don't mean empty, but the cells have disappeared and their location is left blanc in the table. When I try using the '''reloadSections( )''' rather, the whole section is also returned blank. After some recherche , I start to suspect that '''reloadRowAtIndexPath(: WithAnimation:)''' and '''reloadSections( )''' works with table associated with a data source.
Am I right? If yes, how can I control animation of the cell I want to hide/unhide so that the transition is smooth?
Thanks (edited)
To get a smooth animation, simply reload the tableView with
tableView.beginUpdates()
tableView.endUpdates()
this will call the heightForRowAtIndexPath automatically and render a smooth transition.
To expand on irkinosor's answer. In this example I have 1 section with 3 static rows. When tableView is initially loaded, my UISwitch is off and I only want the first row (row 0) be visible (hide rows 1,2). When the UISwitch is on, I want to show (rows 1,2). Also, I want smooth animation having the rows accordion to hide or show.
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row > 0 && toggleSwitch.isOn == false {
return 0.0 // collapsed
}
// expanded with row height of parent
return super.tableView(tableView, heightForRowAt: indexPath)
}
In my switch action I initiate the tableView refresh
#IBAction func reminderSwitch(_ sender: UISwitch) {
// to initiate smooth animation
tableView.beginUpdates()
tableView.endUpdates()
}
You can obviously add more logic to the heightForRowAt function if you have multiple sections and more interesting row requirements.
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) {
}