UIContextualAction delete row issue. UIContextualAction's menu overlaps the cell - ios

I have two UIContextualActions which should delete rows from the table after completion. I faced the following intermittent issue:
If I apply this actions very fast one by one, then the first action works fine, but when I call UIContextualAction's menu for the second time it overlaps the cell. In the third time, it is not possible to call UIContextualAction's menu until tableView is scrolled. Here the short video.
Please find below the piece of code
//MARK: Swipe actions
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = self.contextualDeleteAction(forRowAtIndexPath: indexPath)
let markAsReadAction = self.contextualMarkAsReadAction(forRowAtIndexPath: indexPath)
let markAsUnreadAction = self.contextualMarkAsUnreadAction(forRowAtIndexPath: indexPath)
let swipeConfig = UISwipeActionsConfiguration(actions: [
isArchiveModeEnabled ? markAsUnreadAction : markAsReadAction,
deleteAction
])
swipeConfig.performsFirstActionWithFullSwipe = false
return swipeConfig
}
func contextualMarkAsUnreadAction(forRowAtIndexPath indexPath: IndexPath) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "") { _, _, completion in
let index = indexPath.row
if let article = self.getSelectedArticleByIndex(index) {
self.articlesManager.restore(article: article)
self.refreshDataSource()
self.tableView.deleteRows(at: [indexPath], with: .automatic)
self.tableView.isEditing = false //seems like it helps to fix visual bug when the action is left on blank space
completion(true)
}
}
action.backgroundColor = UIColor(patternImage: swipeActinImages.markAsUnread)
return action
}
refreshDataSource function do the following:
private func refreshDataSource() {
articles = isArchiveModeEnabled ? dataController.getArchivedArticles() : dataController.getUnreadArticles()
}
private func reloadTableData() {
if dataController.getActiveUser() != nil {
refreshDataSource()
introView?.isHidden = articles.count != 0 || isArchiveModeEnabled
tableView.isScrollEnabled = (introView?.isHidden)!
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Appreciate any help
UPDATE
So I solved it. I've replaced UIContextualAction(style: .normal, title: "") to UIContextualAction(style: .destructive, title: "") and don't update datasource manualy anymore. Completion(true) do all needed manipulation with cell and table
automaticaly.
The final code is
func contextualMarkAsUnreadAction(forRowAtIndexPath indexPath: IndexPath) -> UIContextualAction {
let action = UIContextualAction(style: .destructive, title: "") { _, _, completion in
let index = indexPath.row
if let article = self.getSelectedArticleByIndex(index) {
self.articlesManager.restore(article: article)
//self.refreshDataSource()
completion(true)
} else {
completion(false)
}
}
action.backgroundColor = UIColor(patternImage: swipeActinImages.markAsUnread)
return action
}

Related

How to add accessibility ID to Swipe action configuration in table view row for Automation purpose

I am trying to add accessibility ID to 2 buttons(Delete, Add) which is present in table view row when swipe left. The accessibilityIdentifier is not auto populating for deleteAction when I try to add it. Can somebody take a look.Thank you in advance
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
var contextualAction: [UIContextualAction] = []
// 1st button
let deleteAction = UIContextualAction(style: .normal , title: "DELETE") {
(action, view, handler) in
// code to remove item
}
contextualAction.append(deleteAction)
// 2nd button
let addAction = UIContextualAction(style: .normal , title: "Add") { (action,
view, handler) in
// code to add item
}
contextualAction.append(addAction)
let swipeAction = UISwipeActionsConfiguration(actions: contextualAction)
swipeAction.performsFirstActionWithFullSwipe = false
return swipeAction
}
Here you are
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
tableView.subviews.forEach { tableSubview in
if NSStringFromClass(type(of: tableSubview)) == "_UITableViewCellSwipeContainerView" {
tableSubview.subviews.forEach { swipeContainerSubview in
if NSStringFromClass(type(of: swipeContainerSubview)) == "UISwipeActionPullView" {
swipeContainerSubview.subviews.enumerated().forEach { index, view in
// index 0 => your Delete Button
// index 1 => your Add Button
if let button = view as? UIButton {
button.accessibilityIdentifier = ""
button.titleLabel?.accessibilityIdentifier = ""
}
}
}
}
}
}
}

