tvOS performsegue in a nested TableViewController - ios

I have been having issues calling the performsegue method on my custom CollectionViewCell. My view hierarchy is UIView-UITableView-UICollectionView. The tableview is a static tableview with a collectionview inside my "CustomTableViewCell". Because I am using tvOS I read I should be using a UITapGestureRecognizer instead of the collectionview(didSelectCell) function. Here is my method "tapped" which I know is hooked up properly because my print function works, the issue is I am getting an error "Value of type CustomTableViewCell has no member "perfromSegue" when I add the "self.performsegue" line. I tried to control+drag a segue from the cell to my next view but still nothing. I assume it has to do with the the type of class my "CustomTableViewCell" is but I am not sure what else to add to it.
cell for item which adds the gesture:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postCell", for: indexPath) as! PostCollectionViewCell
if cell.gestureRecognizers?.count == nil {
let tap = UITapGestureRecognizer(target: self, action: #selector(CustomTableViewCell.tapped(_:)))
tap.allowedPressTypes = [NSNumber(value: UIPressType.select.rawValue)]
cell.addGestureRecognizer(tap)
}
here is the tapped method:
func tapped(_ gesture: UITapGestureRecognizer){
if let cell = gesture.view as? PostCollectionViewCell {
//load next view pass movie
guard let post = cell.post else {return;}
print("\(post.title) tapped")
self.performSegue(withIdentifier: "toPost", sender: post)
}
}
I've tried to replace the self.performsegue with cell.perfomsegue because technically that is where I get the information to pass in my "perfromSegue(identifier:) method. The last thing I was thinking is to somehow call the parent view of the CustomTableViewCell which would be the original ViewController but I cannot call the .performsegue on ViewController or ViewController.sharedController when I create a sharedController

Add an extension to your uitableviewcell
extension UITableViewCell {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.nextResponder()
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
then in didSelectItemAtIndexPath
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
collectionView.deselectItemAtIndexPath(indexPath, animated: true)
if let viewController = parentViewController as? NameOfYourViewController {
viewController.performSegueWithIdentifier("seguename", sender: nil)
}
}

Related

UICollectionView not displaying, cellForItemAt and numberOfItemsInSection not called

I'm trying to make an app which will display a popup with of devices with "right" or "left" names depending on whether left or right button was pressed. However, after I press left or right button, the popup appears with empty CollectionView. I colored it green to make sure that it's size is not {0, 0} and it isn't.
Popup screen
Also, I tried to make sure that cells are not bigger than the CollectionView, and it seems, that they are not. I can't put size restraints on them, though, so maybe they get resized or something.
Storyboard
I tried understanding and using solution for that question, but it seems that my situation is somewhat different. In this question cellForItemMethod was called, but in my case it wasn't and I've got no subclass of UICollectionView, which is, I guess, why I cant override intrinsicContentSize.
Here is the code for PopUpViewController:
import UIKit
protocol PopUpDelegate {
func connect(device: CollectionCell)
}
class PopUpController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
static let identifier: String = "PopUpController"
var parentVC: ViewController?
var left: Bool?
let reuseIdentifier = "cell"
var delegate: PopUpDelegate?
var lastSelectedCell: CollectionCell? = nil
#IBOutlet weak var collectionView: UICollectionView!
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("number of items in section called")
if (left!) {
return (parentVC!.leftDevices.count)
} else {
return (parentVC!.rightDevices.count)
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("cell for item called")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! CollectionCell
print("left: ", left!)
if (left!) {
cell.displayDevice(device: parentVC!.leftDevices[indexPath.row])
} else {
cell.displayDevice(device: parentVC!.rightDevices[indexPath.row])
}
cell.backgroundColor = UIColor.white
cell.selectButton.setTitleColor(UIColor.black, for: UIControl.State.normal)
cell.selectButton.setImage(UIImage(named: "unselectedButton"), for: UIControl.State.normal)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("You selected cell #\(indexPath.item)!")
let cell = collectionView.cellForItem(at: indexPath) as! CollectionCell
cell.setSelected()
lastSelectedCell?.setUnselected()
lastSelectedCell = cell
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
print("parent has ", parentVC?.rightDevices.count, " items")
collectionView.reloadData()
}
static func showPopup(parentVC: UIViewController, left: Bool){
//creating a reference for the dialogView controller
if let popupViewController = UIStoryboard(name: "PopUp", bundle: nil).instantiateViewController(withIdentifier: "PopUpController") as? PopUpController {
popupViewController.modalPresentationStyle = .custom
popupViewController.modalTransitionStyle = .crossDissolve
//setting the delegate of the dialog box to the parent viewController
popupViewController.delegate = parentVC as? PopUpDelegate
popupViewController.left = left
parentVC = parentVC as? ViewController
print("showing left popup: ", left)
//presenting the pop up viewController from the parent viewController
parentVC.present(popupViewController, animated: true)
}
}
#IBAction func onConnectPressed(_ sender: Any) {
self.delegate?.connect(device: lastSelectedCell!)
self.dismiss(animated: true, completion: nil)
}
#IBAction func onDismissPressed(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
Check again leftDevices and rightDevices data passing between ViewController to PopUpController screen and conform delegate and datasources.
collectionView.delegate = self
collectionView.dataSource = self

