Passing current UITableViewCell swipe to parent UITableView - ios

I have a custom UITableViewCell, which has a functionality to be swiped to the right. The swipe gesture is based on the translation of x-axis, so, when the translation of x is exceeded 40 points, I want to trigger a segue.
I think this is a perfect place to use delegates to pass data about the current X value. So, I have created a protocol with a function didSwipeCell(), but I'm not sure how to pass the current x value to the UITableView.
Please let me know how to do it, and if you need any extra info, please let me know in the comment instead of downvoting.

Add x value as a parameter of didSwipeCell() method
protocol TableViewCellDelegate {
func didSwipeCell(on xValue: Float)
}
Add the delegate instance to your cell
class TableViewCell: UITableViewCell {
weak var delegate: TableViewCellDelegate?
}
Then, call the method when user swipes the cell giving xValue
delegate?.didSwipeCell(on: xValue)
In your UITableView implement TableViewCellDelegate method.
class TableView: UITableView, TableViewCellDelegate {
func didSwipeCell(on xValue: Float) {
//here you can get the xValue
}
And, do not forget to set the delegate of TableViewCell in your cellForRowAtIndexPath method
cell.delegate = self

You can get value of x simply from your UItableViewCell Class to your UIViewController. Declare closure
var getValue: ((_ xValue: CGFloat)->())!
in your UItableViewCell Class and implement it into your UIViewController Class.
In ViewController
cell. getValue = { (xValue) in
// do what you want to do
}

Here is another way to do it...
class SwipeTableViewCell: UITableViewCell {
var didSwipeAction : ((CGFloat)->())?
var startLocation = CGPoint.zero
var theSwipeGR: UISwipeGestureRecognizer?
func respondToSwipeGesture(_ sender: UIGestureRecognizer) {
if let swipe = sender as? UISwipeGestureRecognizer {
if (swipe.state == UIGestureRecognizerState.began) {
startLocation = swipe.location(in: self.contentView);
} else if (swipe.state == UIGestureRecognizerState.ended) {
let stopLocation = swipe.location(in: self.contentView);
let dx = stopLocation.x - startLocation.x;
if dx > 40 {
// pass the ending X coordinate in the callback
didSwipeAction?(stopLocation.x)
}
}
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
myInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
myInit()
}
func myInit() -> Void {
if theSwipeGR == nil {
let g = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture(_:)))
g.direction = UISwipeGestureRecognizerDirection.right
self.contentView.addGestureRecognizer(g)
theSwipeGR = g
}
}
}
and then your table view cell setup becomes:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SwipeCell", for: indexPath) as! SwipeTableViewCell
cell.didSwipeAction = {
(xValue) in
print("Simple", indexPath, "X =", xValue)
// call performSegue() or do something else...
}
return cell
}

Related

Open URL with a button inside a table view cell

