Swift - select single radioButton in tableView - swift

I have a tableView, which is expandable. The headerCell has a checkBox, a label and a radioButton. The collapsableCell has a checkBox and a label.
I have used M13Checkbox Library to implement the checkBox and the radioButton.
The problem is when I select a radioButton or a checkBox of HeaderViewCell at index 0, then the radioButton/checkBox at index 8,16,24 also get selected. I know it is because of the numberOfSections in tableView property, But then how do I select just single radioButton at a time.
My requirement is I have to select single radioButton in the tableView and CheckBoxes can have multiple selections for the HeaderCell.
I am badly stuck on this issue. I have googled a lot but nothing worked. Any help or suggestion much appreciated.
func numberOfSections(in tableView: UITableView) -> Int {
return states.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return states[section].cities.count
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50.0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if (states[indexPath.section].expanded) {
return 44
}else{
return 0.0
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableHeaderFooterView(withIdentifier: "headerviewcell") as! HeaderView
var list = states[section]
headerCell.customInit(titleLabel: list.stateName, section: section, delegate: self)
return headerCell
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "subcells") as! CollapsibleCell
cell.selectionStyle = .none
cell.textLabel?.text = states[indexPath.section].cities[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// works for headers or cell??
}
func toggleHeader(header : HeaderView, section : Int){
states[section].expanded = !states[section].expanded
tableView.beginUpdates()
for i in 0 ..< states[section].cites.count {
tableView.reloadRows(at: [IndexPath(row: i, section: section)], with: .automatic)
}
tableView.endUpdates()
}
UPDATE:
Here is my code for HeaderView. Whenever I select any radioButton, then any random radioButton for other sections also get selected. Upon scrolling the state and section of radioButtons changes. Please help me solve this issue. I am aware it's related to cell reuse property, but I can't solve it.
import UIKit
import M13Checkbox
protocol HeaderViewDelegate {
func toggleHeader(header : HeaderView, section : Int)
}
protocol CustomHeaderDelegate: class {
func didTapButton(in section: Int, headerView : HeaderView, button : M13Checkbox)
}
class HeaderView: UITableViewHeaderFooterView {
#IBOutlet weak var stateCheckBox: M13Checkbox!
#IBOutlet weak var stateNameLabel: UILabel!
#IBOutlet weak var favouriteState: M13Checkbox!
var delegate : HeaderViewDelegate?
weak var delegateHeader: CustomHeaderDelegate?
var sectionNumber : Int!
var section : Int!
override func awakeFromNib() {
stateCheckBox.boxType = .square
stateCheckBox = .bounce(.fill)
favouriteState.boxType = .circle
favouriteState.setMarkType(markType: .radio, animated: true)
favouriteState.stateChangeAnimation = .bounce(.stroke)
}
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
self.addGestureRecognizer(UITapGestureRecognizer(target : self, action: #selector(selectHeaderView)))
}
required init?(coder aDecoder: NSCoder) {
super.init(coder : aDecoder)
self.addGestureRecognizer(UITapGestureRecognizer(target : self, action: #selector(selectHeaderView)))
}
func selectHeaderView(gesture : UITapGestureRecognizer) {
let cell = gesture.view as! HeaderView
delegate?.toggleHeader(header: self, section: cell.section)
}
func customInit(titleLabel : String, section : Int, delegate : HeaderViewDelegate) {
self.stateNameLabel.text = titleLabel
self.section = section
self.delegate = delegate
}
#IBAction func selectPrimaryCondition(_ sender: M13Checkbox) {
// get section when favourite state radioButton is selected
delegateHeader?.didTapButton(in: sectionNumber, headerView : self, button : sender)
}
override func prepareForReuse() {
// What do do here…??
}
}

TableViews are tricky when saving data without knowing some kind of saved state that can be held in some sort of object class is it can do some weird things such as reuse cells; selecting incorrect cells in your case. I'm guessing that you've created some kind of object for your states and cities. I'd make sure that they're classes and not structs and make sure they have some sort of isTapped Bool so you're able to actually save the state for that individual object like so:
class CellObject: NSObject {
var isTapped: Bool
init(isTapped: Bool) {
self.isTapped = isTapped
}
}
Instead of just setting your isSelected to true or false inside your didSelect method, set THAT cells object isTapped to true or false AS WELL as changing the state to whatever you want, i.e change it's background colour to black to show it's selected. When loading your cell inside of your cellForRowAtIndexPath method, check if the cells object isTapped is either true of false and set it's state, colours etc. accordingly. This is the barebones of the idea but I hope this points you in the right direction.

Related

Update Specific Button Image in UITableView Cell

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

UITableView button color issue

I have some questions to answer with Yes or Not in a UITableView, each option (yes/no) is it's own button in the cell.
First, I have to handle the click event, because I have to change button's backgroundColor on click and I have to add the clicked row to create another struct with the answers.
My actual code is like this:
extension MyViewController: UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//click row
}
}
extension MyViewController: UITableViewDataSource{
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return questions.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellAnswers", for: indexPath) as! MyTableViewCell
//iterate here? and how can i do that?
cell.actionYes = {
cell.buttonYes.backgroundColor = .blue
cell.buttonNo.backgroundColor = .darkGray
}
cell.actionNot = {
cell.buttonYes.backgroundColor = .darkGray
cell.buttonNo.backgroundColor = .blue
}
let question = questions[indexPath.row]
cell.question = question
return cell
}
}
Like you see, I have clousures to handle click event and change backgroundColor, but if i click one button, the rest change too.
and my TableViewCell:
import UIKit
class MyTableViewCell: UITableViewCell{
#IBOutlet weak var question: UILabel!
#IBOutlet weak var buttonYes: UIButton!
#IBOutlet weak var buttonNo: UIButton!
var actionYes: (() -> Void)? = nil
var actionNo: (() -> Void)? = nil
#IBAction func answerYes(_ sender: Any) {
actionYes?()
}
#IBAction func answerNo(_ sender: Any) {
actionNo?()
}
var question: QuestionResponse!{
didSet{
updateUI()
}
}
func updateUI(){
question.text = question.value
}
}
I don't have any idea how to iterate the the cells to fill the correct color on buttons.
What you are facing is the result of cell reusing that UTableView does.
Whenever a cell is out of the screen, (after scrolling) gets added to a queue to be reused. That's why you need to call dequeueReusableCell(withIdentifier:for:) function to get a cell that you need to prepare for displaying your data. That means that a UITableViewCell instance may already have some changes left over from the last time that has been used.
The correct way to deal with a UTableView is to keep whatever you change in you datasource. For example you can add an answer property to your QuestionResponse model, like this:
enum Answer {
case yes, no
}
struct QuestionResponse {
var answer: Answer?
}
And store there the user's answer. Also you have to make sure that you always set the background color of your buttons to avoid unwanted leftover colors from previous usage of the same cell instance:
extension MyViewController: UITableViewDataSource{
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return questions.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellAnswers", for: indexPath) as! MyTableViewCell
//iterate here? and how can i do that?
cell.actionYes = {
cell.buttonYes.backgroundColor = .blue
cell.buttonNo.backgroundColor = .darkGray
}
cell.actionNot = {
cell.buttonYes.backgroundColor = .darkGray
cell.buttonNo.backgroundColor = .blue
}
let question = questions[indexPath.row]
switch question.answer {
case .yes:
cell.buttonYes.backgroundColor = .blue
cell.buttonNo.backgroundColor = .darkGray
case .no:
cell.buttonYes.backgroundColor = .darkGray
cell.buttonNo.backgroundColor = .blue
default:
cell.buttonYes.backgroundColor = .darkGray
cell.buttonNo.backgroundColor = .darkGray
}
cell.question = question
return cell
}
}