Protocol not passing to ViewController (Swift)

So I defined the ClickableCell protocol which is supposed to respond to tap gestures on an image. I want to redirect to ViewController2 when the user taps on the image, so my approach is to define a protocol, pass the info of tap to the CurrentViewController, and use the protocol in CurrentViewController to redirect to ViewController2. But I used print statements and it only printed "TAP TAP TAP" and "DONE" but not the portion from CurrentViewController. Can anyone help me figure out the problem? Thanks!
MessageCell.swift
protocol ClickableCell {
func onTapImage(indexPath: IndexPath)
}
class MessageCell: UITableViewCell {
#IBOutlet weak var leftImage: UIImageView!
var cellDelegate: ClickableCell?
var index: IndexPath?
override func awakeFromNib() {
super.awakeFromNib()
self.setupLabelTap()
}
#objc func imageTapped(tapGestureRecognizer: UITapGestureRecognizer){
print("TAP TAP TAP")
cellDelegate?.onTapImage(indexPath: index!)
print("DONE")
}
func setupLabelTap() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTapped(tapGestureRecognizer:)))
self.leftImage.isUserInteractionEnabled = true
self.leftImage.addGestureRecognizer(tapGestureRecognizer)
}
}
CurrentViewController
extension CurrentViewController: ClickableCell {
func onTapImage(indexPath: IndexPath) {
print("Doesn't Print This")
let popOverVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "nextView") as! UIViewController
self.navigationController?.pushViewController(popOverVC, animated: true)
}
}
You need to assign that cellDelegate when you create cell
extension CurrentViewController: UITableViewDataSource {
// other your code here
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "your_message_cell_id",
for: indexPath) as! MessageCell
/// other setup code here
cell.cellDelegate = self // << here !!
return cell
}
}
You need to set the delegate. The core purpose of the delegate pattern is to allow an object to communicate back to its owner in a decoupled way.
So in cellForRowAt method:-
cell.delegate = self

index of button in custom cell

