Swift retain UISegmentedControl values in UITableViewCells - ios

I'm creating a quiz app with custom cells that include a label of questions and then an answer coming from a UISegmentedControl.
The values of the segmentedcontrols get changed when scrolling and this leads to an inaccurate score. I understand that this is due to UITableView reusing cells.
My tableview's datasource in my main vc is simply the labels for all my questions coming from a plist file.
The code for my custom tableviewcell class is
class QuestionsTableViewCell: UITableViewCell {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var selection: UISegmentedControl!
var question: String = "" {
didSet {
if (question != oldValue) {
questionLabel.text = question
}
}
}
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
}
//Just for testing
#IBAction func segmentChanged(_ sender: UISegmentedControl) {
print("value is ", sender.selectedSegmentIndex);
}
}
where the View is stored in an .XIB file.
And the code for my main vc is
class ViewController: UIViewController, UITableViewDataSource {
let questionsTableIdentifier = "QuestionsTableIdentifier"
#IBOutlet var tableView:UITableView!
var questionsArray = [String]();
var questionsCellArray = [QuestionsTableViewCell]();
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let path = Bundle.main.path(forResource:
"Questions", ofType: "plist")
questionsArray = NSArray(contentsOfFile: path!) as! [String]
tableView.register(QuestionsTableViewCell.self,
forCellReuseIdentifier: questionsTableIdentifier)
let xib = UINib(nibName: "QuestionsTableViewCell", bundle: nil)
tableView.register(xib,
forCellReuseIdentifier: questionsTableIdentifier)
tableView.rowHeight = 108;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return questionsArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: questionsTableIdentifier, for: indexPath)
as! QuestionsTableViewCell
let rowData = questionsArray[indexPath.row]
cell.question = rowData
return cell
}
#IBAction func calculate(_ sender: UIButton) {
var score = 0
for cell in tableView.visibleCells as! [QuestionsTableViewCell] {
score += cell.selection.selectedSegmentIndex
}
let msg = "Score is, \(score)"
print(msg)
}
#IBAction func reset(_ sender: UIButton) {
for cell in tableView.visibleCells as! [QuestionsTableViewCell] {
cell.selection.selectedSegmentIndex = 0;
}
}
}
What I'd like to do is just keep track of all 'selection' changes of the Questions cells in an array, and then use that array in cellForRowAt. I'm just confused as to how i can dynamically keep track of changes from a view in another class. I'm new to Swift and would like to solve this is a proper MVC fashion. Thanks

Instead of a simple string array as data source create a class holding the text and the selected index
class Question {
let text : String
var answerIndex : Int
init(text : String, answerIndex : Int = 0) {
self.text = text
self.answerIndex = answerIndex
}
}
Declare questionArray as
var questions = [Question]()
Populate the array in viewDidLoad with
let url = Bundle.main.url(forResource: "Questions", withExtension: "plist")!
let data = try! Data(contentsOf: url)
let questionsArray = try! PropertyListSerialization.propertyList(from: data, format: nil) as! [String]
questions = questionsArray.map {Question(text: $0)}
In the custom cell add a callback and call it in the segmentChanged method passing the selected index, the property question is not needed, the label is updated in cellForRow of the controller
class QuestionsTableViewCell: UITableViewCell {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var selection: UISegmentedControl!
var callback : ((Int) -> ())?
#IBAction func segmentChanged(_ sender: UISegmentedControl) {
print("value is ", sender.selectedSegmentIndex)
callback?(sender.selectedSegmentIndex)
}
}
In cellForRow add the callback and update the model in the closure
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: questionsTableIdentifier, for: indexPath) as! QuestionsTableViewCell
let question = questions[indexPath.row]
cell.questionLabel.text = question.text
cell.selection.selectedSegmentIndex = question.answerIndex
cell.callback = { index in
question.answerIndex = index
}
return cell
}
To reset the segmented controls in the cells set the property in the model to 0 and reload the table view
#IBAction func reset(_ sender: UIButton) {
questions.forEach { $0.answerIndex = 0 }
self.tableView.reloadData()
}
Now you could calculate the score directly from the model instead of the view.

