UIMenuController in a TableViewCell Element - swift

I am looking to get a UIMenuController when an image within a cell is tapped. To be clear this is NOT when the cell is tapped, but rather when the image within the cell is tapped. The tap gesture on the image is firing off properly and running the function, but the menu never shows.
Custom Cell:
class VideoHomeCell: UITableViewCell {
var thumbImage:UIImage! {
didSet {
createCell()
}
}
private lazy var thumbImageView:UIImageView = {
let view = UIImageView(image: self.thumbImage)
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
view.isUserInteractionEnabled = true
view.translatesAutoresizingMaskIntoConstraints = false
let gesture = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
view.addGestureRecognizer(gesture)
return view
}()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func createCell() {
self.addSubview(thumbImageView)
thumbImageView.heightAnchor.constraint(equalToConstant: 100).isActive = true
thumbImageView.widthAnchor.constraint(equalToConstant: 100).isActive = true
thumbImageView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
thumbImageView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
}
#objc func imageTapped(recognizer: UIGestureRecognizer) {
let menuController = UIMenuController.shared
let trimItem = UIMenuItem(title: "Trim", action: #selector(menuSelected))
menuController.menuItems = [trimItem]
menuController.setTargetRect(self.thumbImageView.frame, in: self.superview!)
menuController.setMenuVisible(true, animated: true)
}
#objc func menuSelected() {
print("Menu Selection!")
}
}
TableView:
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.videoModelArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! VideoHomeCell
cell.thumbImage = self.videoModelArray[indexPath.row].originalThumbnail
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100.0
}
I have tried different values within setTargetRect to include the gesture's view and self.frame. I have also tried rendering in self. Additionally, I manually set a CGRect. Nothing I do allows for the menu to display.

I solved this issue by creating a protocol within the cell subclass and I assigned the table view controller as the delegate. When the image is tapped it fires off the delegate and then the UIMenuController is created within the tableview rather than within the tableviewcell. I pass back the cell and I am then able to use it's view to show the menu. It is also required to have the following on the tableview per Leo Dabus' comment.
override var canBecomeFirstResponder: Bool {
return true
}
In the end I while I was not able to make this work inside the tableviewcell subclass, I do find this an acceptable workaround.

Related

UITableView Not Showing Reorder Controls

When I use trailingSwipeActionsConfigurationForRowAt my TableView will show the delete and reorder options, however when selecting reorder nothing happens. I think I have all of the correct methods and am calling setEditing; is there anything else I'm missing? Thanks!
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
func setupTableView() {
tableView.frame = self.view.frame
tableView.dataSource = self
tableView.delegate = self
tableView.register(CustomCell.self, forCellReuseIdentifier: "CustomCell")
tableView.dragInteractionEnabled = true
self.view.addSubview(tableView)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 8
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.backgroundColor = .gray
cell.showsReorderControl = true
return cell
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
self.tableView.setEditing(editing, animated: animated)
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .normal, title: "delete") { (action, view, completion) in
tableView.reloadData()
completion(true)
}
let reorderAction = UIContextualAction(style: .normal, title: "reorder") { (action, view, completion) in
tableView.setEditing(true, animated: true)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction, reorderAction])
}
}
class CustomCell: UITableViewCell {
}
Result after swiping:
After selecting reorder:
A few observations:
You are not going to get the reorder controls if you do not implement tableView(_:moveRowAt:to:), e.g., assuming you had a model which was an array called objects, you could do the following:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = objects.remove(at: sourceIndexPath.row)
objects.insert(object, at: destinationIndexPath.row)
}
The trailingSwipeActionsConfigurationForRowAt is probably not the right place to put a “reorder” command. Part of the reason is that once the table view is in edit mode and you tap on the ⛔️, the trailing actions show up, and “reorder” does not make sense in that context. E.g., here I am tapping on ⛔️ and I see the confusing actions.
I would suggest only adding “delete” as the trailing action. That way, you (a) get only “delete” if you tap on ⛔️ in isEditing mode, but also (b) get the stand-alone swipe action, too.
You cannot initiate isEditing from the trailing swipe actions (and, as discussed above, I do not think you want to, anyway). So, if you do not have “reorder” in the trailing swipe actions, you need some other method to enter edit mode. E.g., above, I added an “edit” button to the navigation bar that toggles isEditing:
#IBAction func didTapEdit(_ sender: Any) {
tableView.isEditing.toggle()
}
Then, you can keep the swipe to delete functionality, but when you tap on edit button, you have the tap on ⛔️ to delete functionality (plus the handles for reordering because we added tableView(_:moveRowAt:to:) as outlined in step one, above):
Another way to achieve reordering is to just allow drag and drop within the table view where you can long-press on a row and then drag it:
This is enabled by setting dragInteractionEnabled and dropDelegate:
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
return formatter
}()
private var objects: [Foo] = ...
override func viewDidLoad() {
super.viewDidLoad()
...
tableView.dragInteractionEnabled = true
tableView.dropDelegate = self
}
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource { ... }
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "delete") { [weak self] action, view, completion in
self?.objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .middle)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
// This is used if table view is in `isEditing` mode and by `UITableViewDropDelegate`
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = objects.remove(at: sourceIndexPath.row)
objects.insert(object, at: destinationIndexPath.row)
}
}
// MARK: - UITableViewDropDelegate
extension ViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
guard
session.items.count == 1, // Accept only one drag item ...
tableView.hasActiveDrag // ... from within this table view
else {
return UITableViewDropProposal(operation: .cancel)
}
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath else { return }
for item in coordinator.items {
if let sourceIndexPath = item.sourceIndexPath {
DispatchQueue.main.async {
tableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
}
}
}
}
}
Clearly, if you were going to enable drag from this app to others, you would add UITableViewDragDelegate conformance here, and make your model objects conform to NSItemProviderReading and NSItemProviderWriting. But the above should be sufficient for dragging and dropping to reorder within a UITableView.