I want to include a button in each table cell that opens a URL.
I've created tables (using an array) with images and labels just fine, however I'm confused how to create a button
Here's what I have so far
class ExploreCell: UITableViewCell {
#IBOutlet weak var exploreImageView: UIImageView!
#IBOutlet weak var exploreTitleView: UILabel!
#IBOutlet weak var exploreDescriptionView: UILabel!
#IBOutlet weak var exploreButton: UIButton!
func setExplore(explore: Explore) {
exploreImageView.image = explore.image
exploreTitleView.text = explore.title
exploreDescriptionView.text = explore.description
exploreButton.addTarget(self, action: "connected:", for: .touchUpInside) = explore.button
}
My Class for the array looks like this
class ExploreListScreen: UIViewController {
#IBOutlet weak var tableView: UITableView!
var explores: [Explore] = []
override func viewDidLoad() {
super.viewDidLoad()
explores = createArray ()
tableView.delegate = self
tableView.dataSource = self
}
func createArray() -> [Explore] {
var tempExplores: [Explore] = []
let explore1 = Explore(image: #imageLiteral(resourceName: "test"), title: "Demo", description: "Essential", button: "")
tempExplores.append(explore1)
return tempExplores
}
Finally I have another file which contains the declared variables
class Explore {
var image: UIImage
var title: String
var description: String
var button: UIButton
init(image: UIImage, title: String, description: String, button: UIButton) {
self.image = image
self.title = title
self.description = description
self.button = button
}
Any advice and guidance would be fantastic. Thank-you!
Here's how I usually solve this. Create a delegate for your UITableViewCell subclass, and set the view controller owning the tableView as its delegate. Add methods for the interactions that happens inside the cell.
protocol YourTableViewCellDelegate: class {
func customCellDidPressUrlButton(_ yourTableCell: YourTableViewCell)
}
class YourTableViewCell: UITableViewCell {
weak var delegate: YourTableViewCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let button = UIButton()
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
addSubview(button)
}
required init?(coder _: NSCoder) {
return nil
}
#objc func buttonTapped() {
delegate?.customCellDidPressUrlButton(self)
}
}
Then, in the controller, set itself as a delegate and get the indexPath trough the proper method, indexPath(for:)
class YourTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! YourTableViewCell
cell.delegate = self
return cell
}
}
extension YourTableViewController: YourTableViewCellDelegate {
func customCellDidPressUrlButton(_ yourTableCell: YourTableViewCell) {
guard let indexPath = tableView.indexPath(for: yourTableCell) else { return }
print("Link button pressed at \(indexPath)")
}
}
Then use that indexPath to grab the correct URL and present it from your table viewcontroller with a SFSafariViewController.
Swift 4
This is best way to get indexPath using touchPoint
class YourTableViewController: UITableViewController {
// ...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SwiftyCell", for: indexPath) as! SwiftyTableViewCell
cell.label.text = "This is cell number \(indexPath.row)"
// WRONG! When cells get reused, these actions will get added again! That's not what we want.
// Of course, we could get around this by jumping through some hoops, but maybe there's a better solution...
cell.yourButton.addTarget(self, action: #selector(self.yourButtonTapped(_:)), for: .touchUpInside)
return cell
}
func yourButtonTapped(_ sender: Any?) {
let point = tableView.convert(sender.center, from: sender.superview!)
if let wantedIndexPath = tableView.indexPathForItem(at: point) {
let cell = tableView.cellForItem(at: wantedIndexPath) as! SwiftyCell
}
}
// ...
}
For more details you can follow this tutorials
Just create UIButton object in viewDidLoad and add this button as a sub view on cell in cellForRowAtIndexPath function. Take Burton's frame as per your requirement.

How to preserve user input in UITableViewCell before dequeue