Don't try to use cells to hold information. As the user scrolls through your table view, cells that scroll out of view will get recycled and their field settings will be lost. Also, newly dequeued cells will have the settings from the last time they were used.
You need to refactor your code to read/write information into a data model. Using an array of Structs as a data model is a reasonable way to go. (Or, as vadian suggests in his answer, and array of Class objects, so you get reference semantics.)
You have an IBAction segmentChanged() in your custom cell class. The next trick is to notify the view controller when the user changes the selection, and to update cells when you set them up in cellForRowAt.
I suggest defining a protocol QuestionsTableViewCellProtocol, and have the view controller conform to that protocol:
protocol QuestionsTableViewCellProtocol {
func userSelected(segmentIndex: Int, inCell cell: UITableViewCell)
}
}
Add a delegate property to your QuestionsTableViewCell class:
class QuestionsTableViewCell: UITableViewCell {
weak var delegate: QuestionsTableViewCellProtocol?
//The rest of your class goes here...
}
Update your cell's segmentChanged() method to invoke the delegate's userSelected(segmentIndex:inCell:) method.
In your view controller's cellForRowAt, set the cell's delegate to self.
func userSelected(segmentIndex: Int, inCellCell cell: UITableViewCell) {
let indexPath = tableView.indexPath(for: cell)
let row = indexPath.row
//The code below assumes that you have an array of structs, `dataModel`, that
//has a property selectedIndex that remembers which cell is selected.
//Adjust the code below to match your actual array that keeps track of your data.
dataModel[row].selectedIndex = segmentIndex
}
Then update cellforRowAt() to use the data model to set the segment index on the newly dequeued cell to the correct index.
Also update your calculate() function to look at the values in your dataModel to calculate the score, NOT the tableView.
That's a rough idea. I left some details out as "an exercise for the reader." See if you can figure out how to make that work.

Related

Updating label in UITableViewCell with UIStepper in Swift