Update Specific Button Image in UITableView Cell

I am trying to add an action to my "like" button. So that when the user taps the heart UIButton in a cell, the heart in the cell they tapped updates to a pink heart showing that they liked it. But instead it likes the heart they tapped and another random heart in a different cell that they did not interact with. I have been on this all day and any help would be grateful. For Example, if I like/tap my heart UIButton the buttons image I tapped updates, but when I scroll down another random heart updates from that same first cell button tap.
Also When I scroll and the cell leaves view and scroll back up the image returns back to unlike and other like buttons become liked.
Keep a data model for your buttons state
Try with the below code
struct TableModel {
var isLiked: Bool
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var dataSource: [TableModel] = []
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
dataSource = Array(repeating: TableModel(isLiked: false), count: 20)
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.showsVerticalScrollIndicator = false
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
#objc func buttonSelected(_ sender: UIButton) {
dataSource[sender.tag].isLiked = !dataSource[sender.tag].isLiked
let indexPath = IndexPath(row: sender.tag, section: 0)
tableView.reloadRows(at: [indexPath], with: .automatic)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.likeBtn.tag = indexPath.row
cell.likeBtn.addTarget(self, action: #selector(buttonSelected(_:)), for: .touchUpInside)
let isLiked = dataSource[indexPath.row].isLiked
if isLiked {
cell.likeBtn.setImage(UIImage(named: "liked"), for: UIControl.State.normal)
} else {
//set unlike image
}
return cell
}
}
Currently, you have a hardcoded number of rows, but anyway you will need to have a data source with data models. When you press the button, you have to save the state of the button of a specific row. I would recommend you create a model first.
Here I provided an easy (but flexible enough) way how to do this. I haven't debugged it, but it should work and you can see the idea. I hope this would be helpful.
Create Cell Model
struct CellViewModel {
let title: String
var isLiked: Bool
// Add other properties you need for the cell, image, etc.
}
Update cell class
It's better to handle top action right in the cell class. To handle this action on the controller you can closure or delegate like I did.
// Create a delegate protocol
protocol TableViewCellDelegate: AnyObject {
func didSelectLikeButton(isLiked: Bool, forCell cell: TableViewCell)
}
class TableViewCell: UITableViewCell {
// add a delegate property
weak var delegate: TableViewCellDelegate?
#IBOutlet var titleTxt: UILabel!
#IBOutlet var likeBtn: UIButton!
//...
override func awakeFromNib() {
super.awakeFromNib()
// You can add target here or an action in the Storyboard/Xib
likeBtn.addTarget(self, action: #selector(likeButtonSelected), for: .touchUpInside)
}
/// Method to update state of the cell
func update(with model: CellViewModel) {
titleTxt.text = model.title
likeBtn.isSelected = model.isLiked
// To use `isSelected` you need to set different images for normal state and for selected state
}
#objc private func likeButtonSelected(_ sender: UIButton) {
sender.isSelected.toggle()
delegate?.didSelectLikeButton(isLiked: sender.isSelected, forCell: self)
}
}
Add an array of models and use it
This is an updated class of ViewController with usage of models.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// Provide a list of all models (cells)
private var cellModels: [CellViewModel] = [
CellViewModel(title: "Title 1", isLiked: false),
CellViewModel(title: "Title 2", isLiked: true),
CellViewModel(title: "Title 3", isLiked: false)
]
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.showsVerticalScrollIndicator = false
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return count of cell models
return cellModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
let model = cellModels[indexPath.row]
// call a single method to update the cell UI
cell.update(with: model)
// and you need to set delegate in order to handle the like button selection
cell.delegate = self
return cell
}
}
extension ViewController: TableViewCellDelegate {
func didSelectLikeButton(isLiked: Bool, forCell cell: TableViewCell) {
// get an indexPath of the cell which call this method
guard let indexPath = tableView.indexPath(for: cell) else {
return
}
// get the model by row
var model = cellModels[indexPath.row]
// save the updated state of the button into the cell model
model.isLiked = isLiked
// and set the model back to the array, since we use struct
cellModels[indexPath.row] = model
}
}

