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.
Related
I'm testing a simple tableView in a UIViewController for fun
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
tableView.dataSource = self
tableView.delegate = self
tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: "CustomTableViewCell")
}
var data = [1,2,3,4,5,6,7]
}
extension ViewController : UITableViewDelegate {
}
extension ViewController : UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath)
cell.textLabel?.text = data[indexPath.row].description
return cell
}
}
and I want to write a test to check that the correct data is being displayed in a presented cell.
My test looks like the following:
var controller: ViewController?
override func setUp() {
controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController") as? ViewController
}
func testViewCell() {
guard let controller = controller else {
return XCTFail("Could not instantiate ViewController")
}
let tableCell = Bundle(for: CustomTableViewCell.self).loadNibNamed("CustomTableViewCell", owner: nil)?.first as! CustomTableViewCell
tableCell.textLabel?.text = "2"
controller.loadViewIfNeeded()
let actualCell = controller.tableView!.cellForRow(at: IndexPath(row: 0, section: 0) )
XCTAssertEqual(actualCell, tableCell)
}
But the actual cell is nil. How can I test the presented cell in my view controller against an expected cell?
In your case I believe you will need to call reloadData on the table view as well. Try:
func testViewCell() {
guard let controller = controller else {
return XCTFail("Could not instantiate ViewController")
}
let tableCell = Bundle(for: CustomTableViewCell.self).loadNibNamed("CustomTableViewCell", owner: nil)?.first as! CustomTableViewCell
tableCell.textLabel?.text = "2"
controller.loadViewIfNeeded()
controller.tableView!.reloadData()
let actualCell = controller.tableView!.cellForRow(at: IndexPath(row: 0, section: 0) )
XCTAssertEqual(actualCell, tableCell)
}
In general for these cases I would also be worried about the view controller size. Since this is not put to any window it might in some cases use some intrinsic size and if that is for instance set to 0 your cells will not be there either. Maybe you should consider creating a window with fixed size (the size you want to test on) and apply your view controller as a root to it.
Also what do you expect to get from XCTAssertEqual(actualCell, tableCell)? Not sure but I would say this tests only pointers and will always fail. You will need to implement your own logic to check equality.
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
}
}
}
I have a tableview inside a VC that has a navigation controller and it contains custom table cells. I was wondering what the best practice is for pushing onto the parent VC's navigation stack if a button in the custom table cell is tapped. I am able to get this to work if i pass the parent VC's navigation controller to the cell; but is this the most effective/efficient practice? Please see my current implementation below:
UserAccountVC:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TextPostTableViewCell = Bundle.main.loadNibNamed("TextPostTableViewCell", owner: self, options: nil)?.first as! TextPostTableViewCell
cell.setupCell(navigationController: self.navigationController!)
cell.selectionStyle = .none
return cell
}
CustomTableCell:
import UIKit
class TextPostTableViewCell: UITableViewCell {
var aNavigationController: UINavigationController!
//MARK: Actions
#IBAction func profilePicButtonTapped() { //We want to present a users profile
let sb = UIStoryboard(name: "SuccessfulLogin", bundle: nil)
let cc = (sb.instantiateViewController(withIdentifier: "otherUserViewController")) as! OtherUserAccountViewController
self.aNavigationController.pushViewController(cc, animated: true)
}
func setupCell(navigationController: UINavigationController) -> Void {
aNavigationController = navigationController
}
}
Thank you in advance!
No, this is not best practice. You can setup an IBAction in interface builder for your UIButton or add your UIViewController as a target in cellForRowAt. With either method you may need some method of identifying the indexPath, since you are not using didSelectRow in your tableview delegate:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TextPostTableViewCell = Bundle.main.loadNibNamed("TextPostTableViewCell", owner: self, options: nil)?.first as! TextPostTableViewCell
cell.button.tag = indexPath.row // Or use some other method of identifying your data in `myAction(_:)`
cell.button.addTarget(self, action:, #selector(myAction(_:)), for: .touchUpInside)
...
}
You can use delegate in this situation.
The code is a bit more here, but this is better way in iOS development IMO.
class ViewController: UIViewController {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TextPostTableViewCell = Bundle.main.loadNibNamed("TextPostTableViewCell", owner: self, options: nil)?.first as! TextPostTableViewCell
cell.delegate = self
cell.selectionStyle = .none
return cell
}
}
extension ViewController: TextPostTableViewCellDelegate {
func didTappedProfilePicButton() {
let sb = UIStoryboard(name: "SuccessfulLogin", bundle: nil)
let cc = (sb.instantiateViewController(withIdentifier: "otherUserViewController")) as! OtherUserAccountViewController
navigationController?.pushViewController(cc, animated: true)
}
}
protocol TextPostTableViewCellDelegate: class {
func didTappedProfilePicButton()
}
class TextPostTableViewCell: UITableViewCell {
weak var delegate: TextPostTableViewCellDelegate?
//MARK: Actions
#IBAction func profilePicButtonTapped() { //We want to present a users profile
delegate?.didTappedProfilePicButton()
}
}
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.
I am using Xib files in my project for building interface of my app.
I have a tableView in my first viewController from which I want to pass data to next ViewController. I have created a custom cell for my tableView which contains an imageView and two labels.
This is my code
import UIKit
class YASProductListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// registering my custom cell
tableView.registerNib(UINib(nibName: "YASProductListTableViewCell", bundle: NSBundle.mainBundle()), forCellReuseIdentifier: "cell")
let cell : YASProductListTableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as! YASProductListTableViewCell
cell.productNameLabel.text = prodcutNames[indexPath.row]
cell.productDetailLabel.text = productDetail[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return prodcutNames.count
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 140
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath) as! YASProductListTableViewCell
let destination = YASProductDetaiilViewController(nibName: "YASProductDetaiilViewController", bundle: NSBundle.mainBundle())
destination.productImage = cell.productImageView.image
destination.productTitle = cell.productNameLabel.text!
let productDetails = YASProductDetaiilViewController(nibName: "YASProductDetaiilViewController", bundle: nil) as YASProductDetaiilViewController
navigationController?.navigationBarHidden = false
navigationController?.title = ""
navigationController?.pushViewController(productDetails, animated: true)
}
Now what I want to do is pass the image and labels text to next viewController when user tap on any cell. Here is the code of next ViewController
import UIKit
class YASProductDetaiilViewController: UIViewController {
#IBOutlet weak var productImageView: UIImageView!
#IBOutlet weak var productTitleLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setupViewControllerUI()
// Do any additional setup after loading the view.
}
// MARK: - UIViewController helper Methods
func setupViewControllerUI(){
productImageView.image = productImage
productTitleLabel.text = productTitle
}
}
As you can see I have tried it didSelectRowAtIndexPath but its not working. Please help! Thanks
You are using right method to share data between viewController. However you have made a mistake. You are creating two instance of your ProductDetailViewController. You need to create only one instance of destination ViewCotroller and then set its properties accordingly you you can simply replace your didSelectRowAtIndexPath method with following
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath) as! YASProductListTableViewCell
let productDetails = YASProductDetaiilViewController(nibName: "YASProductDetaiilViewController", bundle: nil) as YASProductDetaiilViewController
productDetails.productImage = cell.productImageView.image
productDetails.productTitle = cell.productNameLabel.text!
navigationController?.pushViewController(productDetails, animated: true)
}
I hope it will work.
Change your didSelect method like below code,
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath) as! YASProductListTableViewCell
let productDetails = YASProductDetaiilViewController(nibName: "YASProductDetaiilViewController", bundle: nil) as YASProductDetaiilViewController
navigationController?.navigationBarHidden = false
navigationController?.title = ""
productDetails.productImage = cell.productImageView.image
productDetails.productTitle = cell.productNameLabel.text!
navigationController?.pushViewController(productDetails, animated: true)
}
Hope this helps you.
Do as follow might be help you.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath) as! YASProductListTableViewCell
let objYASProductDetaiilViewController = self.storyboard?.instantiateViewControllerWithIdentifier("STRORYBOARD_ID") as? YASProductDetaiilViewController
objYASProductDetaiilViewController.productImage = cell.productImageView.image
objYASProductDetaiilViewController.productTitle = cell.productNameLabel.text!
self.navigationController?.pushViewController(objMedicalDevicesVC!, animated: true)
navigationController?.navigationBarHidden = false
navigationController?.title = ""
navigationController?.pushViewController(objYASProductDetaiilViewController, animated: true)
}
Here You can not assign a image View to image View directly.You need to get a image first then it give to a other view controller image then set it to image to UIImage
import UIKit
class YASProductDetaiilViewController: UIViewController {
#IBOutlet weak var productImageView: UIImage!
#IBOutlet weak var productTitleLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setupViewControllerUI()
// Do any additional setup after loading the view.
}
// MARK: - UIViewController helper Methods
func setupViewControllerUI(){
productImageView.image = productImage
productTitleLabel.text = productTitle
}
}
create dictionary of all the needed information and the add to array and get using indexpath.row when cell is clicked
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var dicTemp=prodcutNames[indexPath.row] as! NSMutableDictionary
let destination = YASProductDetaiilViewController(nibName: "YASProductDetaiilViewController", bundle: NSBundle.mainBundle())
destination.productImage = prodcutNames.valueForKey("imageObj") //get image object
destination.productTitle = prodcutNames.valueForKey("product_title")//get product title
let productDetails = YASProductDetaiilViewController(nibName: "YASProductDetaiilViewController", bundle: nil) as YASProductDetaiilViewController
navigationController?.navigationBarHidden = false
navigationController?.title = ""
navigationController?.pushViewController(productDetails, animated: true)
}