I'm a Swift beginner and I'm trying to make a simple app for ordering food. The user could add a new order by setting food name, price and serving. After adding an order, that order will be shown on the tableView as a FoodTableViewCell, and the user could change the serving with an UIStepper called stepper in each cell. Each order is a FoodItem stored in an array called foodList, and you can see all orders listed in a tableView in ShoppingListVC.
My problem is: When I press "+" or "-" button on stepper, my servingLabel doesn't change to corresponding value. I tried to use NotificationCenter to pass serving value to stepper, and store new value back to food.serving after stepperValueChanged with delegate pattern. However, there still seems to be some bugs. I've been kind of confused after browsing lots of solutions on the Internet. Any help is appreciated.
Update
I removed NotificationCenter and addTarget related methods as #Tarun Tyagi 's suggestion. Now my UIStepper value turns back to 1 whereas the servingLabels are showing different numbers of serving. Since NotificationCenter doesn't help, how can I connect the label and stepper value together? Is it recommended to implement another delegate?
Here are my codes(Updated on July 8):
FoodItem
class FoodItem: Equatable {
static func == (lhs: FoodItem, rhs: FoodItem) -> Bool {
return lhs === rhs
}
var name: String
var price: Int
var serving: Int
var foodID: String
init(name: String, price: Int, serving: Int) {
self.name = name
self.price = price
self.serving = serving
self.foodID = UUID().uuidString
}
}
ViewController
import UIKit
class ShoppingListVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
var foodList = [FoodItem]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
...
for i in 1...5 {
let testItem = FoodItem(name: "Food\(i)", price: Int.random(in: 60...100), serving: Int.random(in: 1...10))
self.foodList.append(testItem)
}
}
// MARK: - Table view data source
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "foodCell", for: indexPath) as! FoodTableViewCell
let food = foodList[indexPath.row]
cell.nameLabel.text = food.name
cell.priceLabel.text = "$\(String(food.price)) / serving"
cell.servingLabel.text = "\(String(food.serving)) serving"
cell.stepper.tag = indexPath.row
cell.delegate = self
return cell
}
}
// MARK: - FoodTableViewCellDelegate Method.
extension ShoppingListVC: FoodTableViewCellDelegate {
func stepper(_ stepper: UIStepper, at index: Int, didChangeValueTo newValue: Double) {
let indexPath = IndexPath(item: index, section: 0)
guard let cell = tableView.cellForRow(at: indexPath) as? FoodTableViewCell else { return }
let foodToBeUpdated = foodList[indexPath.row]
print("foodToBeUpdated.serving: \(foodToBeUpdated.serving)")
foodToBeUpdated.serving = Int(newValue)
print("Value changed in VC: \(newValue)")
cell.servingLabel.text = "\(String(format: "%.0f", newValue)) serving"
}
}
TableViewCell
import UIKit
protocol FoodTableViewCellDelegate: AnyObject {
func stepper(_ stepper: UIStepper, at index: Int, didChangeValueTo newValue: Double)
}
class FoodTableViewCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var priceLabel: UILabel!
#IBOutlet weak var servingLabel: UILabel!
#IBOutlet weak var stepper: UIStepper!
weak var delegate: FoodTableViewCellDelegate?
#IBAction func stepperValueChanged(_ sender: UIStepper) {
sender.minimumValue = 1
servingLabel.text = "\(String(format: "%.0f", sender.value)) serving"
// Pass the new value to ShoppingListVC and notify which cell to update using tag.
print("sender.value: \(sender.value)")
delegate?.stepper(stepper, at: stepper.tag, didChangeValueTo: sender.value)
}
override func awakeFromNib() {
super.awakeFromNib()
print(stepper.value)
}
}
Initially FoodTableViewCell is the ONLY target for UIStepper value changed (looking at #IBAction inside FoodTableViewCell).
When you dequeue a cell to display on screen, you call -
cell.stepper.addTarget(self, action: #selector(stepperValueChanged(_:)), for: .valueChanged)
which causes your ShoppingListVC instance to be added as an additional target every time a cellForRow call is executed.
Things to fix :
Remove all of your NotificationCenter related code from both classes.
Remove cell.stepper.addTarget() line as well.
This would give you a better idea of why it is happening this way. Update your question with these changes in case you still don't have what you want.
UPDATE
// Inside cellForRow
cell.stepper.value = food.serving
Cell Config:
protocol FoodTableViewCellDelegate: AnyObject {
func stepper(sender: FoodTableViewCell)
}
#IBAction func stepperButtonTapped(sender: UIStepper) {
delegate?.stepperButton(sender: self)
stepperLabel.text = "\(Int(countStepper.value))"
}
Controller Config:
cellForRow:
cell.countStepper.value = Double(foodList[indexPath.row].serving);
cell.stepperLabel.text = "\(Int(cell.countStepper.value))"
Delegate Method:
func stepperButton(sender: FoodTableViewCell) {
if let indexPath = tableView.indexPath(for: sender){
print(indexPath)
foodList[sender.tag].serving = Int(sender.countStepper.value)
}
}
Please check value stepper pod it will help you: Value stepper
Integrate value stepper pod and use below code for basic implementation.
import ValueStepper
let valueStepper: ValueStepper = {
let stepper = ValueStepper()
stepper.tintColor = .whiteColor()
stepper.minimumValue = 0
stepper.maximumValue = 1000
stepper.stepValue = 100
return stepper
}()
override func viewDidLoad() {
super.viewDidLoad()
valueStepper.addTarget(self, action: "valueChanged:", forControlEvents: .ValueChanged)
}
#IBAction func valueChanged1(sender: ValueStepper) {
// Use sender.value to do whatever you want
}
Its simplify custom stepper implantation.Take outlet of value stepper view in table tableview and use it.

UITableView: reloadRows(at:) takes two hits for table to be updated and scroll every time

