I have simple chat created using UITableView. I want to add the ability to highlight a message after long press. In fact, I want to create the same feature like iMessage:
After a long press, we unhighlight background (more darker), highlight message, scroll to this message and show the actionSheet
For now I managed to add only longPress and actionSheet
Long press recongizer on viewDidLoad:
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onCellLongPressed(gesture:)))
messagesTableView.addGestureRecognizer(longPressRecognizer)
onCellLongPressed function:
#objc func onCellLongPressed(gesture: UILongPressGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began {
let touchPoint = gesture.location(in: self.messagesTableView)
if let indexPath = messagesTableView.indexPathForRow(at: touchPoint) {
self.messagesTableView.selectRow(at: indexPath, animated: true, scrollPosition: UITableViewScrollPosition.none)
shareWithFriend()
}
}
}
#objc func shareWithFriend() {
alert(style: .actionSheet, actions: [
UIAlertAction(title: "Share with friend", style: .default, handler: { [weak self] (_) in
print("SHARE HERE")
}),
UIAlertAction(title: "Cancel", style: .destructive),
])
}
func alert(_ title: String? = nil, message: String? = nil, style: UIAlertController.Style, actions: [UIAlertAction]) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: style)
actions.forEach(alertController.addAction)
present(alertController, animated: true)
}
As you can see, the background color is above the navigation bar so I guess there is a secondary view controller astutely presented above the collection view when the user selects a cell.
I think this is two different view hierarchies which look like one:
One view controller contains the balloon list
One view controller contains a copy of the selected balloon and the few actions associated to it
Here is the road map :
Detect a long press in the collection view cells
Copy the selected balloon and present it inside a secondary view controller (ActionVC)
Adjust the position of the selected balloon inside the ActionVC. If the selected balloon is under the future action button, it should be moved. If the selected balloon does not bother anyone, it should be presented without any change.
Adjust the content offset of the collection view to reflect 3. The modification should be done alongside 3 to look like if the cell was actually moved.
Detect any touch on the ActionVC
Dismiss the ActionVC
Here is an example project.
To copy the balloon, I actually create a view of the same class as the one used in the collection view cell. But you could use a snapshot.
To adjust the selected balloon position inside the ActionVC, I used constraints with priorities. One claims "don't be under the action button", another one claims "be exactly where the cell was". I used a simple coordinates conversion to compute the expected selected balloon position inside the ActionVC.
To perform 3 alongside 4, I used the transitionCoordinator of the ActionVC, but you could use a simple animation block and present the ActionVC without animation.
I'm sorry if this answer will not fulfill your request completely, but hope it will help you.
Your initial code was right, but you have not set Scrolling type. So I suggest you use this method selectRow(at:animated:scrollPosition:) Just set scroll position:
self.messagesTableView.selectRow(at: indexPath, animated: true, scrollPosition: UITableViewScrollPosition.top)
This also will select this row and hence call the following method tableView(_:didSelectRowAt:). So you will need to implement highlighting logic here:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// your logic of changing color
}
Then you should implement similar action, but to deselect the row using deselectRow(at:animated:)I believe this should be done when user finished his action. Also you need to implement similar function tableView(_:didDeselectRowAt:) to return color back:
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
// change color back
}
Update:
You may also use the setHighlighted(_:animated:) method to highlight a cell. In this way you may avoid using selectRowAt/didSelectRowAt/didDeselectRowAt and scroll tableView using
tableView?.scrollToRow(at: indexPath, at: UITableViewScrollPosition.top, animated: true).
Related
I'm currently working on an app that has a table-view-like collection view and some other view controllers. Basically, my question is how can I update the indexPath of each cell when one of the collection view cells is deleted.
I attached my view controller file below, but here is what's going on on the app.
When a user opens the table-view-like collection view (in EventCollectionVC), it reloads the data from a database and presents them on the collection view. I also added the code to the navigation bar button item that the user can change the collection view to the edit mode. While in the edit mode, a small ellipsis.circle (SF symbols) is displayed on the collection view cell. When a user taps the ellipsis.circle icon, it displays a new view controller (ModalVC) and lets the user select either delete or edit the cell. When the user selects delete, it shows an alert to delete the cell and delete the cell information with modal dismiss (which means the ModalVC is closed and the MyCollectionVC is displayed now).
Since I have to make the two view controllers (like getting cell information from EventCollectionVC and present in ModalVC) talk to each other, I need to use the indexPath.row to get the information of the cell. Before deleting the cells, the numbers of indexPath.row in the collection view is like
[0,1,2,3,4,5]
But, for example, after I delete the second (indexPath.row = 1) cell and when I try to delete another item, the indexPath becomes
[0,2,3,4,5]
and I can see the collection view's index is not refreshed.
So my question is how can I update/refresh the cell's indexPath.row value after I delete a cell from the collection view?
This is the code with some explanations.
import UIKit
class EvnetCollectionViewController: UIViewController {
var EventDataSource: EventDataSource! // <- this is a class for the Model, and it stores array or Events
let ListView = ListView() // view file
var collectionViewDataSource: UICollectionViewDiffableDataSource<Section, Event>?
var targetEventIndex = Int() // variable to store the index of the event when event cell is tapped
override func loadView() {
view = ListView
}
override func viewDidLoad() {
super.viewDidLoad()
configureNavItem()
setupCollectionView()
displayEvents()
}
func configureNavItem() {
navigationItem.rightBarButtonItem = editButtonItem
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
if (editing){
ListView.collectionView.isEditing = true
} else {
ListView.collectionView.isEditing = false
}
}
func setupCollectionView() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Event> { cell, indexPath, Event in
var content = UIListContentConfiguration.cell()
content.text = Event.title
cell.contentConfiguration = content
let moreAction = UIAction(image: UIImage(systemName: "ellipsis.circle"),
handler: { _ in
let vc = EventActionModalViewController(); // when the user select the cell in edit mode, it displays action modal VC and then come back to this VC with dismiss later
vc.modalPresentationStyle = .overCurrentContext
self.targetEventIndex = indexPath.row // I need targetEvemtIndex when user selects delete event in EventActionModalVC, so just sotre value in here
})
let moreActionButton = UIButton(primaryAction: moreAction)
let moreActionAccessory = UICellAccessory.CustomViewConfiguration(
customView: moreActionButton,
placement: .trailing(displayed: .whenEditing, at: { _ in return 0 })
)
cell.accessories = [
.disclosureIndicator(displayed: .whenNotEditing),
.customView(configuration: moreActionAccessory)
]
}
collectionViewDataSource = UICollectionViewDiffableDataSource<Section, Event>(collectionView: ListView.collectionView) {
collectionView, indexPath, Event in
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: Event)
}
}
func displayEvents() {
EventDataSource = EventDataSource()
EventDataSource.loadData() // get Events in db and sore in an array Events
populate(with: EventDataSource.Events)
}
func populate(with Events: [Event]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Event>()
snapshot.appendSections([.List])
snapshot.appendItems(Events)
collectionViewDataSource?.apply(snapshot)
}
func showDeleteAlert() {
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in
self.EventDataSource.delete(at: targetEventIndex)
self.refreshList()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
self.showAlert(title: "Delete", message: nil, actions: [deleteAction, cancelAction], style: .actionSheet, completion: nil)
}
func refreshList() {
EventDataSource.loadData()
setupCollectionView() // since I write this code, it updates all of the indexPath in the collection view, but after deleting one item, the whole collection view is deleted and new collection view is reappeared.
populate(with: EventDataSource.Events)
}
}
I kinda know why this is happening. I only configure cell (in let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Event>...) once, so it won't update the cell information as well as its index path until I configure it again. But if I call setupCollectionView every after deleting one item, the whole collection view disappears and shows up again. Is it possible to reload the collection view list and updates its information without reloading the entire collection view?
Without writing setupCollectionView() in refreshList, the cell's indexPath is not refreshed and I get an error after I delete one cell and try to delete another one. So, I was wondering if there is a way to avoid recreating the whole collection view but update cells' indexPath when the user delete one of the cell in collection view.
I fixed the code in the refresh list function.
func refreshList() {
self.EventDataSource.loadData()
self.populate(with: self.EventDataSource.Events)
ListView.collectionView.reloadData()
}
I just needed to call reloadData after I populate all the data...
Ok, this one may be a bit difficult to illustrate but here is the issue: I tried attaching a long press gesture recognizer on the tableView cell, and linked it in the Viewcontroller. But, the gesture does not work on every cell in the table - only 1. And the 1 cell it functions on changes (sometimes it is the first, sometimes the second, etc - depends on how many cells actually have data). If anyone can point me into the right direction it would be very appreciated.
Below is the code for handling the gesture. Thanks!
if recognizer.state == .changed
{
let alertController = UIAlertController(title: nil, message:
"Open Product in Safari", preferredStyle: .alert)
let indexPath = tableView.indexPathForSelectedRow
let itemSku = self.itemArray[indexPath?.row ?? 0].sku
alertController.addAction(UIAlertAction(title: "Go to Safari", style: .default,handler: { action in
UIApplication.shared.open(URL(string: "\(itemURL)\(itemSku ?? "")") ?? URL(string: "")!, options: [:]) { _ in
print("Link opened")
}
}))
present(alertController, animated: true, completion: nil)
}
}
Since you didn't post any code related to your issue, best guess is:
Gesture recognizers - such as UILongPressGestureRecognizer - are distinct instances. They can only be added to one object at a time. If you try to add it to multiple objects, it will only "exist" on the last one.
You probably created one gesture recognizer and then tried to add it to every cell.
Assuming you are using a custom cell class, you probably want to instantiate a UILongPressGestureRecognizer inside your cell init code and add it to self (or self.contentView or whatever view you want to respond to the gesture). Also set its target to a func within your cell class. When that is triggered, use either a "callback" closure or a protocol / delegate pattern to inform your controller that the gesture occurred.
I'm creating an iOS App using Swift 4 and I'm not using Storyboards.
To delete a row from the Table View Controller the user swipe left the row and then click the Delete Button.
Here is the code I'm using to implement that (no external libraries have been used):
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
self.isAccessibilityElement = true
self.accessibilityLabel = "Delete row"
let deleteAction = UIContextualAction(style: .normal , title: "DELETE") { (action, view, handler) in
self.removeRowFromMyList(indexPath: indexPath.row)
MyListController.stations.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
self.tableView.setEditing(false, animated: true)
self.tableView.reloadData()
}
let swipeAction = UISwipeActionsConfiguration(actions: [deleteAction])
swipeAction.performsFirstActionWithFullSwipe = false
return swipeAction
}
I did check other questions and none of them address that.
Please don't hesitate to comment here for any other information you need to know to solve this issue.
Thanks :)
Using Accessibility Custom Action from UIAccessibility by Apple
You just need to set Accessibility Custom Action:
cell.accessibilityCustomActions = [UIAccessibilityCustomAction(name: "Delete", target: self, selector: #selector(theCustomAction))]
#objc private func theCustomAction() -> Bool {
//Do anything you want here
return true
}
Update:
So I did recreate the project but this time I was using Storyboards (I wasn't the last time) and I imported from Cocoapods the SwipeCellKit Library and I followed their documentation and VoiceOver was working perfectly fine with deleting a cell from them indexPath.row with no problem.
When Voice Over is turned on and the UITableViewCell is in focus, Voice Over would announce "Swipe Up or Down to Select a Custom Action, then double tap to activate"
If the user follows the above mentioned instruction, the user would be able to select one of the many actions available and double tap to activate it
After performing the action, Voice Over would announce "Performed action "
Note:
Advantage with using standard controls is that accessibility is mostly handled for you.
You don't have to worry about it breaking with newer versions of iOS
If system provides built in functionality then go with it.
Implementing members of the UIAccessibilityCustomAction class will allow you to add additional functionality to the UITableViewCell. In cellForRowAt: IndexPath, add the follow code to attach a custom action to the cell.
cell.isAccessibilityElement = true
let customAction = UIAccessibilityCustomAction(name: "Delete Row", target: self, selector: #selector(deleteRowAction))
cell.accessibilityCustomActions = [selectAction, disclosureAction]
Selectors associated with the custom actions are powerful, but it is difficult to pass in parameters as arguments that indicate the cell or the index path. Furthermore, a tableView's didSelectRowAt: IndexPath isn't activated by the accessibility focus.
The solution here is to find the location of the VoiceOver accessibility focus and obtain information from the cell. This code can be included in your selector as shown below.
#objc func deleteRowAction() -> Bool {
let focusedCell = UIAccessibilityFocusedElement(UIAccessibilityNotificationVoiceOverIdentifier) as! UITableViewCell
if let indexPath = tableView?.indexPath(for: focusedCell) {
// perform the custom action here using the indexPath information
}
return true
}
I really have no idea how to call this feature, music app had it up to 8.4, it looks like on the screenshot. I want to implement it in my app so when user presses the cell the "bubble" with 2 option buttons shows up.
I am interested in how to make it happen in Obj-C but I'm sure people will apreciate the answer written in Swift. Thanks
Screenshot
I'm doing exactly the same thing as what you want.
To do so, you have to create your own view controller, not UIMenuItem. The screen capture is as follows.
What I'm doing is to create a viewController as popup (PopoverMenuController), and adding a tableView as a subView for menu.
In the same way, you can add whatever UI controls you want instead of tableView.
Here is how you use my PopoverMenuController.
var myPopupMenu: PopoverMenuController!
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// make cell fully visible
let tableCell = tableView.cellForRow(at: indexPath)!
tableView.scrollRectToVisible(tableView.rectForRow(at: indexPath), animated: false)
// pop up menu
myPopupMenu = PopoverMenuController()
myPopupMenu.sourceView = tableCell
var rect = tableCell.bounds
// I'm adjusting popup menu position slightly (this is optional)
rect.origin.y = rect.size.height/3.0
rect.size.height = rect.size.height / 2.0
myPopupMenu.sourceRect = rect
myPopupMenu.addAction(PopMenuAction(textLabel: "MyMenu-1", accessoryType: .none, handler: myMenuHandler1))
myPopupMenu.addAction(PopMenuAction(textLabel: "MyMenu-2", accessoryType: .none, handler: myMenuHandler2))
present(myPopupMenu, animated: true, completion: nil)
}
func myMenuHandler1(_ action: PopMenuAction?) -> Bool {
// do some work...
return false
}
func myMenuHandler2(_ action: PopMenuAction?) -> Bool {
// do some work...
return false
}
To transltae to Obj-C should not be a big effort.
I put souce code of PopoverMenuController.swift here. This controller is self-contained and it has a description on how to use.
Hope this helps.
Look up how to implement UIMenuController on a UITableViewCell
How to show a custom UIMenuItem for a UITableViewCell
I want to remove cell of table in Apple watch. Is there any delegate method like commitEditingStyle which we use in iPhone app. Or any user interface like gesture or any thing so i can remove cell and data which i want at runtime. I already use didSelectRowAtIndex for navigate to other controller so please give me a other way.So, i can maintain both events. Thank you in advance.
There is no way. Context menu could be used but it works for full controller, and there is no way to detect row/coordinates. The best way when user clicks on row you display detail controller with delete button or context menu
for Swift 5
If you are looking to delete one of the rows, you can do this.
add a remove row function.
add the didSelectRowAt function
add an alert function to the didSelectRowAt
then add the remove function to the alert
also add a cancel button to the alert.
func removeRow(at row: Int) {
array.remove(at: row)
self.loadTable()
}
override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
let delete = WKAlertAction(title: "Delete Item", style: .default) {
self.removeRow(at: rowIndex)
self.userDefaults.setValue(self.array, forKey: "items")
}
let cancel = WKAlertAction(title: "cancel", style: .cancel) {}
self.presentAlert(withTitle: "Alert", message: "Do you want to delete this item?", preferredStyle: .alert, actions: [delete,cancel])
}