I create a custom cell that contains a button, I need to create segue from this button to other VC but first of all, I would like to push an object with that segue.
I already try to use cell.button.tag, but I did not succeed.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showMap" {
let mapVC = segue.destination as! MapViewController
//guard let indexPath = tableView.indexPathForSelectedRow else { return }
mapVC.place = places[] // <- "here I need index of button in cell"
}
}
Instead of using the segue, handle the navigation programatically through a closure in UITableViewCell.
class CustomCell: UITableViewCell {
var buttonTapHandler: (()->())?
#IBAction func onTapButton(_ sender: UIButton) {
self.buttonTapHandler?()
}
}
In the above code, I've create a buttonTapHandler - a closure, that will be called whenever the button inside the cell is tapped.
Now, in cellForRowAt method when you dequeue the cell, set the buttonTapHandler of CustomCell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.buttonTapHandler = {[weak self] in
if let mapVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MapViewController") as? MapViewController {
mapVC.place = places[indexPath.row]
self?.navigationController?.pushViewController(mapVC, animated: true)
}
}
return cell
}
In the above code, buttonTapHandler when called will push a new instance of MapViewController along with the relevant place based on the indexPath.
if you don't want to execute your code in didSelectRowAt method, another good approach in my opinion is to create a delegate of your custom cell. See the code below
// This is my custom cell class
class MyCustomCell: UITableViewCell {
// The button inside your cell
#IBOutlet weak var actionButton: UIButton!
var delegate: MyCustomCellDelegate?
#IBAction func myDelegateAction(_ sender: UIButton) {
delegate?.myCustomAction(sender: sender, index: sender.tag)
}
// Here you can set the tag value of the button to know
// which button was tapped
func configure(index: IndexPath){
actionButton.tag = index.row
}
}
protocol MyCustomCellDelegate {
func myDelegateAction(sender: UIButton, index: Int)
}
Delegate the ViewController where you use your custom cell.
class MyViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCellIdentifier", for: indexPath) as! MyCustomCell
cell.configure(index: indexPath)
cell.delegate = self
return cell
}
}
And at the end customize your method extending your custom cell delegate
extension MyViewController: MyCustomCellDelegate {
func myDelegateAction(sender: UIButton, index: Int) {
// Do your staff here
}
}
I hope I was helpful.
In the custom cell:
import UIKit
protocol CustomCellDelegate: class {
func btnPressed(of cell: CustomCell?)
}
class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
#IBAction func btnTapped(_ sender: UIButton) {
delegate?.btnPressed(of: self)
}
}
And in the view controller:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: CustomCell = tableView.dequeueReusableCell(for: indexPath)
cell.delegate = self
return cell
}
extension ViewController: CustomCellDelegate {
func btnPressed(of cell: CustomCell?) {
if let cell = cell, let indexPath = tableView.indexPath(for: cell) {
// Your stuff here
}
}
}

Pass data from a button in reused custom cell

I'm having trouble passing data from a custom cell by a user tapping a button in that custom cell. I sometimes get the wrong cells data since the cell is being reused. I was wondering if there was a full proof way to always get the right cell data to its button in each cell no matter which cell is currently on the screen. Below is my code. Any help is greatly appreciated.
My Custom Cell:
protocol CustomCellDelegate {
func segueWithCellData()
}
class CustomTableViewCell : UITableViewCell {
var delegate = CustomCellDelegate?
#IBAction func buttonTapped() {
if let delegate = self.delegate {
delegate.segueWithCellData()
}
}
}
MyTableViewController:
class MyTableViewController : UITableViewController, CustomCellDelegate {
var posts = [Post]()
var title: String!
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCellReuseIdentifier", forIndexPath: indexPath)
title = post.title
cell.delegate = self
return cell
}
func segueWithCellData() {
self.performSegueWithIdentifier("passMyData", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == “passMyData” {
let destination = segue.destinationViewController as! UINavigationController
let targetVC = destination.topViewController as! nextVC
targetVC.title = title
}
}
}
My Custom Cell:
protocol CustomCellDelegate {
func segueWithCellData(cell:CustomTableViewCell)
}
class CustomTableViewCell : UITableViewCell {
var delegate = CustomCellDelegate?
#IBAction func buttonTapped() {
if let delegate = self.delegate {
delegate.segueWithCellData(self)
}
}
}
CustomCellDelegate Method:
func segueWithCellData(cell:CustomTableViewCell) {
//Get indexpath of selected cell here
let indexPath = self.tableView.indexPathForCell(cell)
self.performSegueWithIdentifier("passMyData", sender: self)
}
Hence, no need of tagging cell.
Since, you have indexPath of the selected cell, you can get data from this and pass this through sender parameter of performSegueWithIdentifier method.
For example,
func segueWithCellData(cell:CustomTableViewCell) {
//Get index-path of selected cell here
let selectedIndexPath = self.tableView.indexPathForCell(cell)
let post = posts[selectedIndexPath.row]
self.performSegueWithIdentifier("passMyData", sender: post)
}
and, get the data inside prepareForSegue as follows:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == “passMyData” {
let destination = segue.destinationViewController as! UINavigationController
let targetVC = destination.topViewController as! nextVC
//Get passed data here
let passedPost = sender as! Post
targetVC.title = title
}
}
Full proof solution which i have used in almost all apps. Create a custom property of type NSIndexPath in a category class of UIButton and assign the indexPath in cellForRowAtIndexPath function. Now in the callback of the button find the object at index by the buttons indexPath.row from the datasource. this never fails.
first you have to create a dictionary of index and titles like this in MyTableViewController:
var titleDict = [Int:String]()
set the tag of the cell to index in table view and append title to titleDict like this in MyTableViewController:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCellReuseIdentifier", forIndexPath: indexPath)
title = post.title
let index = indexPath.row
cell.tag = index
titleDict[index] = title
cell.delegate = self
return cell
}
and pass the tag value of that cell in cell delegate method like this in My Custom Cell:
protocol CustomCellDelegate {
func segueWithCellData(index:Int)
}
class CustomTableViewCell : UITableViewCell {
var delegate = CustomCellDelegate?
#IBAction func buttonTapped() {
if let delegate = self.delegate {
let index = self.tag
delegate.segueWithCellData(index)
}
}
}
and access the title from the titleDict with the given index from delegate method and set to title variable in MyTableViewController:
func segueWithCellData(index:Int) {
if let title = titleDict[index]{
self.title = title
}
self.performSegueWithIdentifier("passMyData", sender: self)
}
Simple solution: fill the tableView from an array (String) and update the tableView. If you want change some datas in the tableView you need to update your array and refresh the tableView.
I use this solution in my applications and it works great.