I have a table view (controller: MetricsViewController) which gets updated from a CoreData database. I have used prototype cells (MetricsViewCell) which I have customized for my needs. It contains a segmented control, a UIView (metricsChart, which is used to display a chart - animatedCircle), and some UILabels.
MetricsViewCell:
class MetricsViewCell: UITableViewCell {
var delegate: SelectSegmentedControl?
var animatedCircle: AnimatedCircle?
#IBOutlet weak var percentageCorrect: UILabel!
#IBOutlet weak var totalPlay: UILabel!
#IBOutlet weak var metricsChart: UIView! {
didSet {
animatedCircle = AnimatedCircle(frame: metricsChart.bounds)
}
}
#IBOutlet weak var recommendationLabel: UILabel!
#IBOutlet weak var objectType: UISegmentedControl!
#IBAction func displayObjectType(_ sender: UISegmentedControl) {
delegate?.tapped(cell: self)
}
}
protocol SelectSegmentedControl {
func tapped(cell: MetricsViewCell)
}
MetricsViewController:
class MetricsViewController: FetchedResultsTableViewController, SelectSegmentedControl {
func tapped(cell: MetricsViewCell) {
if let indexPath = tableView.indexPath(for: cell) {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
var container: NSPersistentContainer? = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer { didSet { updateUI() } }
private var fetchedResultsController: NSFetchedResultsController<Object>?
private func updateUI() {
if let context = container?.viewContext {
let request: NSFetchRequest<Object> = Object.fetchRequest()
request.sortDescriptors = []
fetchedResultsController = NSFetchedResultsController<Object>(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: "game.gameIndex",
cacheName: nil)
try? fetchedResultsController?.performFetch()
tableView.reloadData()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Object Cell", for: indexPath)
if let object = fetchedResultsController?.object(at: indexPath) {
if let objectCell = cell as? MetricsViewCell {
objectCell.delegate = self
let request: NSFetchRequest<Object> = Object.fetchRequest()
...
...
}
}
}
return cell
}
When a user selects one of the segments in a certain section's segmented control, MetricsViewController should reload the data in that particular row. (There are two sections with one row each). Hence, I've defined a protocol in MetricsViewCell to inform inform my controller on user action.
Data is being updated using FetchedResultsTableViewController - which basically acts as a delegate between CoreData and TableView. Everything is fine with that, meaning I am getting the correct data into my TableView.
There are two issues:
I have to tap segmented control's segment twice to reload the data in the row where segmented control was tapped.
The table scrolls back up and then down every time a segment from segmented control is selected.
Help would be very much appreciated. I've depended on this community for a lot of issues I've faced during the development and am thankful already :)
For example, in Animal Recognition section, I have to hit "Intermediate" two times for its row to be reloaded (If you look closely, the first time I hit Intermediate, it gets selected for a fraction of second, then it goes back to "Basic" or whatever segment was selected first. Second time when I hit intermediate, it goes to Intermediate). Plus, the table scroll up and down, which I don't want.
Edit: Added more context around my usage of CoreData and persistent container.
Instead of using indexPathForRow(at: <#T##CGPoint#>) function to get the indexPath object of cell you can directly use indexPath(for: <#T##UITableViewCell#>) as you are receiving the cell object to func tapped(cell: MetricsViewCell) {} and try to update your data on the UI always in main thready as below.
func tapped(cell: MetricsViewCell) {
if let lIndexPath = table.indexPath(for: <#T##UITableViewCell#>){
DispatchQueue.main.async(execute: {
table.reloadRows(at: lIndexPath, with: .none)
})
}
}
Your UISegmentedControl are reusing [Default behaviour of UITableView].
To avoid that, keep dictionary for getting and storing values.
Another thing, try outlet connection as Action for UISegmentedControl in UIViewController itself, instead of your UITableViewCell
The below code will not reload your tableview when you tap UISegmentedControl . You can avoid, delegates call too.
Below codes are basic demo for UISegmentedControl. Do customise as per your need.
var segmentDict = [Int : Int]()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...29 // number of rows count
{
segmentDict[i] = 0 //DEFAULT SELECTED SEGMENTS
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SOTableViewCell
cell.mySegment.selectedSegmentIndex = segmentDict[indexPath.row]!
cell.selectionStyle = .none
return cell
}
#IBAction func mySegmentAcn(_ sender: UISegmentedControl) {
let cellPosition = sender.convert(CGPoint.zero, to: tblVw)
let indPath = tblVw.indexPathForRow(at: cellPosition)
segmentDict[(indPath?.row)!] = sender.selectedSegmentIndex
print("Sender.tag ", indPath)
}

Properly delegate button action from custom Cell to delete rows in UITableView

Still very much a Swift noob, I have been looking around for a proper way/best practice to manage row deletions in my UITableView (which uses custom UserCells) based on tapping a UIButton inside the UserCell using delegation which seems to be the cleanest way to do it.
I followed this example: UITableViewCell Buttons with action
What I have
UserCell class
protocol UserCellDelegate {
func didPressButton(_ tag: Int)
}
class UserCell: UITableViewCell {
var delegate: UserCellDelegate?
let addButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Add +", for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
addSubview(addButton)
addButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -6).isActive = true
addButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
addButton.heightAnchor.constraint(equalToConstant: self.frame.height / 2).isActive = true
addButton.widthAnchor.constraint(equalToConstant: self.frame.width / 6).isActive = true
}
func buttonPressed(_ sender: UIButton) {
delegate?.didPressButton(sender.tag)
}
}
TableViewController class:
class AddFriendsScreenController: UITableViewController, UserCellDelegate {
let cellId = "cellId"
var users = [User]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! UserCell
cell.delegate = self
cell.tag = indexPath.row
return cell
}
func didPressButton(_ tag: Int) {
let indexPath = IndexPath(row: tag, section: 0)
users.remove(at: tag)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
where the Users in users are appended with a call to the database in the view controller.
My issues
The button in each row of the Table View is clickable but does not do anything
The button seems to be clickable only when doing a "long press", i.e. finger stays on it for a ~0.5s time
Will this method guarantee that the indexPath is updated and will not fall out of scope ? I.e. if a row is deleted at index 0, will deleting the "new" row at index 0 work correctly or will this delete the row at index 1 ?
What I want
Being able to click the button in each row of the table, which would remove it from the tableview.
I must be getting something rather basic wrong and would really appreciate if a Swift knight could enlighten me.
Many thanks in advance.
There are at least 3 issues in your code:
In UserCell you should call:
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
once your cell has been instantiated (say, from your implementation of init(style:reuseIdentifier:)) so that self refers to an actual instance of UserCell.
In AddFriendsScreenController's tableView(_:cellForRowAt:) you are setting the tag of the cell itself (cell.tag = indexPath.row) but in your UserCell's buttonPressed(_:) you are using the tag of the button. You should modify that function to be:
func buttonPressed(_ sender: UIButton) {
//delegate?.didPressButton(sender.tag)
delegate?.didPressButton(self.tag)
}
As you guessed and as per Prema Janoti's answer you ought to reload you table view once you deleted a row as your cells' tags will be out of sync with their referring indexPaths. Ideally you should avoid relying on index paths to identify cells but that's another subject.
EDIT:
A simple solution to avoid tags being out of sync with index paths is to associate each cell with the User object they are supposed to represent:
First add a user property to your UserCell class:
class UserCell: UITableViewCell {
var user = User() // default with a dummy user
/* (...) */
}
Set this property to the correct User object from within tableView(_:cellForRowAt:):
//cell.tag = indexPath.row
cell.user = self.users[indexPath.row]
Modify the signature of your UserCellDelegate protocol method to pass the user property stored against the cell instead of its tag:
protocol UserCellDelegate {
//func didPressButton(_ tag: Int)
func didPressButtonFor(_ user: User)
}
Amend UserCell's buttonPressed(_:) action accordingly:
func buttonPressed(_ sender: UIButton) {
//delegate?.didPressButton(sender.tag)
//delegate?.didPressButton(self.tag)
delegate?.didPressButtonFor(self.user)
}
Finally, in your AddFriendsScreenController, identify the right row to delete based on the User position in the data source:
//func didPressButton(_ tag: Int) { /* (...) */ } // Scrap this.
func didPressButtonFor(_ user: User) {
if let index = users.index(where: { $0 === user }) {
let indexPath = IndexPath(row: index, section: 0)
users.remove(at: index)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
Note the if let index = ... construct (optional binding) and the triple === (identity operator).
This downside of this approach is that it will create tight coupling between your User and UserCell classes. Best practice would dictate using a more complex MVVM pattern for example, but that really is another subject...
There is a lot of bad/old code on the web, even on SO. What you posted has "bad practice" written all over it. So first a few pointers:
Avoid an UITableViewController at all cost. Have a normal view controller with a table view on it
Delegates should always be weak unless you are 100% sure what you are doing
Be more specific when naming protocols and protocol methods
Keep everything private if possible, if not then use fileprivate. Only use the rest if you are 100% sure it is a value you want to expose.
Avoid using tags at all cost
The following is an example of responsible table view with a single cell type which has a button that removes the current cell when pressed. The whole code can be pasted into your initial ViewController file when creating a new project. In storyboard a table view is added constraint left, right, top, bottom and an outlet to the view controller. Also a cell is added in the table view with a button in it that has an outlet to the cell MyTableViewCell and its identifier is set to "MyTableViewCell".
The rest should be explained in the comments.
class ViewController: UIViewController {
#IBOutlet private weak var tableView: UITableView? // By default use private and optional. Always. For all outlets. Only expose it if you really need it outside
fileprivate var myItems: [String]? // Use any objects you need.
override func viewDidLoad() {
super.viewDidLoad()
// Attach table viw to self
tableView?.delegate = self
tableView?.dataSource = self
// First refresh and reload the data
refreshFromData() // This is to ensure no defaults are visible in the beginning
reloadData()
}
private func reloadData() {
myItems = nil
// Simulate a data fetch
let queue = DispatchQueue(label: "test") // Just for the async example
queue.async {
let items: [String] = (1...100).flatMap { "Item: \($0)" } // Just generate some string
Thread.sleep(forTimeInterval: 3.0) // Wait 3 seconds
DispatchQueue.main.async { // Go back to main thread
self.myItems = items // Assign data source to self
self.refreshFromData() // Now refresh the table view
}
}
}
private func refreshFromData() {
tableView?.reloadData()
tableView?.isHidden = myItems == nil
// Add other stuff that need updating here if needed
}
/// Will remove an item from the data source and update the array
///
/// - Parameter item: The item to remove
fileprivate func removeItem(item: String) {
if let index = myItems?.index(of: item) { // Get the index of the object
tableView?.beginUpdates() // Begin updates so the table view saves the current state
myItems = myItems?.filter { $0 != item } // Update our data source first
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade) // Do the table view cell modifications
tableView?.endUpdates() // Commit the modifications
}
}
}
// MARK: - UITableViewDelegate, UITableViewDataSource
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myItems?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as? MyTableViewCell {
cell.item = myItems?[indexPath.row]
cell.delegate = self
return cell
} else {
return UITableViewCell()
}
}
}
// MARK: - MyTableViewCellDelegate
extension ViewController: MyTableViewCellDelegate {
func myTableViewCell(pressedMainButton sender: MyTableViewCell) {
guard let item = sender.item else {
return
}
// Delete the item if main button is pressed
removeItem(item: item)
}
}
protocol MyTableViewCellDelegate: class { // We need ": class" so the delegate can be marked as weak
/// Called on main button pressed
///
/// - Parameter sender: The sender cell
func myTableViewCell(pressedMainButton sender: MyTableViewCell)
}
class MyTableViewCell: UITableViewCell {
#IBOutlet private weak var button: UIButton?
weak var delegate: MyTableViewCellDelegate? // Must be weak or we can have a retain cycle and create a memory leak
var item: String? {
didSet {
button?.setTitle(item, for: .normal)
}
}
#IBAction private func buttonPressed(_ sender: Any) {
delegate?.myTableViewCell(pressedMainButton: self)
}
}
In your case the String should be replaced by the User. Next to that you will have a few changes such as the didSet in the cell (button?.setTitle(item.name, for: .normal) for instance) and the filter method should use === or compare some id or something.
try this -
update didPressButton method like below -
func didPressButton(_ tag: Int) {
let indexPath = IndexPath(row: tag, section: 0)
users.remove(at: tag)
tableView.reloadData()
}

