Swift 3 UISwitch in TableViewCell loses State when scrolling - ios

That's weird: I just set up a new Single-View iOS Project and put a TableView into the Main.storyboard. In this TableView I put a TableViewCell and into this Cell, I put an UILabel and an UISwitch.
For this TableViewCell I created a CocoaTouchClass MyTableViewCell and set this for the TableViewCell's Class in Interfacebuilder.
I connected Outlets for the UILabel and UISwitch to MyTableViewCell, as well as an action for the switch.
Also I connected the TableView's dataSource and delegate to the ViewController.
So, as I think, basic stuff for setting up a table.
My ViewController looks like this:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableData = [[String: Bool]]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
for index in 1...40 {
self.tableData.append([String(index): index%2 == 0])
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mycell", for: indexPath) as! MyTableViewCell
let object = tableData[indexPath.row].first!
cell.myLabel.text = object.key
cell.mySwitch.setOn(object.value, animated: false)
return cell
}
}
So, I populate the Table with some Rows of Data and switch every second UISwitch to on.
The MyTableViewCell-Class is nothing special as well:
class MyTableViewCell: UITableViewCell {
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var mySwitch: UISwitch!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func switched(_ sender: UISwitch) {
print("Switched: \(sender.isOn)")
}
}
Ok, fire up the iOS-Simulator and I see the Table as expected. 40 Table-Lines don't fit on one screen, so the TableView makes itself scrollable.
Now: when I change the state of one UISwitch, and drag the TableView so the changed UISwitch gets out of view and then drag the TableView so the changed UISwitch gets visible again, it is changed back to its initial state.
The Switch-event gets fired like it should.
So, what am I doing wrong? Am I missing something?
I recorded a 4-seconds-Screencast to demonstrate, what's going on:
http://b-bereich.de/download/swiftSwitch.mov