How to expand tableview cell on click on button which is located outside the cell in the tableview?

There is a tableview cell and there is a more button which is located on the tableview not in the cell so I want that when the project run it show only five user data in the tableview cell and other user data is show when the user click on the more button(which is located out side the cell) how do I achieved this?
Thanks for help
You can find the prototype cell image
//Mark:TableViewDelegate
extension MembersViewController:UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return imgarray.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tblView.dequeueReusableCell(withIdentifier: "membTableCell", for: indexPath) as! membTableCell
cell.profiletabImg.image = imgarray[indexPath.row]
cell.namelbl.text = names[indexPath.row]
cell.positionlbl.text = "Ceo of code with color"
return cell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 40
}
#IBAction func morebutton(sender:UIButton){
}
}
The tableview expands automatically if you add data to the array and call the tableview.reloadData() function.
From the Apple's Doc -
"Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible. It adjusts offsets if the table shrinks as a result of the reload. The table view’s delegate or data source calls this method when it wants the table view to completely reload its data. It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates() and endUpdates()."
#IBAction func moreBtnPressed(_ sender: Any) {
tableviewArray.append("Append your array with remaining items")
tableview.reloadData()
}
Return 5 in numberOfRowsInSection method if more button is not tapped.
When more button is tapped reload the tableview and return imgarray.count in numberOfRowsInSection method.
class TableViewController: UITableViewController {
var dataArr = [String]()
var expanded = false
override func viewDidLoad() {
super.viewDidLoad()
dataArr = (1..<25).map({ String($0) })
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "\(dataArr.count - 5) more", style: .plain, target: self, action: #selector(morebutton(_:)))
}
#objc func morebutton(_ sender: UIBarButtonItem) {
self.navigationItem.rightBarButtonItem = nil
expanded = true
tableView.reloadData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if !expanded && !dataArr.isEmpty {
return 5
} else {
return dataArr.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") ?? UITableViewCell(style: .default, reuseIdentifier: "reuseIdentifier")
cell.textLabel?.text = dataArr[indexPath.row]
return cell
}
}