How to update DetailView

I have a swift app based on Master-Detail template. Every row in MasterView table is based on custom cell received from a nib. Every cell includes UIlabel and UIbutton. The logic of the app is following. If user taps on a row DetailView shows some details depending on selected row. The button on the row does not call tableView(_, didSelectRowAtIndexPath). If user taps on the button inside a row only an image belongs to DetailView should be changed (other elements on DetailView remain the same) but it isn't. If I select another row and than select previous row back, changed image is shown on the DetailView as it was foreseen. The question is how to redraw the image in the DetailView just by tapping on the button.
I've tried to do following but with no success:
class MasterViewCell: UITableViewCell {
weak var detailViewController: DetailViewController?
#IBAction func buttonTap(sender: AnyObject) {
//method to set new image
detailViewController!.setNewImage()
detailViewController!.view.setNeedsDisplay()
}
}
class MasterViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "itemCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "Cell")
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? MasterViewCell
cell?.detailView = self.detailViewController
return cell!
}
You need to use a handler
typealias ButtonHandler = (Cell) -> Void
class Cell: UITableViewCell {
var changeImage: ButtonHandler?
func configureButton(changeImage: ButtonHandler?) {
self.changeImage = changeImage
}
#IBAction func buttonTap(sender: UIButton) {
changeImage?(self)
}
}
And in your MasterView
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! Cell
cell.configureButton(setNewImage())
return cell
}
private func setNewImage() -> ButtonHandler {
return { [unowned self] cell in
let row = self.tableView.indexPathForCell(cell)?.row //Get the row that was touched
//set the new Image
}
}
SOURCE: iOS Swift, Update UITableView custom cell label outside of tableview CellForRow using tag
I've found the solution. I've used protocol-delegate mechanism. Now the code is:
//protocol declaration:
protocol MasterViewCellDelegate: class {
func updateImage(sender: MasterViewCell, detVC: DetailViewController)
}
// cell class
class MasterViewCell: UITableViewCell {
weak var masterViewCellDelegate: MasterViewCellDelegate? // protocol property
weak var masterViewController: MasterViewController? {
didSet {
// set delegate
self.masterViewDelegate = masterViewController!.detailViewController
}
}
#IBAction func buttonTap(sender: AnyObject) {
var detVC: DetailViewController?
if let split = masterViewController!.splitViewController {
let controllers = split.viewControllers
detVC = (controllers[controllers.count - 1] as! UINavigationController).topViewController as? DetailViewController
}
// call delegate
masterViewCellDelegate?.updateImage(self, detVC: detVC)
}
class MasterViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "itemCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "Cell")
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? MasterViewCell
cell?.masterViewController = self
return cell!
}
// declare detailviewcontroller as delegate
class DetailViewController: UIViewController, MasterViewCellDelegate {
func updateImage(sender: MasterViewCell, detVC: DetailViewController){
detVC.setNewImage()
}
}
It may well be that this solution is excessively complex, but it works and easy could be adapted for various purposes.

Resources