Swipe Actions are not working on Section header

I am using swift 4.2 and xcode 10.1. I am working on a project and Created some fancy expand/Collapsable UITableView.
Everything is working just fine and awesome. Then there comes some need of introducing some swipe actions. So it looks so easy to create them here is how I am using and creating them
#available(iOS 11.0, *)
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let deleteAction = UIContextualAction(style: .destructive, title: "Add") { (action, view, handler) in
print("Add Action Tapped")
}
deleteAction.backgroundColor = .green
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
return configuration
}
#available(iOS 11.0, *)
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { (action, view, handler) in
print("Delete Action Tapped")
}
deleteAction.backgroundColor = .red
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
return configuration
}
But that is not working on my sections. The row gets swipable actions when I swipe them left or right. but No on my section headers. Here is how I am creating view for my section header.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = Bundle.main.loadNibNamed("HeaderCell", owner: self, options: nil)?.first as! HeaderCell
let shoppingList = mDataSource[section]
let itemCount = shoppingList.ShoppingItems.count
let txtStoreName = shoppingList.Store.P_FriendlyName! + " (\(itemCount))"
cell.lblName.text = txtStoreName
let imageHollow = UIImage(named: "checkbox_hollow") // For List Item Check box image
cell.btnSelectHeader.setImage(imageHollow, for: .normal)
if(shoppingList.IsSelected){
let imageChecked = UIImage(named: "checkbox_checked") // For List Item Check box image
cell.btnSelectHeader.setImage(imageChecked, for: .normal)
}
if(shoppingList.IsExpanded){
let image = UIImage(named: "ic-indicator-up")
cell.ivIndicatorExpandCollapse.image = image
cell.viewContentView.backgroundColor = CommonUtils.hexStringToUIColor(hex: AppColors.colorItemSelector)
}else{
let image = UIImage(named: "ic-indicator-down")
cell.ivIndicatorExpandCollapse.image = image
cell.viewContentView.backgroundColor = CommonUtils.hexStringToUIColor(hex: AppColors.colorWhite)
}
cell.backgroundView?.backgroundColor = UIColor.white
cell.isCellSwipable = IS_CELL_RIGHT_SWIPEABLE
cell.delegateHeader = self
cell.clickedSection = IndexPath.init(row: 0, section: section)
cell.clickedModel = shoppingList
return cell
}
Problems: here are my problems and questions
Section headers are not showing swipe actions as other cells are showing.
Are these swipe actions are available from iOS 11.0 up to latest or it will only work on iOS 11 only?
Please help me I am not sure why it is not working on section headers????
As of now, iOS does not allow to swipe section headers. I would like to recommend use your own logic. You can manage by changing view constraints (default view or swiped view) yourself for section header.
func sectionSwiped(gestureRecognizer: UITapGestureRecognizer) {
if let viewSwiped = gestureRecognizer.view as? UIView{
swipedSection = viewSwiped.tag
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let vw = UIView()
vw.tag = section
//You can customize view
var swipeRight = UISwipeGestureRecognizer(target: self, action: "sectionSwiped:")
swipeRight.direction = UISwipeGestureRecognizerDirection.Right
vw.addGestureRecognizer(swipeRight)
return vw
}

Swiping on a tableview is deselecting all selected rows trailingSwipeActionsConfigurationForRowAt

Recently implemented trailingSwipeActionsConfigurationForRowAt , where after swiping from right to left showing two options and its working fine. But the problem is when i select multiple rows or single row, after swiping the row/s they are getting deselected. Is there a way to keep the selection even after swiping?
Below is my code
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let renameAction = contextualToggleRenameAction(forRowAtIndexPath: indexPath)
let lastResAction = contextualToggleLastResponseAction(forRowAtIndexPath: indexPath)
let swipeConfig = UISwipeActionsConfiguration(actions: [renameAction, lastResAction])
swipeConfig.performsFirstActionWithFullSwipe = false
return swipeConfig
}
func contextualToggleLastResponseAction(forRowAtIndexPath indexPath: IndexPath) -> UIContextualAction {
let sensorData = sensorsList?[indexPath.row]
var lastResponse = ""
if sensorData != nil{
if let lstRes = sensorData!["last_response"] as? String{
lastResponse = lstRes
}
}
let action = UIContextualAction(style: .normal, title: lastResponse) { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
print("Last Response Action")
}
action.backgroundColor = UIColor(red: 61/255, green: 108/255, blue: 169/255, alpha: 1.0)
return action
}
Holy crap, I fixed this stupid issue.
Yes, yes make sure tableView.allowsSelectionDuringEditing = true and tableView.allowsMultipleSelectionDuringEditing = true.
But...
Shout-out to this SO answer which almost directly led me on the way to success (see here).
KEY: Re-select/de-select the rows in your implementation of setEditing, which is a method you override. UITableView goes into editing mode when swiping.
IMPORTANT NOTE: Call super.setEditing(...) before any of your code, as shown below, or it likely won't work, or at least not perfectly.
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
for i in 0..<your data.count {
if this item is selected {
self.tableView.selectRow(at: IndexPath(row: i, section: 0), animated: false, scrollPosition: .none)
} else {
self.tableView.deselectRow(at: IndexPath(row: i, section: 0), animated: false)
}
}
}
A swipe is considered as an edit, so you can enable allowsSelectionDuringEditing if you want to keep the selected state:
tableView.allowsSelectionDuringEditing = true
Depending on indexPathsForSelectedRows doesn't always give the expected result.
Instead you should maintain and array of selectedIndexPaths.
Here is a code snippet to demonstrate:
var selectedIndexPaths = [IndexPath]()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if selectedIndexPaths.contains(indexPath) {
selectedIndexPaths.removeAll { (ip) -> Bool in
return ip == indexPath
}else{
selectedIndexPaths.append(indexPath)
}
}
You can simply do this :
Create array in your controller for selected index
var arrSelectedIndex : [Int] = []
In didSelect,
if arrSelectedIndex.contains(indexPath.row) { // Check index is selected or not
// If index selected, remove index from array
let aIndex = arrSelectedIndex.firstIndex(of: indexPath.row)
arrSelectedIndex.remove(at: aIndex!)
}
else {
// If index not selected, add index to array
arrSelectedIndex.append(indexPath.row)
}
// reload selected row, or reloadData()
self.tblView.reloadRows([indexPath.row], with: .automatic)
Edit
In trailingSwipeActionsConfigurationForRowAt,
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
if self.arrSelectedIndex.contains(indexPath.row) {
let action = UIContextualAction(style: .normal, title: "") { (action, view, handler) in
}
action.backgroundColor = .green
let configuration = UISwipeActionsConfiguration(actions: [])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
let action = UIContextualAction(style: .normal, title: "Selected") { (action, view, handler) in
}
action.backgroundColor = .green
let configuration = UISwipeActionsConfiguration(actions: [action])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
Output

tableview cell action issue xcode

So I am creating this todo app. It is on a tableview. And each cell when tapped or clicked should take you to a ask.com search to search for the item if it is not clear what the Item is. I have gotten it to search on ask.com with the code I have written. But the issue that I have coming up is that after the first click. The page doesn't refresh or update. I can click on the second or third cell and it wont search for what is in that particular cell. It keeps showing what is in the first cell. and won't change. I have tried clearing cells and it still keeps going through as the old search from the first time. Ex: Cell 1 : cleaning products Cell 2: a bike Cell 3: dog. No matter what cell I pick it will only show cleaning product. Even if I change cell 1 to another item. How can I fix this. Source code would be amazing.
import UIKit
class NewTableViewController: UITableViewController, NewCellDelegate, {
var news:[News]!
override func viewDidLoad() {
super.viewDidLoad()
loadData()
func loadData() {
news = [News]()
news = DataManager.loadAll(News.self).sorted(by: {$0.createdAt < $1.createdAt})
self.tableView.reloadData()
}
#IBAction func Save(_ sender: Any) {
let addAlert = UIAlertController(title: "ADD", message: "TODO", preferredStyle: .alert)
addAlert.addTextField { (textfield:UITextField) in
textfield.placeholder = "TODO"
}
addAlert.addAction(UIAlertAction(title: "Save", style: .default, handler: { (action:UIAlertAction) in
guard let title = addAlert.textFields?.first?.text else {return}
let newsave = News(title: title, completed: false, createdAt: Date(), itemIdentifier: UUID())
newsave.saveItem()
self.news.append(newsave)
let indexPath = IndexPath(row: self.tableView.numberOfRows(inSection: 0), section: 0)
self.tableView.insertRows(at: [indexPath], with: .automatic)
}))
addAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(addAlert, animated: true, completion: nil)
}
};
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return news.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! NewTableViewCell
cell.delegte = self
let news = self.news[indexPath.row]
cell.label.text = news.title
return cell
}
func tableView(tableView: UITableView, didSelectRowAt indexPath:
NSIndexPath) {
//getting the index path of selected row
let indexPath = tableView.indexPathForSelectedRow
//getting the current cell from the index path
let currentCell = tableView.cellForRow(at: indexPath!)! as UITableViewCell
//getting the text of that cell
let TODO = currentCell.textLabel!.text
let appURL = NSURL(string: "https://www.ask.com/web?q=\
(TODO))&o=0&qo=homepageSearchBox)")
if UIApplication.shared.canOpenURL(appURL! as URL) {
if #available(iOS 10.0, *) {
UIApplication.shared.open(appURL! as URL, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(appURL! as URL)
}
}
}
}
I think it relates to search content change this
let currentCell = tableView.cellForRow(at: indexPath!)! as UITableViewCell
to
let currentCell = tableView.cellForRow(at: indexPath!) as! NewTableViewCell
&&& change this
let TODO = currentCell.textLabel!.text
to
let TODO = currentCell.label.text