TableViewCells are reused.
That means you need to keep track of the data you are using to fill the content of the cells. If the cell content changes - such as when you tap a Switch in the cell - you need to update your datasource. When you scroll, and that row is displayed again, your data will know how to set the state of the Switch.
Here is a simple example:
//
// TableWithSwitchTableViewController.swift
// SWTemp2
//
// Created by Don Mag on 6/5/17.
// Copyright © 2017 DonMag. All rights reserved.
//
import UIKit
class MyTableViewCell: UITableViewCell {
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var mySwitch: UISwitch!
var switchTapAction : ((Bool)->Void)?
#IBAction func switched(_ sender: UISwitch) {
print("Switched: \(sender.isOn)")
// send the Switch state in a "call back" to the view controller
switchTapAction?(sender.isOn)
}
}
// simple data object
class MyObject: NSObject {
var theTitle = ""
var theSwitchState = false
init(_ title: String) {
theTitle = title
}
}
class TableWithSwitchTableViewController: UITableViewController {
// array of MyObjects
var myData = [MyObject]()
override func viewDidLoad() {
super.viewDidLoad()
// just to make it a little easier to see the rows scroll
tableView.rowHeight = 60
// create 40 data objects for the table
for i in 1...40 {
let d = MyObject("Data Item: \(i)")
myData.append(d)
}
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 {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SwitchCell", for: indexPath) as! MyTableViewCell
// Configure the cell...
let d = myData[indexPath.row]
cell.myLabel.text = d.theTitle
cell.mySwitch.isOn = d.theSwitchState
// set a "Callback Closure" in the cell
cell.switchTapAction = {
(isOn) in
// update our Data Array to the new state of the switch in the cell
self.myData[indexPath.row].theSwitchState = isOn
}
return cell
}
}

I think you need to have your IBAction change the Bool each time you flip the switch. Currently the switch changes the UI but not the underlying Bool so the cellForRowAt method uses the currently saved value when the cell scrolls back on screen.

Add function prepareForReuse() in your custom cell class and set switch state to off by default. -:
class MyTableViewCell: UITableViewCell {
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var mySwitch: UISwitch!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func switched(_ sender: UISwitch) {
print("Switched: \(sender.isOn)")
}
override func prepareForReuse(){
// SET SWITCH STATE OFF HERE
}

Related

How do you create a tableView inside a tableViewCell?

I have been searching for some time now and have developed a massive headache trying to solve this problem. Basically, I would like to have a parent tableView mainTableView to contain a tableViewCell that includes a label and another tableView innerTableView. This problem is not as simple as I had hoped.
If there is a better way to implement what I want with different components, please let me know.
Otherwise, I have tried to simplify the code to its bare bones with simple data. Please assume I have correctly connected all my outlets properly. I have 3 swift files for this simplified version. ViewController.swift is the main VC that holds the mainTableView. MainTableViewCell.swift is the main cell that will hold a label and a tableView innerTableView in it. InnerTableViewCell is the final cell that will be associated with the innerTableView. In this cell, there is only a label. I have been successful in creating this first layer and the mainNameLabel.text shows up properly on the main storyboard. However, I have configured MainTableViewCell to the best of my knowledge and have come short to success. The innerTableView does not appear on the main storyboard.
Below, I will provide the code of each swift file:
ViewController.swift
//
// ViewController.swift
// NestedTableViews
//
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var mainTableView: UITableView!
//fake data
var arr = ["Hello", "Hi", "Whats up", "How's it going"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
mainTableView.delegate = self
mainTableView.dataSource = self
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MainTableViewCell") as! MainTableViewCell
let word = arr[indexPath.row]
cell.mainNameLabel.text = word
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arr.count
}
}
MainTableViewCell.swift
//
// MainTableViewCell.swift
// NestedTableViews
//
import UIKit
class MainTableViewCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var mainNameLabel: UILabel!
#IBOutlet weak var innerTableView: UITableView!
var innerArr = ["How are you?", "How are you doing?", "What's the matter?"]
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
innerTableView.dataSource = self
innerTableView.delegate = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "InnerTableViewCell") as! InnerTableViewCell
let innerWord = innerArr[indexPath.row]
cell.innerNameLabel.text = innerWord
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return innerArr.count
}
}
InnerTableViewCell
//
// InnerTableViewCell.swift
// NestedTableViews
//
import UIKit
class InnerTableViewCell: UITableViewCell {
#IBOutlet weak var innerNameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Here is what the storyboard looks like in this current state:
What is it that I am doing wrong? I could ask 100 questions but regardless, I am still very stuck here. How do I get the nested tableView innerTableView to appear and with the proper data?
Thank you in advance

Table view cell elements not able to click and get data

I have one table view and inside that i placed one main view. And inside that main view i placed one button.And when ever use click on my cell button. I need to get the cell title label.This is what i need. But i tried following below code. Not sure what i am missing out. It not at all calling my cell.add target line.
Code in cell for row at index:
cell.cellBtn.tag = indexPath.row
cell.cellBtn.addTarget(self, action:#selector(self.buttonPressed(_:)), for:.touchUpInside)
#objc func buttonPressed(_ sender: AnyObject) {
print("cell tap")
let button = sender as? UIButton
let cell = button?.superview?.superview as? UITableViewCell
let indexPath = tableView.indexPath(for: cell!)
let currentCell = tableView.cellForRow(at: indexPath!)! as! KMTrainingTableViewCell
print(indexPath?.row)
print(currentCell.cellTitleLabel.text)
}
I even added a breakpoint, still it not at calling my cell.addTarget line
Tried with closure too. In cell for row at index:
cell.tapCallback = {
print(indexPath.row)
}
In my table view cell:
var tapCallback: (() -> Void)?
#IBAction func CellBtndidTap(_ sender: Any) {
print("Right button is tapped")
tapCallback?()
}
Here that print statement is getting print in console.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var list = [String]()
#IBOutlet weak var tableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
cell.saveButton.tag = indexPath.row
//cell.saveButton.accessibilityIdentifier = "some unique identifier"
cell.tapCallback = { tag in
print(tag)
}
return cell
}
}
class MyTableViewCell: UITableViewCell {
// MARK: - IBOutlets
#IBOutlet weak var saveButton: UIButton!
// MARK: - IBActions
#IBAction func saveTapped(_ sender: UIButton) {
tapCallback?(sender.tag)
}
// MARK: - Actions
var tapCallback: ((Int) -> Void)?
}
Actually this is not a good programming practice to add the button (which contains in table view cell) target action in view controller. We should follow the protocol oriented approach for it. Please try to under stand the concept.
/*This is my cell Delegate*/
protocol InfoCellDelegate {
func showItem(item:String)
}
/*This is my cell class*/
class InfoCell: UITableViewCell {
//make weak reference to avoid the Retain Cycle
fileprivate weak var delegate: InfoCellDelegate?
//Outlet for views
#IBOutlet var showButton: UIButton?
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
//This is the public binding function which will bind the data & delegate to cell
func bind(with: DataModel?, delegate: InfoCellDelegate?, indexPath: IndexPath) {
//Now the bind the cell with data here
//.....
//Assign the delegate
self.delegate = delegate
}
//Button action
#IBAction func rowSelected(sender: UIButton) {
self.delegate?.showItem(item: "This is coming from cell")
}
}
/*Now in your ViewController you need to just confirm the InfoCellDelegate & call the bind function*/
class ListViewController: UIViewController {
//Views initialisation & other initial process
}
//Table view Delegate & Data source
extension ListViewController: UITableViewDataSource, UITableViewDelegate {
/**
Configure the table views
*/
func configureTable() {
//for item table
self.listTable.register(UINib.init(nibName: "\(InfoCell.classForCoder())", bundle: nil), forCellReuseIdentifier: "\(InfoCell.classForCoder())")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "InfoCell") as! InfoCell
cell.bind(with: DataModel, delegate: self, indexPath: indexPath)
return cell
}
}
extension ListViewController: InfoCellDelegate {
func showItem(item) {
print(item)
}
}