Can't get indexPath of cell in header

I have created prototype custom header cell for a tableView with a button on it. I am trying to get the indexPath of the cell when the button is tapped, but I don't receive it. Any idea what I am doing wrong here?
protocol MediaHeaderCellDelegate: class {
func editPost(cell: MediaHeaderCell)
}
class MediaHeaderCell: UITableViewCell {
weak var delegate: MediaHeaderCellDelegate?
#IBAction func moreOptionsAction(_ sender: UIButton) {
delegate?.editPost(cell: self)
}
}
class NewsfeedTableViewController:UITableViewController, MediaHeaderCellDelegate {
func editPost(cell: MediaHeaderCell) {
guard let indexPath = tableView.indexPath(for: cell) else {
print("indexpath could not be given")
return}
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.mediaHeaderCell) as! MediaHeaderCell
cell.delegate = self
cell.media = media[section]
return cell
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
cell.delegate = self
return cell
}
}
So this is actually all about learning what section a section header belongs to?? Here’s what I do. I have a header class:
class MyHeaderView : UITableViewHeaderFooterView {
var section = 0
}
I register it:
self.tableView.register(
MyHeaderView.self, forHeaderFooterViewReuseIdentifier: self.headerID)
I use and configure it:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let h = tableView
.dequeueReusableHeaderFooterView(withIdentifier: self.headerID) as! MyHeaderView
// other stuff
h.section = section // *
return h
}
Now if the header view is tappable or contains a button or whatever, learning what section this is the header of is trivial.
Your immediate issue is that you are using a table cell as a section header view. That should not be done. Once you resolve that, your next task is to determine the table section from the header view whose button was tapped.
First, change your MediaHeaderCell to be a header view that extends UITableViewHeaderFooterView and update your protocol accordingly:
protocol MediaHeaderViewDelegate: class {
func editPost(view: MediaHeaderView)
}
class MediaHeaderView: UITableViewHeaderFooterView {
weak var delegate: MediaHeaderViewDelegate?
#IBAction func moreOptionsAction(_ sender: UIButton) {
delegate?.editPost(cell: self)
}
}
Then you need to register the header view in your view controller.
Then update your viewForHeaderInSection:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: Storyboard.mediaHeaderView) as! MediaHeaderView
view.delegate = self
view.media = media[section]
view.tag = section
return view
}
And last, update your protocol method implementation:
func editPost(view: MediaHeaderView) {
let section = view.tag
// do something
}
There is one possible issue with this. If your table allows sections to be added or removed, then it is possible that a header view's tag could be wrong when the button is tapped.

Swift: How to access the cells within a UITableView

I currently have UITableView that has one UITableViewCell within it. Within my code I created a button over this cell. When that button is pressed, the cells within it expand.
How would I go about making something expand when one of those cells are selected? Would I need to put a UITableView within the cell?
This is what I would do if I needed to bang something out in a hurry. Let each vegetable occupy its own table section, then expand or contract the section when the user toggles:
// We need some kind of data model.
class Vegetable {
init(name: String, facts: [String]) {
self.name = name
self.facts = facts
}
let name: String
let facts: [String]
var isShown: Bool = false // This flag gets toggled, which is why I've defined it as a class.
}
// And a table view controller configured for dynamic (not static!) cells. Create this in a storyboard, and change the UITableViewControllers class to VeggieTable.
class VeggieTable: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
veggieData = [Vegetable(name: "Tomato", facts: ["Number: 15", "Red Colored"]),
Vegetable(name: "Lettuce", facts: ["Good with peanut butter", "Green", "Crunchy snack"])] // etc.
}
var veggieData: [Vegetable]!
override func numberOfSections(in tableView: UITableView) -> Int {
return veggieData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let veg = veggieData[section]
return veg.isShown ? veg.facts.count + 1 : 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let veg = veggieData[indexPath.section]
switch indexPath.row {
case 0:
cell.textLabel?.text = veg.name
default:
cell.textLabel?.text = veg.facts[indexPath.row - 1]
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let veg = veggieData[indexPath.section]
// Toggle the is shown flag:
veg.isShown = !veg.isShown
tableView.deselectRow(at: indexPath, animated: true)
// TODO: replace reloadData() with sophisticated animation.
// tableView.beginUpdates()
// tableView.insertRows() / deleteRows() etc etc.
// tableView.endUpdates()
tableView.reloadData()
}
}
Eventually you'd add table animations to make the rows appearing look smooth etc.
Hope that helps.