How to access custom cell textfield values Swift 3.0

I have created a custom tableViewCell class for a prototype cells which is holding a text field. Inside ThirteenthViewController, I would like to reference the tableViewCell class so that I can access its doorTextField property in order to assign to it data retrieved from UserDefaults. How can I do this?
class ThirteenthViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,UITextFieldDelegate {
var options = [
Item(name:"Doorman",selected: false),
Item(name:"Lockbox",selected: false),
Item(name:"Hidden-Key",selected: false),
Item(name:"Other",selected: false)
]
let noteCell:NotesFieldUITableViewCell! = nil
#IBAction func nextButton(_ sender: Any) {
//save the value of textfield when button is touched
UserDefaults.standard.set(noteCell.doorTextField.text, forKey: textKey)
//if doorTextField is not empty assign value to FullData
guard let text = noteCell.doorTextField.text, text.isEmpty else {
FullData.finalEntryInstructions = noteCell.doorTextField.text!
return
}
FullData.finalEntryInstructions = "No"
}
override func viewDidLoad() {
let index:IndexPath = IndexPath(row:4,section:0)
let cell = tableView.cellForRow(at: index) as! NotesFieldUITableViewCell
self.tableView.delegate = self
self.tableView.dataSource = self
cell.doorTextField.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return options.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
// configure the cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
if indexPath.row < 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = options[indexPath.row].name
return cell
} else {
let othercell = tableView.dequeueReusableCell(withIdentifier: "textField") as! NotesFieldUITableViewCell
othercell.doorTextField.placeholder = "some text"
return othercell
}
}
}//end of class
class NotesFieldUITableViewCell: UITableViewCell {
#IBOutlet weak var doorTextField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
In order to access the UITextField in your cell, you need to know which row of the UITableView contains your cell. In your case, I believe the row is always the 4th one. So, you can create an IndexPath for the row and then, you can simply do something like this:
let ndx = IndexPath(row:3, section: 0)
let cell = table.cellForRow(at:ndx) as! NotesFieldUITableViewCell
let txt = cell.doorTextField.text
The above might not be totally syntactically correct since I didn't check for syntax, but I'm sure you can take it from there, right?
However, do note that in order for the above to work, the last row (row 4) has to be always visible. If you try to fetch rows which are not visible, you do run into issues with accessing them since UITableView reuses cells and instantiates cells for the visible rows of data.
Also, if you simply want to get the text that the user types and text input ends when they tap "Enter", you can always simply bypass accessing the table row at all and add a UITextFieldDelegate to your custom cell to send a notification out with the entered text so that you can listen for the notification and take some action.
But, as I mentioned above, this all depends on how you have things set up and what you are trying to achieve :)
Update:
Based on further information, it appears as if you want to do something with the text value when the nextButton method is called. If so, the following should (theoretically) do what you want:
#IBAction func nextButton(_ sender: Any) {
// Get the cell
let ndx = IndexPath(row:4, section: 0)
let cell = table.cellForRow(at:ndx) as! NotesFieldUITableViewCell
//save the value of textfield when button is touched
UserDefaults.standard.set(cell.doorTextField.text, forKey: textKey)
//if doorTextField is not empty assign value to FullData
guard let text = cell.doorTextField.text, text.isEmpty else {
FullData.finalEntryInstructions = cell.doorTextField.text!
return
}
FullData.finalEntryInstructions = "No"
}
You can create a tag for the doorTextField (for instance 111)
Now you can retrieve the value.
#IBAction func nextButton(_ sender: Any) {
//save the value of textfield when button is touched
guard let textField = self.tableViewview.viewWithTag(111) as! UITextField? else { return }
prit(textField.text)
.....
}

