I have an app with multiple view controllers with a tableview in each of them. Each view controller's table have their own specific purpose, but I use the same swipe actions in each table. So far I have just been copying and pasting the same code for trailingSwipeActionsConfigurationForRowAt and leadingSwipeActionsConfigurationForRowAt in each table. This feels wrong and I know there should be a way where I can write the code once and use it throughout the app but I am not sure how to do this.
I have tried to extensions of UITableView but can not seem to use the trailingSwipeActionsConfigurationForRowAt or leadingSwipeActionsConfigurationForRowAt functions in the extension file. What am I missing?
Here's the code for trailingSwipeActionsConfigurationForRowAt which is just swipe to delete:
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(style: .destructive, title: "") { (action, view, completionHandler) in
if let item = self.dataSource.itemIdentifier(for: indexPath) {
CoreDataManager.sharedManager.deleteItem(item)
}
completionHandler(true)
}
action.image = UIImage(named: "deleteSymbol")
let configuration = UISwipeActionsConfiguration(actions: [action])
return configuration
}
you declare below function as global function for the project and access any where you want
You can modify function by adding parameter which you want to use when button clicked. just simply add parameter in function and pass data when you call
func getSwipeAction(indexpath : IndexPath)-> UISwipeActionsConfiguration{
let action = UIContextualAction(style: .destructive, title: "") { (action,
view, completionHandler) in
completionHandler(true)
}
action.image = UIImage(named: "deleteSymbol")
let configuration = UISwipeActionsConfiguration(actions: [action])
return configuration
}
You can add these inner code in UITableView extension function and try to use it like that. You have to implement trailingSwipeActionsConfigurationForRowAt or leadingSwipeActionsConfigurationForRowAt in each of your view controllers.
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return self.YourFunctionNameInExtension()
}
You can define a protocol like BaseTableViewProtocol and in this protocol's extension you can write your code. So when you extend your tableView class from this protocol you can use the function. For example,
protocol BaseTableViewProtocol { }
extension BaseTableViewProtocol {
func trailingSwipeActionsConfigurationForRowAt() {
//Something goes here
}
func leadingSwipeActionsConfigurationForRowAt() {
//Something goes here
}
}
Related
I have a UIViewController with a UICollectionView and a UITableView. Both views use UICollectionViewDiffableDataSource and UITableViewDiffableDataSource respectively.
In the table, I am trying to set up swipe functionality for the delete action. It was a very simple task with a regular data source, but I can't get it to work with the diffable one.
I tried creating a custom class for UITableViewDiffableDataSource, but I wasn't even able to compile the code with my attempts to access the property from the controller. (I needed to access the property storing the model to delete not only the row, but also the data.) Using UITableViewDelegate with a tableView(leadingSwipeActionsConfigurationForRowAt:) did compile, and I could swipe, but the app crashed with an error: UITableView must be updated via the UITableViewDiffableDataSource APIs when acting as the UITableView's dataSource.
How do I do it? Is there a best practice for a job like this?
Edit 1:
As requested in the comments, I'm providing some of my code, for the most recent attempt at implementation.
In the UIViewController (it's a long implementation, so I left out most of it):
class ViewController: UIViewController, ItemsTableViewDiffableDataSourceDelegate {
#IBOutlet var tableView: UITableView!
var items = [Item]()
var tableViewDataSource: ItemsTableViewDiffableDataSource!
var itemsSnapshot: NSDiffableDataSourceSnapshot<String, Item> {
var snapshot = NSDiffableDataSourceSnapshot<String, Item>()
snapshot.appendSections(["Items"])
snapshot.appendItems(items)
return snapshot
}
func configureTableViewDataSource() {
// A working implementation
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { action, view, handler in
self.items.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
deleteAction.backgroundColor = .red
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
}
In the UITableViewDiffableDataSource (complete implementation):
#MainActor
class ItemsTableViewDiffableDataSource: UITableViewDiffableDataSource<String, Item> {
}
protocol ItemsTableViewDiffableDataSourceDelegate: AnyObject {
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
}
Originally, I had the tableView(leadingSwipeActionsConfigurationForRowAt:) method defined within the data source class, but I couldn't access the items property, so I tried it with a protocol. Either way, the implementation doesn't work — I'm unable to swipe, let alone delete items and table rows.
Edit 2:
I guess the real question is — how do I register the tableView(leadingSwipeActionsConfigurationForRowAt:) method with my tableView. Still, my guess is that my implementation is faulty in general.
It turns out that the implementation was fairly simple. A custom UITableViewDiffableDataSource class wasn't needed in my case. The entirety of the implementation went into the view controller.
class ViewController: UIViewController, UITableViewDelegate {
#IBOutlet var tableView: UITableView!
var items = [Item]()
var tableViewDataSource: UITableViewDiffableDataSource<String, Item>!
var itemsSnapshot: NSDiffableDataSourceSnapshot<String, Item> {
var snapshot = NSDiffableDataSourceSnapshot<String, Item>()
snapshot.appendSections(["Items"])
snapshot.appendItems(items)
return snapshot
}
override func viewDidLoad() {
super.viewDidLoad()
configureTableViewDataSource()
tableView.delegate = self
}
func configureTableViewDataSource() {
tableViewDataSource = UITableViewDiffableDataSource<String, Item>(tableView: tableView, cellProvider: { (tableView, indexPath, itemIdentifier) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: ItemTableViewCell.reuseIdentifier, for: indexPath) as! ItemTableViewCell
cell.configureCell(for: itemIdentifier)
return cell
})
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { action, view, handler in
self.items.remove(at: indexPath.row)
self.tableViewDataSource.apply(self.itemsSnapshot, animatingDifferences: true)
}
deleteAction.backgroundColor = .red
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
configuration.performsFirstActionWithFullSwipe = true
return configuration
}
}
Remember to set the view controller as the delegate of the table view, which you can do in viewDidLoad(). (Thanks to HangarRash for the hint in the comments.)
My first mistake when trying this approach was that I still tried to update the data by acting directly on the table view. This will give you an error that reads:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView must be updated via the UITableViewDiffableDataSource APIs when acting as the UITableView's dataSource: please do not call mutation APIs directly on UITableView.
Instead call self.tableViewDataSource.apply(self.itemsSnapshot, animatingDifferences: true).
I am working on an small project where I have an app that takes in tvshow information entered by the user and displays it in a custom tableview cell. I would like to sort the shows as they are entered based on which current episode the user is on. I know this code works because I tested it with print statements and it sorts the array but it does not sort on the simulator. So I just was curious where I should place this so that it sorts on the app side.
func sortShows() {
let sortedShows = tvShows.sorted { $0.currentEpisode > $1.currentEpisode}
TVShowTableView.reloadData()
print(sortedShows)
}
Here is where I am currently placing it inside my view controller
extension TVShowListViewController: AddTVShowDelegate {
func tvShowWasCreated(tvShow: TVShow) {
tvShows.append(tvShow)
dismiss(animated: true, completion: nil)
TVShowTableView.reloadData()
sortShows()
}
}
In this part of your code:
func sortShows() {
// here you are creating a NEW array
let sortedShows = tvShows.sorted { $0.currentEpisode > $1.currentEpisode}
// here you tell the table view to reload with the OLD array
TVShowTableView.reloadData()
print(sortedShows)
}
In your controller class, you probably have something like:
var tvShows: [TVShow] = [TVShow]()
and then you populate it with shows, like you do with a new show:
tvShows.append(tvShow)
Then your controller is doing something like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tvShowCell", for: indexPath) as! TVShowCell
cell.tvShow = tvShows[indexPath.row]
return cell
}
What you want to do is add another var to your class:
var sortedShows: [TVShow] = [TVShow]()
then change your sort func to use that array:
func sortShows() {
// use the existing class-level array
sortedShows = tvShows.sorted { $0.currentEpisode > $1.currentEpisode}
// here you tell the table view to reload
TVShowTableView.reloadData()
print(sortedShows)
}
and change your other funcs to use the sortedShows array:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// use sortedShows array
return sortedShows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tvShowCell", for: indexPath) as! TVShowCell
// use sortedShows array
cell.tvShow = sortedShows[indexPath.row]
return cell
}
and you'll want to call sortShows() at the end of viewDidLoad() (or wherever you are getting your initial list of shows).
Edit
Another way you might use cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tvShowCell", for: indexPath) as! TVShowCell
// use sortedShows array
let tvShow = sortedShows[indexPath.row]
cell.showTitleLable.text = tvShow.title
cell.showDecriptionLable.text = tvShow.description
return cell
}
Evening ladies and gentleman,
I am currently getting used to Swift and wanted to start with a little todo app. So far I can add an item and safe it persistently in a context. When an item has been added, it will be shown in a tableview. Now, I want to use a check swipe to strikethrough items, which have been added and safe this information in my context. Deleting using a swipe works perfectly fine.
Has anybody an idea how realize this? I tried to solve it by myself, but couldnt get it done. A similar question has been asked here before, but didnt get a proper answer: Add strikethrough to tableview row with a swipe
func checkAccessoryType(cell: UITableViewCell, isCompleted: Bool) {
if isCompleted {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let todo = CoreDataManager.shared.getTodoItem(index: indexPath.row)
todo.completed = !todo.completed
CoreDataManager.shared.safeContext()
if let cell = tableView.cellForRow(at: indexPath){
checkAccessoryType(cell: cell, isCompleted: todo.completed)
}
}
Assuming you are trying to strikethrough the title of your task -- which should be defined as a label -- here is the approach to take:
1- Make sure your label is set to attributed text rather than plain. To do that, go to Main.storyboard, select your label, and inside the attribute inspector, set text to Attributed.
2- Inside your completion block (that is the completion block executed after a swipe) add the following code:
(SWIFT 5)
let attributeString: NSMutableAttributedString = NSMutableAttributedString(string: taskLabel.text)
attributeString.addAttribute(.strikethroughStyle, value: 1, range: NSRange(location: 0, length: taskLabel.text.count))
taskLabel.attributedText = attributeString
Just a little advice: it's always helpful if you add some code when you ask a question.
Let me know if anything doesn't make sense.
Looking at the link that you provided, you need swipe action on your UITableViewCell.
Try looking into:
leadingSwipeActionsConfigurationForRowAt
trailingSwipeActionsConfigurationForRowAt
You need this action to perform the strikethrough label or delete:
func tableView(_ tableView: UITableView,
leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let closeAction = UIContextualAction(style: .normal, title: "Close", handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in
print("OK, marked as Closed")
success(true)
})
closeAction.image = UIImage(named: "tick")
closeAction.backgroundColor = .purple
return UISwipeActionsConfiguration(actions: [closeAction])
}
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
{
let modifyAction = UIContextualAction(style: .normal, title: "Update", handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in
print("Update action ...")
success(true)
})
modifyAction.image = UIImage(named: "hammer")
modifyAction.backgroundColor = .blue
return UISwipeActionsConfiguration(actions: [modifyAction])
}
Source: https://developerslogblog.wordpress.com/2017/06/28/ios-11-swipe-leftright-in-uitableviewcell/
I'm using SwipeCellKit for my TO DO List app. When the user swipes left it deletes the item, but when the user swipes right I want him to be able to set a reminder on this item, so I've created an actionset a reminder
this action should perform a segue which brings the user to a custom popup with a date picker in it. The problem is that when I click on the button to set a reminder the simulator quits with an uncaught exception. I've already tried to perform deletion from this button it works perfectly, I've also tried to perform another segue to another view controller from this button the simulator quits. Could someone tell me what I'm doing wrong here? Here's my code:
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? { if orientation == .left {
guard isSwipeRightEnabled else { return nil }
let setReminder = SwipeAction(style: .default, title: "Set a reminder") { action, indexPath in
self.updateModelByAddingAReminder(at: indexPath)
}
setReminder.image = UIImage(named: "reminder-icon")
return[setReminder]
}else{
let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
self.updateModel(at: indexPath)
}
// customize the action appearance
deleteAction.image = UIImage(named: "delete-icon")
// return [setReminder, deleteAction]
return [deleteAction]
}
Ok, I found problem in your options for cell.
From doc
The built-in .destructive, and .destructiveAfterFill expansion styles are configured to automatically perform row deletion when the action handler is invoked (automatic fulfillment).
And you need use destructive style for cell in editActionsForRowAt. Or use another options, for example
func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeTableOptions {
var options = SwipeTableOptions()
options.transitionStyle = .border
if orientation == .left{
//or none
options.expansionStyle = .selection
}else{
options.expansionStyle = .destructive
}
return options
}
Hope it's help.
I am preparing a table in which when I swipe the cell I need to get two rounded buttons. Each button should have one image and and a label.
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
var hello = UITableViewRowAction(style: .Default, title: "Image") { (action, indexPath) in
// do some action
if let buttonImage = UIImage(named: "Image") {
// self.bgColor = UIColor.imageWithBackgroundColor(image: buttonImage, bgColor: UIColor.blueColor())
}
return editButtonItem()
}
First of all, there are some problems with your code :
You return the result of editButtonItem() method, which basically discards your hello action. I'm gonna assume from the name of it, that this method returned a single action, and not two as you wanted.
In your action handler, you tried to set the background on self. Blocks capture variables from their parent scope, so self in this block didn't relate to hello action, but rather to the class in which your editActionsForRowAtIndexPath method was implemented.
How to achieve what you need (two buttons with title and image) :
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
var firstAction = UITableViewRowAction(style: .Default, title: "First") { (action, indexPath) in
// action handler code here
// this code will be run only and if the user presses the button
// The input parameters to this are the action itself, and indexPath so that you know in which row the action was clicked
}
var secondAction = UITableViewRowAction(style: .Default, title: "Second") { (action, indexPath) in
// action handler code here
}
firstAction.backgroundColor = UIColor(patternImage: UIImage(named: "firstImageName")!)
secondAction.backgroundColor = UIColor(patternImage: UIImage(named:"secondImageName")!)
return [firstAction, secondAction]
}
We create two separate actions, assign their background colors to use pattern images and return an array containing both our actions. This is the most you can do to alter the appearance of UITableViewRowAction - we can see from the docs, that this class doesn't inherit from UIView.
If you wanted to customize the appearance more, you should look for an external library or implement your own solution from the scratch.