Label not appearing in Swift table

No data is appearing in my Swift table. I'm fairly new to Swift and not quite sure why this or what I might be missing. I followed the guide here for the most part with some differences:
Apple Table Creation
Here's the tableView definition:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "AccountTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? AccountTableViewCell else {
fatalError("The dequeued cell is not an instance of AccountTableViewCell.")
}
let item = userDataSource[indexPath.row]
// Dummy values just to test this out
cell.leftLabel.text = "test1";
cell.rightLabel.text = "test2";
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1;
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) ->Int {
return userDataSource.count;
// This should be an array value, but I have also tried passing a static int here as well to test
}
Here is my class definition with the implemented procotols:
class AccountViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
And here is my table cell definition:
class AccountTableViewCell: UITableViewCell {
//MARK: Properties
#IBOutlet weak var leftLabel: UILabel!
#IBOutlet weak var rightLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
I've got both rightLabel and leftLabel setup in the Storyboard.
I can go to the account page represented by this view controller and a table display does come up - it just has absolutely no data in it.
What am I missing?
It is not sufficient to simply add a UITableView to your view controller scene. You must set the tableview's dataSource property to your view controller instance in the Storyboard connections inspector for the tableview.

iOS swift UIButton in TableView Cell

I have a tableView with custom cell. in my custom cell I have a like button. for like Button I wrote a function to change state from .normal to .selected like this:
FeedViewCell
class FeedViewCell: UITableViewCell {
#IBOutlet weak var likeButton: UIButton!
var likes : Bool {
get {
return UserDefaults.standard.bool(forKey: "likes")
}
set {
UserDefaults.standard.set(newValue, forKey: "likes")
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.likeButton.setImage(UIImage(named: "like-btn-active"), for: .selected)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func likeBtnTouch(_ sender: AnyObject) {
print("press")
// toggle the likes state
self.likes = !self.likeButton.isSelected
// set the likes button accordingly
self.likeButton.isSelected = self.likes
}
}
FeedViewController :
class FeedViewController: UIViewController {
#IBOutlet var feedTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Register Cell Identifier
let feedNib = UINib(nibName: "FeedViewCell", bundle: nil)
self.feedTableView.register(feedNib, forCellReuseIdentifier: "FeedCell")
}
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.feeds.count
}
func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedCell", for: indexPath) as! FeedViewCell
return cell
}
}
But my problem is when I tap like button in cell with indexPath.row 0 the state of button in cell with indexPath.row 3 change state too.
where is my mistake?
thanks
You didn't post all your code, but I can tell you that for this to work the #IBAction func likeBtnTouch(_ sender: AnyObject) { } definition must be inside the FeedViewCell class definition to make it unique to a particular instance of the cell.
As a rule of thumb, I normally ensure that all the UI elements inside my cell are populated in cellForRowAtIndexPath when using dequeued cells. Also it should be set from an external source. I.o.w not from a property inside the cell. Dequeuing cells reuse them, and if not setup properly, it might have some leftovers from another cell.
For example, inside cellForRowAtIndexPath:
self.likeButton.isSelected = likeData[indexPath.row]

Swift - TableView of steppers, click one stepper in a cell and other steppers get activated?

I am new to IOS and basically I have a tableView and whenever it has 40 cells and each cell has a stepper and a label. the label displays the stepper's value. The tableview generates the cells fine, but the problem is that whenever I click the stepper in one cell, some other random cells also have their steppers activated.This is swift by the way. Here is the code for the cell:
import UIKit
class StudentTableViewCell: UITableViewCell {
#IBOutlet weak var studentNameAndValue: UILabel!
#IBOutlet weak var studentValueChanger: UIStepper!
let name:String?
let value:Int?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func stepperValueChanged(sender: AnyObject) {
studentNameAndValue.text = "\(name): \(Int(studentValueChanger.value))"
}
}
Here is the code for the viewcontroller:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView.delegate = self
tableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 40
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("studentCell") as StudentTableViewCell
return cell
}
}
The problem is in your table view controller. It would be better to put the value-changed method there. Alternatively, review your cellForRowAtIndexPath method. You are not updating the cells correctly when they are recycled.
You have to set the value of stepper and label explicitly in cellForRowAtIndexPath. You cannot read these values from the cell - they should be in your datasource (i.e. the table view controller should know what to display for a given index path).
Connect the stepper handler to the method in the view controller, then identify the proper index path via the sender argument.
#IBAction func stepperChanged(sender: UIStepper) {
let point = sender.convertPoint(CGPointZero, toView: tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(point)!
let myData = dataArray[indexPath.row] // or whatever your datasource
// if you need to update the cell
let cell = self.tableView.cellForRowAtIndexPath(indexPath)
}

Resources