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.
Related
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).
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 am trying to replicate the same type of functionality as the stock iOS Mail app when a user swipes a tableview cell as currently seen here:
There are 3 options: More, Flag, and Archive. When the user taps on any one of the 3, the background color changes to indicate it's in a highlighted state. However, the icon and text do not change color.
I am trying to achieve the same effect.
I am following this tutorial guide to make a custom swipeable tableview cell using gestures:
How To Make A Swipeable Table View Cell With Actions – Without Going Nuts With Scroll Views
Currently I have this set up:
In each cell contains 3 UIViews, where each UIView contains an UIImageView and UILabel representing Dirty, Edit, Delete.
I am adding tap gestures to the 3 UIViews like so:
let dirtyButtonView = cell.viewWithTag(7)
let editButtonView = cell.viewWithTag(8)
let deleteButtonView = cell.viewWithTag(9)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(MyVC.buttonPressed))
tapRecognizer.numberOfTapsRequired = 1
dirtyButtonView?.addGestureRecognizer(tapRecognizer)
editButtonView?.addGestureRecognizer(tapRecognizer)
deleteButtonView?.addGestureRecognizer(tapRecognizer)
However, I cannot achieve the highlight state functionality that the Mail app has when a user selects one of the 3 options.
I'm not sure if this is the correct implementation, i.e. having a UIView and adding a gesture to it?
How can I achieve something similar to the iOS Mail app swipe functionality?
Thanks
I found exactly what I was looking for, SwipeCellKit, by jerkoch. This library performs the same exact actions as the stock iOS Mail app does when swiping to the left. No need to deal with different UIViews and UIButtons.
To use, simply conform to the SwipeTableViewCellDelegate, and use it in editActionsForRowAt like so:
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
guard orientation == .right else { return nil }
let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
// handle action by updating model with deletion
}
// customize the action appearance
deleteAction.image = UIImage(named: "delete")
return [deleteAction]
}
Make sure to change the cell's class to SwipeTableViewCell and set its delegate like so: cell.delegate = self.
I would take a look at the SWTableViewCell by CEWendel. It looks like it has exactly what you're looking for.
May be late to answer this, but in case anyone else is looking - consider this read to answer the question: https://www.raywenderlich.com/62435/make-swipeable-table-view-cell-actions-without-going-nuts-scroll-views
I'm currently learning how to build apps entirely programatically and have hit a bit of a wall in the process.
My app is going to be a fairly simple form that the user can fill in and it will generate a PDF at the end containing the information. For the basic information I have a tableview containing a set of UITextfields and have no problem iterating through them and extracting the data into an array of strings.
For the next section the user needed to input a lot more text so I have used UITextviews within the tableview. What I want to be able to do is extract the text inputted and save it to an array of strings that is used to create my customer object. The code that does this is as follows:
func createGoals() {
for section in 0..<secondTableView.numberOfSections {
for row in 0..<secondTableView.numberOfRows(inSection: section) {
let indexPath = NSIndexPath(row: row, section: section)
let thisCell = secondTableView.cellForRow(at: indexPath as IndexPath) as! mySecondCell
if thisCell.textview.text == "Enter Goals" {
print("NO CONTENT IN FIELD")
let ac = UIAlertController(title: "But...But...", message: "If you have no goals, how can you progress?!", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
break
} else {
print("I MADE IT")
print("\(thisCell.textview.text!)")
newClient.goalArray.append(thisCell.textview.text)
print(newClient.goalArray)
}
}
}
}
But when this runs I get the error message "unexpectedly found nil while unwrapping an Optional value" and it particularly highlights this line:
let thisCell = secondTableView.cellForRow(at: indexPath as IndexPath) as! mySecondCell
This code is almost identical to how I iterate through the UITextfields successfully, all thats changed is the variables attached and as far as I can see there are no optionals.
Also if I don't fill in the UITextviews it will show the UIAlertController, it only seems to have a problem when I remove the placeholder and fill in the UITextviews.
Does anyone have any ideas as to why this is happening? I can provide more of my code if needed.
here you are loading cell in iteration. so issue is caused because you are going to load/access cell which is not visible.
here you should not load cell out side of cellForRowAtIndexPath.
Reason: UITableView is reusing cell with reusableCellIdentifier.
here you need to save values in array or dictionary when textfield-didEndEditing called (textfield delegate method).
and for validation check you can use saved value from array/dictionary.
Here is your optional
as! mySecondCell
Obviously, thisCell isn't a mySecondCell
Check if you did register the correct class
EDIT
You shouldn't call delegate method by yourself
In my tvOS app I have a collection view with cells that represent movies.
I attached 2 gesture recognizers to the view:
Go to movie details on selection tap
Play movie directly (with an Apple TV Remote dedicated Play button)
let posterTap = UITapGestureRecognizer(target: self, action: #selector(ListViewController.movieSelect(_:)))
posterTap.allowedPressTypes = [NSNumber(integer: UIPressType.Select.rawValue)]
self.view.addGestureRecognizer(posterTap)
let posterPlay = UITapGestureRecognizer(target: self, action: #selector(ListViewController.moviePlay(_:)))
posterPlay.allowedPressTypes = [NSNumber(integer: UIPressType.PlayPause.rawValue)]
self.view.addGestureRecognizer(posterPlay)
And the respected methods
func movieSelect(gesture: UITapGestureRecognizer) {
if let cell = UIScreen.mainScreen().focusedView as? ItemCollectionViewCell {
let item = ItemViewController(nibName: "ItemViewController", bundle: nil)
item.item = cell.data
self.presentViewController(item, animated: true, completion: nil)
}
}
func moviePlay(gesture: UITapGestureRecognizer) {
if let cell = UIScreen.mainScreen().focusedView as? ItemCollectionViewCell {
let data = cell.data
// TLDR;
// Passing data to AVPlayerViewController and presenting it + start playing the movie.
}
}
Everything seem to work, apart from the fact that when I stop playing the movie and coming back to the list (by closing the AVPlayerViewController), my second gesture recognizer (Play button) no longer works. It is still there if I check with print(self.view.gestureRecognizers) but moviePlay() is never called again no matter what.
Is there a way to debug this? What may cause this issue? I'm thinking this is caused by UIGestureRecognizerState being still in "use"? Or maybe something like that. At this point I have no clue.
I've experience weirdness with gesture recognizers on tvOS. In my case, for some reason, the app behaved as if gesture recognizers were lingering on after container view had been dismissed. Oddly enough, I've observed this weirdness when launching and closing AVPlayerViewController a few times as well.
What I did to fix this was to remove use of gesture recognizers and overriding pressesEnded method instead.
I hope this helps.