Gap between navigation bar and first UITableViewCell

I have a UITableViewController with a search controller embedded in the navigation bar.
When the view shows in a modal, there is a gap between the first row and the navigation bar.
here is my implementation. Some code has been redacted for confidentiality. But anything regarding view controller setup should still be there:
import Foundation
class SearchViewController: UITableViewController {
fileprivate lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
searchController.searchBar.showsCancelButton = true
searchController.searchBar.delegate = self
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
navigationItem.titleView = searchController.searchBar
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.tintColor = BrandColor.appleDarkGray
}
}
extension ExploreSearchViewController: UISearchBarDelegate {
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
presentingViewController?.dismiss(animated: true, completion: nil)
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if let searchText = searchBar.text {
tableView.refreshControl?.beginRefreshing()
searchedText = searchText
}
}
}
// MARK: - UITableView Methods
extension ExploreSearchViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResultsProvider.searchResults.count
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard searchResultsProvider.canLoadNextPage else { return }
// handle paging here...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard searchResultsProvider.searchResults.count > indexPath.row else { return UITableViewCell() }
// Setup search result cell here...
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Handle cell did tap here...
}
#objc private func refresh() {
searchResultsProvider.refreshResults()
}
// MARK - Table View Helpers
private func setupTableView() {
registerCells()
self.tableView.backgroundColor = .white
self.tableView.contentInsetAdjustmentBehavior = .always
self.tableView.refreshControl = UIRefreshControl()
self.tableView.refreshControl?.addTarget(self, action: #selector(refresh), for: .valueChanged)
self.tableView.useDefaultPageLoadingIndicator()
self.tableView.setBackgroundViewIfEmpty(message: Self.searchInitialMessage, remedyButton: nil)
}
private func registerCells() {
//Register Table View Cells here...
}
}
Has anyone seen this kind of issue before? I can't move the top edge insets up because then the refresh control will be hidden behind the navigation bar.
Anyone seen this behaviour before? If so, how did you solve it?
The issue was caused by the table view style was set to .grouped on the previous view controller.
Setting the style to .plain solved this issue.
I believe the reason grouped style causes the blank space is because its adding an empty section header above the cells.

Set UITableViewCell selected before presenting

I have a UITableViewCell that I want to show as selected when the UIViewController is presented. vc.tableView.selectRow:atIndexPath is nice in theory but it bypasses the calls for willSelect and didSelect on the cell.
The cell has an exposed UIImageView that setSelected toggles, which is what I'm trying to show on the initial load.
Any help here would be appreciated. Thanks!
I'll give you an example of how change background color of your cells and select a initial one, so you can follow and put the code that you need:
In your UIViewController subclass, implement these methods, so you can put your logic for selected and deselected states:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedCell = tableView.cellForRow(at: indexPath)!
selectedCell.backgroundColor = UIColor.purple
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let deselectedCell = tableView.cellForRow(at: indexPath)!
deselectedCell.backgroundColor = UIColor.clear
}
In your custom UITableViewCell subclass you have to override the isSelected property, this is the key to avoid the method tableView.selectRow:atIndexPath bypassing didSelect:
class CustomTableViewCell: UITableViewCell {
override var isSelected: Bool {
didSet {
print(isSelected.description)
self.selectionStyle = .none
if isSelected{
self.backgroundColor = UIColor.purple
} else {
self.backgroundColor = UIColor.clear
}
}
}
}
Last, back to your UIViewController subclass, you can call selectRow:atIndexPath in your viewDidLoad method, for instance:
override func viewDidLoad(){
super.viewDidLoad()
tableView.selectRow(at: IndexPath(row: 0, section: 0) , animated: true, scrollPosition: UITableViewScrollPosition.none)
}

Selecting row in tableview, removes custom cell

I have a Viewcontroller with a Searchbar at the top with a tableview below. The tableview has a custom cell with 2 labels in it. My problem is that when i run the app and i select a row/cell everything inside the cell disappears. I then force the blank cell outside the visible area of the tableview, so it will be re-used. That's when everything inside the cell is back. Does anyone know why it behaves like this?
My Custom cell class (ContactCell.swift):
import UIKit
class ContactCell: UITableViewCell {
#IBOutlet var lblContactName: UILabel!
#IBOutlet var lblContactTitle: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
My ViewDidLoad function:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
}
My Delegate and Datasource:
extension contactsTabelViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 6
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("contactCell", forIndexPath: indexPath) as! ContactCell
if let label = cell.lblContactName{
label.text = "This is a name"
}
if let label3 = cell.lblContactTitle{
label3.text = "This is a title"
}
return ContactCell()
}
}
The problem that caused this problem was that i returned ContactCell() instead of the variable cell
Solution was:
Change this:
return ContactCell()
to this:
return cell
in the cellForRowAtIndexPath function.