I have a UITableView in a UIViewController. I have a button at the bottom of this view controller. All the table cells contain a checkbox, which is a subclass of UIButton and a UILabel. When the button is pressed, I want to fetch the text of labels for all the 'checked' cells. Now I can't use cellForRowAtIndexPath because that returns a nil value for cells that are not visible. I therefore want to index my datasource. But the problem is, since UIButton does not have a UIButtonDelegate like UITextfield, I don't know how to capture the button clicks and store the indexPath rows for the cells for which the checkbox was checked. Please help! Thanks a lot!
This is my checkbox code, for reference
class CheckBox: UIButton {
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/
// images
var checkedImage = UIImage(named: "check")
var unCheckedImage = UIImage(named: "uncheck")
// bool property
var isChecked: Bool = false {
didSet {
if isChecked == true {
self.setImage(checkedImage, forState: .Normal)
} else {
self.setImage(unCheckedImage, forState: .Normal)
}
}
}
override func awakeFromNib() {
self.addTarget(self, action: "buttonClicked:", forControlEvents: UIControlEvents.TouchUpInside)
self.isChecked = false
}
func buttonClicked(sender: UIButton) {
if (sender == self) {
if isChecked == true {
isChecked = false
} else {
isChecked = true
}
}
}
}
You can implement a tableviewcell. In that you have to create all elements in you cell. And in that cell class you have to maintain this variables
var indexofRow // Set this at the time of setting cell for UITableView .
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) - > UITableViewCell {
let cell: yourFeedCell = tableView.dequeueReusableCellWithIdentifier("yourTableCell") as!yourFeedCell
cell.indexofRow = indexPath.row;
}
/*Code in you cell class*/
//Maintain one dictonary Globally
var listOfCheckedCells = [Int: Bool]()
// bool property
var isChecked: Bool = false {
didSet {
if isChecked == true {
self.setImage(checkedImage, forState: .Normal)
listOfCheckedCells[self.indexofRow] = true
} else {
self.setImage(unCheckedImage, forState: .Normal)
listOfCheckedCells[self.indexofRow] = false
}
}
}
Hope this will Help
You have already subclassed from UIButton. AFAICS, you need a method call in order to be aware when the button is pressed. There is a method on UIControl (which is also applicable to UIButton):
func sendAction(_ action: Selector,
to target: AnyObject?,
forEvent event: UIEvent?)
UIControl implements this method to forward an action message to the
singleton UIApplication object (in its
sendAction:to:fromSender:forEvent: method) for dispatching it to the
target or, if there is no specified target, to the first object in the
responder chain that is willing to handle it. Subclasses may override
this method to observe or modify action-forwarding behavior. The
implementation of sendActionsForControlEvents: might call this method
repeatedly, once for each specified control event.
You can override the method, call the super's implementation and let your observers/delegates know that the associated action is about to be performed.
Table view cells are populated only when they're visible so if you wanna save a value of a cell and keep it around even when its not visible, create a dictionary with the index path as key and the content of the cell as value. Then just update the dictionary when the check box is checked and unchecked. Finally when 'the button at the bottom of the view' is clicked fetch your data from the dictionary. here is somewhat a sample
var textOfLabels:[NSIndexPath:String]?
func buttonClicked(sender: UIButton) {
let view = sender.superview!
let cell = view.superview as! UITableViewCell //or your custom cell name
let indexPath = itemTable.indexPathForCell(cell)
if (sender == self) {
if isChecked == true {
isChecked = false
textOfLabels?.updateValue(theLableText, forKey: indexPath)
} else {
isChecked = true
textOfLabels?.removeValueForKey(indexPath)
}
}
}
Related
Issue: When I scroll the tableView, my configureCell() method appends too many views to the stackView inside the cell.
When the cell is first displayed, and I press the UIButton, the stack is unhidden and first shows the right amount of views, after scrolling, the amount is duplicated.
prepareForReuse() is empty right now. I want to keep the stackView unHidden after scrolling.
I set the heightAnchor for the UIView as it is programmatic layout.
Expected Outcome: User taps on the button, the cell stackView is unhidden and the cell expands to chow the uiviews related to the cell.
When I call it in the cellForRowAt, nothing happens. Because im not sure how to modify the method for IndexPath.row.
protocol DataDelegate: AnyObject {
func displayDataFor(_ cell: TableViewCell)
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var stackView: UIStackView! {
didSet {
stackView.isHidden = true
}
}
#IBOutlet button: UIButton!
var model = Model()
var detailBool: Bool = false
#IBAction func action(_ sender: Any) {
self.claimsDelegate?.displayClaimsFor(self)
detailBool.toggle()
}
func configureCellFrom(model: Model) {
if let addedData = model.addedData {
if addedData.count > 1 {
for data in addedData {
let dataView = DataView()
dataView.heightAnchor.constraint(equalToConstant: 65).isActive = true
dataView.dataNumber.text = data.authDataNumber
self.stackView.addArrangedSubview(dataView)
}
}
}
}
}
How would I call this in cellForRowAt, so its only created the correct amount of uiviews and not constantly adding?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.dataDelegate = self
cell.configureCellFrom(model: model[indexPath.row])
//if let addedData = model.addedData {
//if addedData.count > 1 {
//for data in addedData {
//let dataView = DataView()
//dataView.heightAnchor.constraint(equalToConstant: 65).isActive = true
//dataView.dataNumber.text = data.authDataNumber
//self.stackView.addArrangedSubview(dataView)
// }}}
cell.layoutIfNeeded()
cell.selectionStyle = .none
return cell
}
extension ViewController: DataDelegate {
func displayDataFor(_ cell: TableViewCell) {
if tableView.indexPath(for: cell) != nil {
switch cell.detailBool {
case true:
UIView.performWithoutAnimation {
tableView.beginUpdates()
cell.detailArrow.transform = CGAffineTransform(rotationAngle:.pi)
cell.stackView.isHidden = false
tableView.endUpdates()
}
case false:
UIView.performWithoutAnimation {
tableView.beginUpdates()
cell.stackView.isHidden = true
cell.detailArrow.transform = CGAffineTransform.identity
tableView.endUpdates()
}
}
}
}
}
If I understand correctly, you would like to create an expandable TableView? If yes you can do it a lot of different ways, but you have to change your approach totally. Please refer LBTA approach:
LBTA video
My favourite the Struct approach, where you create a struct and you can save the complication with the 2D array:
Struct stackoverflow link
I'm having a tableView with a custom cell, whenever I click the checkbox button the value inside the cell increases i.e.,(0 to 1) in cell, and on uncheck value decreases, that works perfectly. But whenever I try to print those values from the cell to a UILabel outside tableView, the values are not changing.
This is the below code I have Used
var data = [[String: AnyObject]]()
func getDetails() {
let paymentURL = paymentListURL + String(28) + "&student_id=" + String(33)
Alamofire.request(paymentURL).responseJSON { (response) in
if ((response.result.value) != nil) {
var jsonVar = JSON(response.result.value!)
print(jsonVar)
if let da = jsonVar["types"].arrayObject {
self.data = da as! [[String:AnyObject]]
}
if self.data.count > 0 {
self.tableView.reloadData()
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TabCell
cell.checkB.tag = indexPath.row
let ip = data[indexPath.row]
cell.nameText.text = ip["title"] as? String
if cell.nameText.text == "Mandatory testing" {
cell.checkB.checkState = .checked
cell.backgroundColor = UIColor.lightGray
cell.checkB.backgroundColor = UIColor.lightGray
}
if ip["mandatory"] as? String == "yes" {
moneyText.text = ip["amount"] as? String
//moneyText is UILabel outside Tableview
cell.amountValue.text = ip["amount"] as? String
cell.checkB.isEnabled = false
} else {
moneyText.text = "0"
if cell.amountValue.text == "1"{
print("ONE")
}
}
return cell
}
func didPressButton(_ tag: Int) {
let indexPath = IndexPath.init(row: 0, section: 0)
let cell = tableView.cellForRow(at: indexPath) as! TabCell
moneyText.text = String(cell.someValue)
}
And for TableviewCell I Used
protocol TabCellDelegate {
func didPressButton(_ tag: Int)
}
class TabCell: UITableViewCell {
#IBOutlet weak var checkB: M13Checkbox!
#IBOutlet weak var nameText: UILabel!
#IBOutlet weak var amountValue: UILabel!
var someValue: Int = 0 {
didSet {
amountValue.text = "\(someValue)"
}
}
#IBAction func checkBAction(_ sender: M13Checkbox) {
cellDelegate?.didPressButton(sender.tag)
if checkB.checkState == .checked {
someValue += 1
} else if checkB.checkState == .unchecked {
someValue -= 1
}
}
}
I tried first adding those values from cell to an Array, and then adding all the values in array and printing in UILabel, but the values are not changing, it was only incrementing.i.e., even after unchecking the checkbox the value is increasing.
I tried even using protocol it did not work for me
Any Help will be appreciated.
You are updating someValue from the checkBAction handler inside the TabCell. The property didSet handler will then update the amountValue label. This is the reason, why the cell's label is being updated.
You do not have any code that updates the moneyText after someValue changed. You only set moneyText.text from tableView(_:cellForRow:), but this is called when a cell is being displayed, maybe multiple times after scrolling etc.
You could do the following:
Create a delegate property inside the cell (use a custom protocol as type)
Set the controller to be that delegate
When the value changes, call a function of that delegate (e.g. call the controller)
Inside that, update the moneyText
As an idea (might not compile because I don't have all your classes):
protocol MyTabCellProtocol {
func checkboxChanged(_ checkbox:M13Checkbox, atRow:Integer)
}
class TabCell: UITableViewCell {
weak delegate:MyTabCellProtocol?
// ...
#IBAction
func checkBAction(_ sender: M13Checkbox) {
if checkB.checkState == .checked {
someValue += 1
} else if checkB.checkState == .unchecked {
someValue -= 1
}
delegate?.checkboxChanged(self, checkB.tag)
}
}
class MyController : UIViewController, MyTabCellProtocol {
func checkboxChanged(_ checkbox:M13Checkbox, atRow:Integer) {
moneyText.text = "\(checkbox.someValue)"
}
}
But if I think further, I would suggest to refactor the whole code a little. The problem I see is that your action handler inside the cell does update the someValue property of the cell, but does not update the outside model (ip["amount"]). I think what you should do is:
Inside the cell's checkBAction, just call the delegate and provide the information about the row that has been modified (self.checkB.tag) and the check state. Do not update the amountValue here!
In the delegate implementation, update the model ip["amount"]
Call reloadRows(at:with:) of the table view to refresh the cell
Then, cellForRow is automatically being called, in which you then update the cell and the outer label.
I am creating a Check List app and trying to update button event which is set images itself after clicked.
Here is my code:
protocol CustomeCellDelegate {
func cell(cell: UITableViewCell, updateTheButton button: UIButton)
}
class Cells: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var checkBox: UIButton!
var buttonPressed = true
#IBAction func checkBoxAction(_ sender: UIButton) {
if buttonPressed {
sender.setImage(UIImage(named: "checkBoxB"), for: UIControlState.normal)
buttonPressed = false
} else {
sender.setImage(UIImage(named:""), for: UIControlState.normal)
buttonPressed = true
}
}
#objc func recordButtonImage(_ button: UIButton) {
cellDelegate?.cell(cell: self, updateTheButton: button) // cell notify the button that touched.
}
override func awakeFromNib() {
checkBox.addTarget(self, action: #selector(recordButtonImage(_:)), for: UIControlEvents.touchUpInside)
}
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate, CustomeCellDelegate {
#IBOutlet weak var tableView: UITableView!
var myImages: [UIImage?]!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
myImages = Array(repeatElement(nil, count: 50))
let nib = UINib(nibName: "Cells", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "Cells")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
internal func tableView(_ tableView:UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "theCells") as? Cells
cell?.theTextField.delegate = self
cell?.cellDelegate = self
cell?.checkBox.setImage(myImages[indexPath.row], for: UIControlState.normal)
return cell!
}
// This function from protocol and it helps to update button images.
func cell(cell: UITableViewCell, updateTheButton button: UIButton) {
print("Delegate method Update Button Images.")
if let indexPath = tableView.indexPath(for: cell), let buttonImage = button.image(for: UIControlState.normal) {
myImages[indexPath.row] = buttonImage
}
}
I would like to update both events of the button that set image after checked and unchecked. Therefore, my table view can dequeue Reusable Cell and have those events updated.
But the result is just one event of button which is checked updated but not the unchecked one. The checked button still repeats whatever I do to uncheck it.
I think you don't need recordButtonImage method, you should call the delegate method from the button tapped action i.e. checkBoxAction, just like below
class Cells: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var checkBox: UIButton!
var buttonPressed = true
#IBAction func checkBoxAction(_ sender: UIButton) {
if buttonPressed {
sender.setImage(UIImage(named: "checkBoxB"), for: UIControlState.normal)
buttonPressed = false
} else {
sender.setImage(UIImage(named:""), for: UIControlState.normal)
buttonPressed = true
}
// cell notify the button that touched.
cellDelegate?.cell(cell: self, updateTheButton: button)
}
}
Also your data source method, doesn't tell the cell whether the button was pressed or not. It just sets the image but doesn't set the variable buttonPressed
internal func tableView(_ tableView:UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "theCells") as? Cells
cell?.theTextField.delegate = self
cell?.cellDelegate = self
cell?.checkBox.setImage(myImages[indexPath.row], for: UIControlState.normal)
return cell!
}
The best way to handle UIButton is using its own state.
You need to hold current state value in one array. So you can keep the state in cellForRow and other things. By default set all state to false.
In CellForRow, just put below code:
YOUR_BUTTON.isSelected = aryState[indexPath.row] as! Bool
Set image for your button
YOUR_BUTTON.setImage(UIImage(named: NORMAL_IMAGE), for: .normal)
YOUR_BUTTON.setImage(UIImage(named: SELECTED_IMAGE), for: .selected)
Just change button state when it clicked:
#IBAction func checkBoxAction(_ sender: UIButton) {
let button = sender
button.isSelected = !button.isSelected
}
How do I tell if I button in the collection view header is selected when populating my collection view? I have 2 tabs in the header which determine which data I populate the collection view with so I want to be able to switch the data and reload the collection view when the user selects a different tab.
some code from header class as requested...I don't see the point though it's just a button. I want to see if this button is selected while populating the cells and cell count etc.
class UserSearchHeader: UICollectionReusableView {
#IBOutlet weak var friendsButton: UIButton!
#IBOutlet weak var peopleButton: UIButton!
#IBOutlet weak var customSlider: UIView!
override func awakeFromNib() {
super.awakeFromNib()
self.friendsButton.selected = true
customSlider.layer.cornerRadius = customSlider.frame.height / 2
}
#IBAction func friendsButtonTapped(sender: AnyObject) {
if self.friendsButton.selected == false {
self.friendsButton.selected = true
self.peopleButton.selected = false
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.customSlider.frame.origin.x = 0
})
}
}
UICollectionviewHeader + UI Button
UIcollectionviewHeader are using dequeueReusableSupplementaryView and for some reason it created a UIView Infront of every Headerview, this would block all gesture that are happening. the solution is to bring the view to front like so:
override func awakeFromNib() {
super.awakeFromNib()
self.bringSubview(toFront: friendsButton) // this depends on you .xib
}
this will solve your gesture issue.
Theres Multiple way to Create a UIButton Action inside a CollectionViewHeader.
AddTarget (as Answered by #derdida)
Drag and Drop Action In CollectionViewHeader Class.
Add Target :-(refer to derdida answer)
inside of viewForSupplementaryElementOfKind// ps: you need to register your CollectionView Class & Nib
Add Target to UIButton like so.
header.friendsButton.tag = 0
header.friendsButton.addTarget(self, action: #selector(self.HeaderClick(_:)), for: .touchUpInside)
Created the Function like so.
#objc func HeaderClick1(_ sender : UIButton){ print("sender.tag \(sender.tag)") }//sender.tag 0
Drag and Drop Action In CollectionViewHeader Class.
for this example i will used #Wayne Filkins Question.
Ctrl + Drag UIButton To Header Class
Select Connection Type to Action create the function like so
#IBAction func friendsButtonTapped(_ sender: Any) { print("something from friendsButtonTapped") }
and thats it.
but the main key here is to get the view to be to front. used debug view hierarchy to know more.
Someone can fixed my code view please!
Update Solution
Using UIButtonSetOnClickListener.Swift and heres the code:
extension UIControl {
func setOnClickListener(for controlEvents: UIControl.Event = .primaryActionTriggered, action: #escaping () -> ()) {
removeTarget(nil, action: nil, for: .allEvents)
let sleeve = ClosureSleeve(attachTo: self, closure: action)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for:
controlEvents)
}
}
class ClosureSleeve {
let closure: () -> ()
init(attachTo: AnyObject, closure: #escaping () -> ()) {
self.closure = closure
objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)
}
#objc func invoke() {
closure()
}
}
Here how to used is:
Add Target :-(refer to derdida answer)
inside of viewForSupplementaryElementOfKind// ps: you need to register your CollectionView Class & Nib
header.friendsButton.setOnClickListener {
//put your code here
}
There are 2 delegate methods who are important to decide which items will be shown. For example:
You have 2 different items, they are populated in:
let items1 = [Item]()
let items2 = [Item]()
Then you have a variable, that holds which items should be shown:
let items1Shown:Bool = true
Now implement the delegate methods with something like:
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if(items1Shown == true) {
return items1.count
} else {
return items2.count
}
}
And
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var item:Item!
if(items1Shown == true) {
item = items1[indexPath.row]
} else {
item = items1[indexPath.row]
}
// format your cell
}
And implement any button function
func ChangeItems() {
if(items1Shown == true) {
items1Shown = false
} else {
items1Shown = true
}
// reload your collectionView
self.collectionView.reloadData()
}
Edit:
Why not giving your button a target? (Add this where you dequeue your headerView!)
headerView.friendsButton.addTarget(self, action: #selector(YourViewControllerWithFunction.cellButtonClick(_:)), forControlEvents: .TouchUpInside)
// for example:
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: headerId, forIndexPath: indexPath)
headerView.friendsButton.addTarget(self, action: #selector(YourViewControllerWithFunction.cellButtonClick(_:)), forControlEvents: .TouchUpInside)
return headerView
}
// Class YourViewControllerWithFunction
func cellButtonClick(button: UIButton) {
// you can do anything with that button now
}
I have a UITableView where each cell get's its data from my model class, which is an array in my TableViewController class. The model for each cell is set in cellForRowAtIndexPath. My model has a "isFavorited" property. My custom cell class has a UIButton with an image of a star.
If model.isFavorited == false, the UIButton image is a gray star.
If model.sFavorited == true, the UIButton image should change to a yellow star.
So far, I have tried to approach this like so:
private let normal = UIImage(named: "star")
private let selection = UIImage(named: "star highlighted")
class CustomCell: UITableViewCell {
var model: Model? {
didSet{
favoriteButton.selected = (model?.isFavorite)!
}
}
#IBOutlet var favoriteButton: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
favoriteButton.adjustsImageWhenHighlighted = true
favoriteButton.setImage(normal, forState: .Normal)
favoriteButton.setImage(selection, forState: .Highlighted)
favoriteButton.setImage(selection, forState: .Selected)
}
#IBAction func favoriteTapped(sender: AnyObject) {
//UPDATE THE MODEL
model?.isFavorite = !(model?.isFavorite)!
}
}
This code successfully changes a star from gray to yellow and vice-versa thanks to didSet being called to update the favoriteButton.selected boolean with the value from (model?.isFavorite)!.
HOWEVER, as I scroll far enough through my tableview, and try to come back to the specific cell I favorited, the image will reappear as unfavorited (gray star instead of yellow star).
If my model is being updated when I tap the button, and the UIButton's image is adjusted during didSet as a result of this change, why does it revert back to the defaulted state when scrolling back to this cell??
EDIT:
My code in cellForRowAtIndexPath
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: CustomCell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CustomCell
cell.model = modelArray[indexPath.row]
return cell
}
Basically you could subclass the UIButton with custom images set based on the tap. I had the same issue and solved it using below:
class FavioriteButton: UIButton {
//Images
let normalImage = UIImage(named: "star")! as UIImage
let selectedImage = UIImage(named: "star_highlighted")! as UIImage
//Bool property
var isSelected: Bool = false {
didSet {
if isSelected == true {
self.setImage(normalImage, forState: .Normal)
}else{
self.setImage(selectedImage, forState: .Normal)
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(buttonClicked(_:)), forControlEvents: .TouchUpInside)
self.isSelected = false
}
func buttonClicked(sender: UIButton) {
if sender == self {
if isSelected == true {
isSelected = false
}else{
isSelected = true
}
}
}
}
Hope it helps you.