I'm creating an application in which I need the users to fill out a number of inputs in a UITableViewCell, kinda like a form. When the user taps on the done button, I need to collect those inputs so I can run some calculations and output them on another view controller
Here is the method I used to collect those inputs:
func doneButtonTapped() {
var dict = [String: Any]()
for rows in 0...TableViewCells.getTableViewCell(ceilingType: node.ceilingSelected, moduleType: node.moduleSelected).count {
let ip = IndexPath(row: rows, section: 0)
let cells = tableView.cellForRow(at: ip)
if let numericCell = cells as? NumericInputTableViewCell {
if let text = numericCell.userInputTextField.text {
dict[numericCell.numericTitleLabel.text!] = text
}
} else if let booleanCell = cells as? BooleanInputTableViewCell {
let booleanSelection = booleanCell.booleanToggleSwitch.isOn
dict[booleanCell.booleanTitleLabel.text!] = booleanSelection
}
}
let calculator = Calculator(userInputDictionary: dict, ceiling_type: node.ceilingSelected)
}
The problem I'm having is when the cell is out of view, the user's input is cleared from the memory. Here are two screenshots to illustrate my point:
As you can see, when all the cells appears, the done button managed to store all the inputs from the user, evidently from the console print. However, if the cells are out of view, the inputs from area/m2 are set to nil:
The solution that came to mind was I shouldn't use a dequeue-able cell as I do want the cell to be in memory when it is out-of-view, but many of the stackover community strong against this practice. How should I solve this problem? Thanks!
UPDATE
Code for cellForRow(at: IndexPath)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let node = node else {
return UITableViewCell()
}
let cellArray = TableViewCells.getTableViewCell(ceilingType: node.ceilingSelected, moduleType: node.moduleSelected)
switch cellArray[indexPath.row].cellType {
case .numericInput :
let cell = tableView.dequeueReusableCell(withIdentifier: "numericCell", for: indexPath) as! NumericInputTableViewCell
cell.numericTitleLabel.text = cellArray[indexPath.row].title
return cell
case .booleanInput :
let cell = tableView.dequeueReusableCell(withIdentifier: "booleanCell", for: indexPath) as! BooleanInputTableViewCell
cell.booleanTitleLabel.text = cellArray[indexPath.row].title
return cell
}
}
}
My two custom cells
NumericInputTableViewCell
class NumericInputTableViewCell: UITableViewCell {
#IBOutlet weak var numericTitleLabel: UILabel!
#IBOutlet weak var userInputTextField: UITextField!
}
BooleanInputTableViewCell
class BooleanInputTableViewCell: UITableViewCell {
#IBOutlet weak var booleanTitleLabel: UILabel!
#IBOutlet weak var booleanToggleSwitch: UISwitch!
}
Any takers?
I agree with the other contributors. The cells should not be used for data storage. You should consider another approach (like the one HMHero suggests).
But, as your question was also about how to access a UITableViewCell before it is removed, there is a method in UITableViewDelegate that you can use for that:
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// do something with the cell before it gets deallocated
}
This method tells the delegate that the specified cell was removed from the table. So it gives a last chance to do something with that cell before it disappears.
Because of table view reuses its cells, usually, it's not a good idea if your data depends on some components from the table view cell. Rather, it should be the other way around. Your table view data always drive it's table view cell's component even before any user input data is provided in your case.
Initial Data - your should already have somewhere in your code. I created my own from your provided code
let data = CellData()
data.title = "Troffer Light Fittin"
data.value = false
let data2 = CellData()
data2.title = "Length Drop"
data2.value = "0"
cellData.append(data)
cellData.append(data2)
Example
enum CellType {
case numericInput, booleanInput
}
class CellData {
var title: String?
var value: Any?
var cellType: CellType {
if let _ = value as? Bool {
return .booleanInput
} else {
return .numericInput
}
}
}
protocol DataCellDelegate: class {
func didChangeCellData(_ cell: UITableViewCell)
}
class DataTableViewCell: UITableViewCell {
var data: CellData?
weak var delegate: DataCellDelegate?
}
class NumericInputTableViewCell: DataTableViewCell {
let userInputTextField: UITextField = UITextField()
override var data: CellData? {
didSet {
textLabel?.text = data?.title
if let value = data?.value as? String {
userInputTextField.text = value
}
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
userInputTextField.addTarget(self, action: #selector(textDidChange(_:)), for: .editingChanged)
contentView.addSubview(userInputTextField)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func textDidChange(_ textField: UITextField) {
//update data and let the delegate know data is updated
data?.value = textField.text
delegate?.didChangeCellData(self)
}
//Disregard this part
override func layoutSubviews() {
super.layoutSubviews()
textLabel?.frame.size.height = bounds.size.height / 2
userInputTextField.frame = CGRect(x: (textLabel?.frame.origin.x ?? 10), y: bounds.size.height / 2, width: bounds.size.width - (textLabel?.frame.origin.x ?? 10), height: bounds.size.height / 2)
}
}
class BooleanInputTableViewCell: DataTableViewCell {
override var data: CellData? {
didSet {
textLabel?.text = data?.title
if let value = data?.value as? Bool {
booleanToggleSwitch.isOn = value
}
}
}
let booleanToggleSwitch = UISwitch(frame: .zero)
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
booleanToggleSwitch.addTarget(self, action: #selector(toggled), for: .valueChanged)
booleanToggleSwitch.isOn = true
accessoryView = booleanToggleSwitch
accessoryType = .none
selectionStyle = .none
}
func toggled() {
//update data and let the delegate know data is updated
data?.value = booleanToggleSwitch.isOn
delegate?.didChangeCellData(self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In View Controller, you should update your original data source so when you scroll the table view, the data source privide right infomation.
func didChangeCellData(_ cell: UITableViewCell) {
if let cell = cell as? DataTableViewCell {
for data in cellData {
if let title = data.title, let titlePassed = cell.data?.title, title == titlePassed {
data.value = cell.data?.value
}
}
}
for data in cellData {
print("\(data.title) \(data.value)")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = cellData[indexPath.row]
let cell: DataTableViewCell
if data.cellType == .booleanInput {
cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(BooleanInputTableViewCell.self), for: indexPath) as! BooleanInputTableViewCell
} else {
cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(NumericInputTableViewCell.self), for: indexPath) as! NumericInputTableViewCell
}
cell.data = cellData[indexPath.row]
cell.delegate = self
return cell
}
In short, try to have a single data source for table view and use the delegate to pass the updated data in the cell back to the data source.
Please disregard anything that has to do with layout. I didn't use the storyboard to test your requirements.

get indexPath of UITableViewCell on click of Button from Cell

I have a button (red color cross) in the UITableViewCell and on click of that button I want to get indexPath of the UITableViewCell.
Right now I am assigning tag to each of the button like this
cell.closeButton.tag = indexPath.section
and the on click of the button I get the indexPath.section value like this:
#IBAction func closeImageButtonPressed(sender: AnyObject) {
data.removeAtIndex(sender.tag)
tableView.reloadData()
}
Is this the right way of implementation or is there any other clean way to do this?
Use Delegates:
MyCell.swift:
import UIKit
//1. delegate method
protocol MyCellDelegate: AnyObject {
func btnCloseTapped(cell: MyCell)
}
class MyCell: UICollectionViewCell {
#IBOutlet var btnClose: UIButton!
//2. create delegate variable
weak var delegate: MyCellDelegate?
//3. assign this action to close button
#IBAction func btnCloseTapped(sender: AnyObject) {
//4. call delegate method
//check delegate is not nil with `?`
delegate?.btnCloseTapped(cell: self)
}
}
MyViewController.swift:
//5. Conform to delegate method
class MyViewController: UIViewController, MyCellDelegate, UITableViewDataSource,UITableViewDelegate {
//6. Implement Delegate Method
func btnCloseTapped(cell: MyCell) {
//Get the indexpath of cell where button was tapped
let indexPath = self.collectionView.indexPathForCell(cell)
print(indexPath!.row)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCell") as! MyCell
//7. delegate view controller instance to the cell
cell.delegate = self
return cell
}
}
How to get cell indexPath for tapping button in Swift 4 with button selector
#objc func buttonClicked(_sender:UIButton){
let buttonPosition = sender.convert(CGPoint.zero, to: self.tableView)
let indexPath = self.tableView.indexPathForRow(at:buttonPosition)
let cell = self.tableView.cellForRow(at: indexPath) as! UITableViewCell
print(cell.itemLabel.text)//print or get item
}
Try with the best use of swift closures : Simple, Quick & Easy.
In cellForRowAtIndexPath method:
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCellIdentifier", for: indexPath) as! CustomCell
cell.btnTick.mk_addTapHandler { (btn) in
print("You can use here also directly : \(indexPath.row)")
self.btnTapped(btn: btn, indexPath: indexPath)
}
Selector Method for external use out of cellForRowAtIndexPath method:
func btnTapped(btn:UIButton, indexPath:IndexPath) {
print("IndexPath : \(indexPath.row)")
}
Extension for UIButton :
extension UIButton {
private class Action {
var action: (UIButton) -> Void
init(action: #escaping (UIButton) -> Void) {
self.action = action
}
}
private struct AssociatedKeys {
static var ActionTapped = "actionTapped"
}
private var tapAction: Action? {
set { objc_setAssociatedObject(self, &AssociatedKeys.ActionTapped, newValue, .OBJC_ASSOCIATION_RETAIN) }
get { return objc_getAssociatedObject(self, &AssociatedKeys.ActionTapped) as? Action }
}
#objc dynamic private func handleAction(_ recognizer: UIButton) {
tapAction?.action(recognizer)
}
func mk_addTapHandler(action: #escaping (UIButton) -> Void) {
self.addTarget(self, action: #selector(handleAction(_:)), for: .touchUpInside)
tapAction = Action(action: action)
}
}
In Swift 4 , just use this:
func buttonTapped(_ sender: UIButton) {
let buttonPostion = sender.convert(sender.bounds.origin, to: tableView)
if let indexPath = tableView.indexPathForRow(at: buttonPostion) {
let rowIndex = indexPath.row
}
}
You can also get NSIndexPath from CGPoint this way:
#IBAction func closeImageButtonPressed(sender: AnyObject) {
var buttonPosition = sender.convertPoint(CGPointZero, to: self.tableView)
var indexPath = self.tableView.indexPathForRow(atPoint: buttonPosition)!
}
Create a custom class of UIButton and declare a stored property like this and use it to retrieve assigned indexPath from callFroRowAtIndexPath.
class VUIButton: UIButton {
var indexPath: NSIndexPath = NSIndexPath()
}
This is the full proof solution that your indexPath will never be wrong in any condition. Try once.
//
// ViewController.swift
// Table
//
// Created by Ngugi Nduung'u on 24/08/2017.
// Copyright © 2017 Ngugi Ndung'u. All rights reserved.
//
import UIKit
class ViewController: UITableViewController{
let identifier = "cellId"
var items = ["item1", "2", "3"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.title = "Table"
tableView.register(MyClass.self, forCellReuseIdentifier: "cellId")
}
//Return number of cells you need
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! MyClass
cell.controller = self
cell.label.text = items[indexPath.row]
return cell
}
// Delete a cell when delete button on cell is clicked
func delete(cell: UITableViewCell){
print("delete")
if let deletePath = tableView.indexPath(for: cell){
items.remove(at: deletePath.row)
tableView.deleteRows(at: [deletePath], with: .automatic)
}
}
}
class MyClass : UITableViewCell{
var controller : ViewController?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
let label : UILabel = {
let label = UILabel()
label.text = "My very first cell"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let btn : UIButton = {
let bt = UIButton(type: .system)
bt.translatesAutoresizingMaskIntoConstraints = false
bt.setTitle("Delete", for: .normal)
bt.setTitleColor(.red, for: .normal)
return bt
}()
func handleDelete(){
controller?.delete(cell: self)
}
func setUpViews(){
addSubview(label)
addSubview(btn)
btn.addTarget(self, action: #selector(MyClass.handleDelete), for: .touchUpInside)
btn.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
label.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 16).isActive = true
label.widthAnchor.constraint(equalTo: self.widthAnchor , multiplier: 0.8).isActive = true
label.rightAnchor.constraint(equalTo: btn.leftAnchor).isActive = true
}
}
Here is a full example that will answer your question.
In your cellForRow:
#import <objc/runtime.h>
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
setAssociatedObject(object: YOURBUTTON, key: KEYSTRING, value: indexPath)
}
#IBAction func closeImageButtonPressed(sender: AnyObject) {
let val = getAssociatedObject(object: sender, key: KEYSTROKING)
}
Here val is your indexPath object, your can pass any object like you can assign pass cell object and get it in button action.
try this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = (tableView.dequeueReusableCell(withIdentifier: "MainViewCell", forIndexPath: indexPath) as! MainTableViewCell)
cell.myButton().addTarget(self, action: Selector("myClickEvent:event:"), forControlEvents: .touchUpInside)
return cell
}
this function get the position of row click
#IBAction func myClickEvent(_ sender: Any, event: Any) {
var touches = event.allTouches()!
var touch = touches.first!
var currentTouchPosition = touch.location(inView: feedsList)
var indexPath = feedsList.indexPathForRow(atPoint: currentTouchPosition)!
print("position:\(indexPath.row)")
}
class MyCell: UICollectionViewCell {
#IBOutlet weak var btnPlus: UIButton!
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
cell.btnPlus.addTarget(self, action: #selector(increment_Action(sender:)),
for: .touchUpInside)
cell.btnPlus.tag = indexPath.row
cell.btnPlus.superview?.tag = indexPath.section
}
#objc func increment_Action(sender: UIButton) {
let btn = sender as! UIButton
let section = btn.superview?.tag ?? 0
let row = sender.tag
}