How to change textView in separate ViewController from button in another ViewController?

I have a Collections View that includes various cells that are also buttons. Upon clicking one of those buttons, a separate ViewController is shown (I just connected the button to the ViewController and clicked "show"). I would like to know how to switch the textView in that separate ViewController for every different button in my Collections View.
Here's the code in my CollectionsView:
class Example : UICollectionViewController {
var Array = [String]()
var ButtonArray = [String]()
#IBOutlet var menu: UIBarButtonItem!
override func viewDidLoad() {
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
Array = ["A", "B", "C, "D]
ButtonArray = ["", "", "", ""]
menu.target = self.revealViewController()
menu.action = Selector("revealToggle:")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return Array.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! UICollectionViewCell
var Label = cell.viewWithTag(1) as! UILabel
Label.text = Array[indexPath.row]
var Button = cell.viewWithTag(2) as! UIButton
Button.setTitle(ButtonArray[indexPath.row], forState: UIControlState.Normal)
return cell
}
}
And the code in the separate ViewController:
class Example2: UIViewController {
#IBOutlet var exText: UITextView!
override func viewDidLoad() {
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Follow these steps...
1: Create a Separate file called Manager.swift and place this code in it...
//manager.swift
import Foundation
struct Manager {
static var messageText = [String]()
}
2: Clean your project by pressing Shift+Command+K.
3: In the first view controller set the messageText to the text fields text...
Manager.messageText.append(self.textField.text)
4: In the second view controller retrieve the text and set the label to the message text...
self.label.text = Manager.messageText[0]
5: Your Finished!!
That was one way, and it relies on creating a global variable that can be defined out of the scope of any available class, no new file needed here. Just define the variable above (or below) the class code.
Another good way is to use UserDefaults that are automatically saved in app settings file, this way the variable is retained, and when you restart the app again the value will be the same you left with.
Swift 3+
To store/change a value:
UserDefaults.standard.setValue(yourValue, forKey: "yourKey")
To retrieve a value
UserDefaults.standard.value(forKey: "yourKey")

Resources