How to delete UITableViewCell with swipe-to-dismiss with fade effect and no red delete button?

After looking into a myriad of StackOverflow posts, nothing really answers how to delete a UITableViewCell with swipe-to-dismiss while fading and without the red delete button.
My Tableviewcell looks like a card, so the red frame of the delete button breaks the sense of continuity and elevation of these cells with card shapes.
Here is the code I am currently using to delete, which does not fade despite the .fade on the UITableViewRowAnimation.
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return .none
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
self.pastOrders.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
Here's a screenshot of the behavior I am trying to achieve:
Output 3
//TO CHANGE "DELETE" TITLE COLOR
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let toDelete = UITableViewRowAction(style: .normal, title: "") { (action, indexPath) in
print("\n\n Delete item at indexPathDelete item at indexPath")
}
let deleteTextImg = swipeCellButtons(labelText: "Delete", textColor: UIColor.darkGray, alphaVal: 1.0)
toDelete.backgroundColor = UIColor(patternImage: deleteTextImg)
return [toDelete]
}
func swipeCellButtons(labelText : String, textColor: UIColor, alphaVal: CGFloat) -> UIImage
{
let commonWid : CGFloat = 40
let commonHei : CGFloat = 70 // ROW HEIGHT
let label = UILabel(frame: CGRect(x: 0, y: 0, width: commonWid, height: commonHei))
label.text = labelText
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 11)
label.textColor = textColor.withAlphaComponent(alphaVal)
UIGraphicsBeginImageContextWithOptions(CGSize(width: self.view.frame.width, height: commonHei), false, UIScreen.main.scale)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(UIColor.clear.cgColor) // YOU CAN GIVE YOUR BGCOLOR FOR DELETE BUTTON
context!.fill(CGRect(x: 0, y: 0, width: (self.view.frame.width) / 3, height: commonHei))
label.layer.render(in: context!)
//If you want to add image instead of text, uncomment below lines.
//Then, comment this "label.layer.render(in: context!)" line
//var img: UIImage = UIImage(named: "deleteIcon")!
//img.draw(in: CGRect(x: 0, y: 0, width: 30, height: 30))
let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return newImage
}
Output 2:
// INSIDE CELL FOR ROW AT INDEXPATH
// COMMENT THIS LINE
//cell.addGestureRecognizer(swipeGesture)
// CELL FADE WILL NOT WORK HERE
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let toDelete = UITableViewRowAction(style: .normal, title: " ") { (action, indexPath) in
print("\n\n Delete item at indexPathDelete item at indexPath")
}
toDelete.backgroundColor = .white
return [toDelete]
}
Output 1:
// GLOBAL DECLARATION
var gotCell : DefaultTableViewCell?
var alphaValue : CGFloat = 1.0
var deletingRowIndPath = IndexPath()
// INSIDE CELL FOR ROW AT INDEXPATH
//let cell = tableView.dequeueReusableCell(withIdentifier: "default", for: indexPath) as! DefaultTableViewCell
let cell = DefaultTableViewCell() // Add this line and comment above line. The issue is `dequeuingreusingcell`. In this method, it will stop dequeuing. But, we have to customise `UITableViewCell` in coding.
let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(handleSwipe))
swipeGesture.delegate = self
cell.addGestureRecognizer(swipeGesture)
func handleSwipe(panGesture: UIPanGestureRecognizer) {
if panGesture.state == UIGestureRecognizerState.began {
let cellPosition = panGesture.view?.convert(CGPoint.zero, to: defTblVw)
let indPath = defTblVw.indexPathForRow(at: cellPosition!)
deletingRowIndPath = indPath!
gotCell = defTblVw.cellForRow(at: indPath!) as! DefaultTableViewCell
}
if panGesture.state == UIGestureRecognizerState.changed
{
let isLeftMoving = panGesture.isLeft(theViewYouArePassing: (gotCell)!)
if isLeftMoving == true
{
self.gotCell?.alpha = self.alphaValue
self.gotCell?.frame.origin.x = (self.gotCell?.frame.origin.x)! - 2.5
self.view.layoutIfNeeded()
self.alphaValue = self.alphaValue - 0.005
}
else // ADD THIS ELSE CASE
{
self.alphaValue = 1.0
self.gotCell?.alpha = 1.0
UIView.animate(withDuration: 0.8, animations: {
self.gotCell?.frame.origin.x = 0
self.view.layoutIfNeeded()
}) { (value) in
}
}
}
if panGesture.state == UIGestureRecognizerState.ended
{
self.alphaValue = 1.0
if (self.gotCell?.frame.origin.x)! < CGFloat(-(defTblVw.frame.size.width - 90))
{
myArr.remove(at: (deletingRowIndPath.row))
defTblVw.beginUpdates()
defTblVw.deleteRows(at: [deletingRowIndPath], with: UITableViewRowAnimation.fade)
defTblVw.endUpdates()
}
else
{
UIView.animate(withDuration: 0.8, animations: {
self.gotCell?.alpha = 1.0
self.gotCell?.frame.origin.x = 0
self.view.layoutIfNeeded()
}) { (value) in
}
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
extension UIPanGestureRecognizer {
func isLeft(theViewYouArePassing: UIView) -> Bool {
let velocityVal : CGPoint = velocity(in: theViewYouArePassing)
if velocityVal.x >= 0 {
return false
}
else
{
print("Gesture went other")
return true
}
}
}
=============================
I guess SwipeCellKit pod is an option as well to do swiping without delete button, so please check out this link: https://github.com/SwipeCellKit/SwipeCellKit.
There is all documentation how you can customize it and if you can see the Destructive gif on the link, it is what you wanted, however you have to make it custom so there is no other buttons nor the delete button as well.
I hope it helped you somehow.
You can use this library with .exit mode
and In your cellForRow
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SwipyCell //User You cell identifier and class of cell here
let checkView = viewWithImageName("check") //Use some white dummy image
cell.addSwipeTrigger(forState: .state(0, .left), withMode: .exit, swipeView: checkView, swipeColor: tableView.backgroundView?.backgroundColor, completion: { cell, trigger, state, mode in
print("Did swipe \"Checkmark\" cell")
})
return cell
}
Hope this will help you
Try this code.
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let toDelete = UITableViewRowAction(style: .normal, title: " ") { (action, indexPath) in
self.rows.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
toDelete.backgroundColor = .white
return [toDelete]
}
Hope it would help you.
You can animate the content view and on completion of animation you can delete the cell
You can add a swipe gesture on the content of your custom cell, when the swipe animation is over you call a delegate method to the ViewController in which it will update the data array delete the tableView row and reload the tableView.

Resources