UIButton inside custom UITableViewCell causes button to be selected in multiple cells

The goal of the UITableView is to display names with a custom styled checkbox beside it. But somehow when I tap the checkbox, multiple checkboxes in the UITableView get selected.
I've googled and implemented multiple solutions from different questions but none of them seem to work.
Custom UITableViewCell
import UIKit
class NameTableViewCell: UITableViewCell {
var name = UILabel()
let checkboxImage_unchecked = UIImage(named: "cellViewCheckbox_unchecked")
let checkboxImage_checked = UIImage(named: "cellViewCheckbox_checked")
let checkbox:UIButton
weak var delegate:HandleCellInteractionDelegate?
override init(style: UITableViewCellStyle, reuseIdentifier: String!) {
self.checkbox = UIButton(frame: CGRectMake(35, 16, self.checkboxImage_unchecked!.size.width/2, self.checkboxImage_unchecked!.size.height/2))
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = UIColor.transparant()
self.checkbox.setBackgroundImage(self.checkboxImage_unchecked, forState: .Normal)
self.checkbox.setBackgroundImage(self.checkboxImage_checked, forState: .Selected)
self.name = UILabel(frame: CGRectMake(97,18,200, 30))
self.addSubview(self.name)
self.addSubview(self.checkbox)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func checkboxPushed(sender:AnyObject){
self.checkbox.selected = !self.checkbox.selected
self.delegate?.didPressButton(self)
}
In the protocol I declare a method didPressButton as a callback for when the button is pressed (the delegate needs to implement it)
Protocol
import UIKit
protocol HandleCellInteractionDelegate:class {
func didPressButton(cell:NameTableViewCell)
}
The ViewController
import UIKit
class NameViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, HandleCellInteractionDelegate{
var nameView:NameView {
get {
return self.view as! NameView
}
}
var names:[Name]?
var firstLetters:[String]?
let cellIdentifier = "nameCell"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//self.view.backgroundColor = UIColor.darkSalmon()
}
override func loadView() {
let bounds = UIScreen.mainScreen().bounds
self.view = NameView(frame: bounds)
if(NSUserDefaults.standardUserDefaults().objectForKey("gender") !== nil){
self.loadJSON()
self.getFirstLetters();
self.nameView.tableView.delegate = self;
self.nameView.tableView.dataSource = self;
self.nameView.tableView.registerClass(NameTableViewCell.classForCoder(), forCellReuseIdentifier: "nameCell")
self.nameView.tableView.separatorStyle = .None
self.nameView.tableView.rowHeight = 66.5 }
}
func loadJSON(){
let path = NSBundle.mainBundle().URLForResource("names", withExtension: "json")
let jsonData = NSData(contentsOfURL: path!)
self.names = NamesFactory.createFromJSONData(jsonData!, gender: NSUserDefaults.standardUserDefaults().integerForKey("gender"))
}
func getFirstLetters(){
var previous:String = ""
for name in self.names! {
let firstLetter = String(name.name.characters.prefix(1))
if firstLetter != previous {
previous = firstLetter
self.firstLetters?.append(firstLetter)
print(firstLetter)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (self.names!.count)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return nameCellAtIndexPath(indexPath)
}
func nameCellAtIndexPath(indexPath:NSIndexPath) -> NameTableViewCell {
let cell = self.nameView.tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! NameTableViewCell
self.setNameForCell(cell, indexPath: indexPath)
cell.delegate = self;
cell.checkbox.addTarget(cell, action: "checkboxPushed:", forControlEvents: .TouchUpInside)
return cell
}
func setNameForCell(cell:NameTableViewCell, indexPath:NSIndexPath) {
let item = self.names![indexPath.row] as Name
cell.name.text = item.name
}
func didPressButton(cell: NameTableViewCell) {
print(cell)
}
}
What am I doing wrong & how can I fix it?
You must have to save the check box state. Add one more attribute say checkboxStatus to your Model(Name).I hope you will have indexpath.row.
After this line in nameCellAtIndexPath
cell.delegate = self;
set the tag like this
cell.tag = indexPath.row;
In the delegate method,
func didPressButton(cell: NameTableViewCell) {
print(cell)
let item = self.names![cell.tag] as Name
item.checkboxstatus = cell.checkbox.selected
}
Just one crucial change,
func nameCellAtIndexPath(indexPath:NSIndexPath) -> NameTableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! NameTableViewCell
self.setNameForCell(cell, indexPath: indexPath)
cell.delegate = self;
cell.tag = indexPath.row;
cell.checkbox.addTarget(cell, action: "checkboxPushed:", forControlEvents: .TouchUpInside)
let item = self.names![cell.tag] as Name
cell.checkbox.selected = item.checkboxstatus
return cell
}
Cheers!
This happens because the UITableViewCell are being reused.
You changed the cell when you press the checkbox, you need to keep track of that in your data source model.
In cellForRowAtIndexPath you have to add a condition to check if that checkbox was checked or not. Then you display the appropriate view accordingly.
Edit:
In your example you need to check in nameCellAtIndexPath if the checkbox is checked or not and then display it correct.

Tappable UITableViewCell with an also tappable button in it

I have a prototype cell that has some labels on it and a button (well, its actually an imageView, not a button):
I want to achieve this behavior:
Tap on the button executes certain code, say println("foo"), but doesn't perform the "show detail" segue
Tap on the rest of the cell performs a show detail segue
If requirement #1 wasn't necessary, I'd do this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedPlace = places[indexPath.row]
self.performSegueWithIdentifier("ShowPlaceSegue", sender: self)
}
What is the recommended way to achieve this?
This is not like HTML DOM events? (z-index, etc)
I tried (in a very naif attempt) the following:
class PlaceTableViewCell: UITableViewCell {
#IBOutlet weak var favoritedImageView: UIImageView!
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var administrativeAreaLevel3: UILabel!
func configureCellWith(place: Place) {
nameLabel.text = place.name
administrativeAreaLevel3.text = place.administrativeAreaLevel3
favoritedImageView.addGestureRecognizer(UIGestureRecognizer(target: self, action:Selector("bookmarkTapped:")))
}
func bookmarkTapped(imageView: UIImageView) {
println("foo")
}
}
But no matter if I click the imageView or the rest of the cell, the "show detail" segue is performed and the "foo" isn't printed.
What do you think of putting a UIView, "v", inside the prototype cell that contains the labels and making "v" tappable? something like this:
If I do that, will the cell be grayed while tapped? I'd like to keep that...
Sorry, it was a stupid problem:
The "naif" way was indeed the way to go. Indeed it works like HTML DOM!...
But I changed this:
func configureCellWith(place: Place) {
nameLabel.text = place.name
administrativeAreaLevel3.text = place.administrativeAreaLevel3
favoritedImageView.addGestureRecognizer(UIGestureRecognizer(target: self, action:Selector("bookmarkTapped:")))
}
func bookmarkTapped(imageView: UIImageView) {
println("foo")
}
For this:
func configureCellWith(place: Place) {
nameLabel.text = place.name
administrativeAreaLevel3.text = place.administrativeAreaLevel3
let gestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("bookmarkTapped:"))
gestureRecognizer.numberOfTapsRequired = 1
favoritedImageView.userInteractionEnabled = true
favoritedImageView.addGestureRecognizer(gestureRecognizer)
}
func bookmarkTapped(sender: UIImageView!) {
println("foo")
}
As you can see, I was using UIGestureRecognizer instead of UITapGestureRecognizer
EDIT:
So, the above is right, but now I think its better to have the action function in the class that contains the tableView, instead of having the action in the cell class itself.
So, i've moved the addGestureRecognizer to the cellForRowAtIndexPath method, ie:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PlacePrototype", forIndexPath: indexPath) as! PlaceTableViewCell
// Configure the cell
let place = places[indexPath.row]
cell.configureCellWith(place)
// HERE!
let gestureRecognizer = UITapGestureRecognizer(target: self, action:Selector("bookmarkTapped:"))
gestureRecognizer.numberOfTapsRequired = 1
cell.favoritedImageView.userInteractionEnabled = true
cell.favoritedImageView.addGestureRecognizer(gestureRecognizer)
return cell
}
And the action:
func bookmarkTapped(gestureRecognizer: UIGestureRecognizer) {
// println("foo")
var point = gestureRecognizer.locationInView(self.tableView)
if let indexPath = self.tableView.indexPathForRowAtPoint(point)
{
places[indexPath.row].toggleBookmarked()
self.tableView.reloadData()
}
}
Assuming your tableView can be displayed five cells, then the cellForRow will to be called five times, and you will add an UITapGestureRecognizer to five imageView of different. but when you scrolling to the seventh cell, you will got a reused cell(maybe the first cell) in the cellForRow, the imageView of the cell had an UITapGestureRecognizer, if you add UITapGestureRecognizer to the imageView again will cause you tap once trigger multiple times.
You can try this:
class PlaceTableViewCell: UITableViewCell {
#IBOutlet weak var favoritedImageView: UIImageView!
var favoritedTappedBlock: ((Void) -> Void)? // block as callback
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("favoritedImageViewTapped"))
gestureRecognizer.numberOfTapsRequired = 1
favoritedImageView.addGestureRecognizer(gestureRecognizer)
favoritedImageView.userInteractionEnabled = true
}
private func favoritedImageViewTapped() {
if let favoritedTappedBlock = self.favoritedTappedBlock {
favoritedTappedBlock()
}
}
}
And cellForRow:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PlacePrototype", forIndexPath: indexPath) as! PlaceTableViewCell
// Configure the cell
let place = places[indexPath.row]
cell.configureCellWith(place)
cell.favoritedTappedBlock = {
println("tapped")
}
return cell
}

Resources