Table view with button that changes label based on user input - ios

I'm trying to create a table view with just 1 label and 1 button in the prototype cell.
When you click the button, it asks the user for text input, and then replaces the label in the same cell with that text. I have been able to create a version of this where pressing the button updates the corresponding label with pre-determined text, but not input text.
The problems are:
(a) Can't seem to run an alert asking for user input in the TableViewCell class I created - must be in the ViewController to do that it seems?
(b) Have set up a TableCellDelegate protocol, and can detect a button press, then pass back to the ViewController to run the alert, but can't find a way to send the text input back to the TableViewCell to be updated.
Any help would be appreciated.
Here is the ViewController code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
extension ViewController: TableCellDelegate {
func didTapButton(label: String) {
print(label)
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell") as! TableViewCell
let label = "Label " + String(indexPath.row)
cell.setLabel(label: label)
cell.delegate = self
return cell
}
}
And here is the TableViewCell (without the alert coding):
import UIKit
protocol TableCellDelegate {
func didTapButton (label: String)
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var tableCellLabel: UILabel!
var delegate: TableCellDelegate?
func setLabel(label: String) {
tableCellLabel.text = label
}
#IBAction func buttonTapped(_ sender: Any) {
delegate?.didTapButton(label: tableCellLabel.text!)
tableCellLabel.text = "pressed"
}
}
Finally, here is the alert code I am trying to insert in place of the tableCellLabel.text = "pressed" code above:
// Create the alert window
let buttonAlert = UIAlertController(title: "Label", message: "Enter Label", preferredStyle: UIAlertControllerStyle.alert)
// Add text input field to alert controller
buttonAlert.addTextField { (buttonLabel:UITextField!) -> Void in
buttonLabel.placeholder = "Enter Label"
}
// Create alert action for OK button
let okButtonAction = UIAlertAction.init(title: "OK", style: UIAlertActionStyle.default) { (alert:UIAlertAction!) -> Void in
// Get the button name from the alert text field
let buttonLabel = buttonAlert.textFields![0]
let buttonLabelString = buttonLabel.text
self.tableCellLabel.text = buttonLabelString
// Dismiss alert if ok pressed
buttonAlert.dismiss(animated: true, completion: nil)
}
// Add ok Button alert actions to alert controller
buttonAlert.addAction(okButtonAction)
// Create alert action for a cancel button
let cancelAction = UIAlertAction.init(title: "Cancel", style: UIAlertActionStyle.cancel) { (alert:UIAlertAction!) -> Void in
// Dismiss alert if cancel pressed
buttonAlert.dismiss(animated: true, completion: nil)
}
// Add Cancel Button alert actions to alert controller
buttonAlert.addAction(cancelAction)
// Display the alert window
self.present(buttonAlert, animated: true, completion: nil)
Where this goes wrong is the self.present(buttonAlert, animated: true, completion: nil), which it doesn't appear you can call from a table cell.

Depending on how you have the UItableview set up, it sounds like you're going to want to take the id of the data of the cell that is registering the alert and then updating the button text in that cell based on that within the table view render statement. Remember that setting the label of a button has an animation, it can cause some odd things to happen if you're animating other things at the same time.
You'll probably want to look at reload rows in the link below in your table view. Be careful though, the indexPath is not always the same as when the user interaction is called if you're using dequeueReusableCell.
UITableView Reference
See UIButton Reference for swift implementation, you'll have to set it for the current state of the button.

Related

Is there an alternative to ViewDidLoad? Any other place to write code which needs to be activated, when a viewController loads?

I have a viewController which holds three text fields. Each text field is connected to a label and uses NSUser Default so that when a user writes something in the textfield, the text is saved with a key - and showed in the label assigned to it. That works fine.
Two of the three text field are for the user to write "a question" and an "extra identifier". In case the user don't know what to write in these, in the top of the screen there are two buttons leading to two different tableViewControllers - both of which holds tableViews with inspiration/options that the user can choose. I implemented a segue from each, so that when the user taps a tableViewCell, the text in this cell is passed to the right textfield in viewController1 - and the user can now press save and save this text in the NSUser Default and let it be shown in the assigned label.
The problem is that these segues are ofcourse using ViewDidLoad to show the passed text from the inspiration tableViews. This means that when you enter the second tableViewController with inspiration after saving you chosen "question" from the first, then - because of the viewDidLoad method - all textfields and labels are cleared again and only the thing you just passed in there is shown... So:
Can I use another method than viewDidLoad to pass the data into ViewController1?
Or can I maybe add some code to the viewDidLoad method, so that it stops "resetting" all labels and textfields every time you enter the viewController?
Or do I need to go about it all a completely different way?
I hope it all makes sense - sorry about the long explanation! :D
Here's the relevant code from viewController1:
var chosenQuestion:String = ""
var chosenIdentifier:String = ""
override func viewDidLoad() {
super.viewDidLoad()
DiaryQuestionTextField.text = chosenQuestion
RandomIdentifierTextField.text = chosenIdentifier
}
#IBAction func WriteDiaryName() {
let title = "You have now chosen you diary's name. \n \n Do you wish to save this name and continue? \n \n You can always change all the details."
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Save", style: .default, handler: { action in
self.defaults.setValue(self.DiaryNameTextField.text, forKey: "DiaryNameKey")
let NewDiaryName = self.defaults.string(forKey: "DiaryNameKey")
self.DiaryNameLabel.text = NewDiaryName
}))
present(alert, animated: true, completion: nil)
}
And there is ofcourse two similar methods for "WriteQuestion" and "WriteExtraIdentifier".
And here's the relevant code from one of the two tableViewControllers with "inspiration":
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is UpdatingIdentifiers {
let vc = segue.destination as? UpdatingIdentifiers
let selectedRowIndex = self.tableView.indexPathForSelectedRow
let cell = self.tableView.cellForRow(at: selectedRowIndex!)
let label = cell?.viewWithTag(420) as! UILabel
vc?.chosenQuestion = label.text!
}
}
Thanks!
You can simply use a closure approach for this scenario.
Create a closure variable in ViewController2.
Set the value of this closure whenever you present ViewController2 from ViewController1.
Call the closure whenever you select a row in tableView of ViewController2 and pass the selected value in the closure.
Update the chosenQuestion whenever you get a new value from closure using which in turn will update the textField's value using the property observer.
Example Code:
class ViewController1: UIViewController
{
#IBOutlet weak var DiaryQuestionTextField: UITextField!
var chosenQuestion = "" {
didSet{
//Textfield will be updated everytime a new value is set in chosenQuestion variable
self.DiaryQuestionTextField.text = self.chosenQuestion
}
}
#IBAction func showInspirationVC(_ sender: UIButton)
{
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController2") as! ViewController2
controller.handler = {[weak self](data) in
self?.chosenQuestion = data
}
self.present(controller, animated: true, completion: nil)
}
}
class ViewController2: UIViewController, UITableViewDelegate, UITableViewDataSource
{
var handler: ((String)->())?
var data = ["Question-1", "Question-2", "Question-3", "Question-4", "Question-5"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return self.data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = self.data[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
self.dismiss(animated: true) {
self.handler?(self.data[indexPath.row])
}
}
}
Storyboard:
Try implementing a simple use case first. And you don't need segues in this approach since you are presenting the controller programatically.
Let me know in case of any issue.
Simple, you just need to re-initialise "chosenQuestion" and "chosenIdentifier" with the values from UserDefaults in the viewDidLoad and you are good to go.

How to present view controller from UILabel with tap gesture recognized in custom table view cell?

I have a custom dynamic table view cell with a label that has a tap gesture recognized added. When the user taps the label, not anywhere else in the cell, I want to present a view controller.
The instagram app has this feature. Ie. when you tap likes, it takes you to a likes table view, when you tap comments, it shows you to a comments table view. This is the same experience I want.
I am not looking to use didSelectRow because then it kind of defeats the purpose of having the specific target area to tap to show a new view controller.
So, how can I present a view controller from a tap gesture recognizer in a subclass of UITableViewCell?
UPDATED:
I am passing a closure to my custom TableViewCell which is successfully being called when the button is pressed. But I am stuck in the TableView and cannot pass information to the next View Controller I want to present. And I can't actaully perform the segue either :\
// From UITableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let story = stories[indexPath.row]
if let cell = tableView.dequeueReusableCell(withIdentifier: "Fun Cell", for: indexPath) as? FunTableViewCell {
cell.configureCell(title: story.title, info: story.info)
cell.buttonAction = { [weak self] (cell) in
print("the button was pressed for \(story.title)")
self?.buttonWAsTapped(title: story.title)
}
return cell
} else {
return FunTableViewCell()
}
}
func buttonWAsTapped(title: String) {
// Need to pass something to the next View Controller... but how???
if let nextVC = UIViewController() as? DetailViewController {
nextVC.storyTitle = title
performSegue(withIdentifier: "Button Pressed", sender: self)
}
}
// Custom TableViewCell
class FunTableViewCell: UITableViewCell {
#IBOutlet weak var funLabel: FunLabel!
#IBOutlet weak var standardLabel: TappedLabel!
#IBOutlet weak var funButton: FunButton!
var buttonAction: ((UITableViewCell) -> Void)?
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: #selector(readMoreTapped))
tap.numberOfTapsRequired = 1
standardLabel.addGestureRecognizer(tap)
standardLabel.isUserInteractionEnabled = true
}
#IBAction func btnPressed(sender: UIButton) {
print("Button pressed")
buttonAction?(self)
}
When creating the cell, pass a block to it. That's the handler for the button.
When the button tapped, call the block.
You can save the block as a property of the subclass of UITableViewCell.
In your tableViewCell class, add a property:
class CustomTableViewCell: UITableViewCell {
open var completionHandler: (()->Void)?
}
In your viewController that has the tableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = CustomTableViewCell()
cell.completionHandler = {
() -> Void in
let newViewController = UIViewController()
//configure the VC here base on the indexPath
self.present(newViewController, animated: true, completion: nil)
}
return cell
}

Adding items to tableview with sections

I display a shopping list for different product groups as a table view with multiple sections. I want to add items with an add button for each group. So I equipped the header cell with a UIToolbar and a + symbol as a UIBarButtonItem.
Now every product group has an add button of his own:
If one add button was pressed, I have a problem identifying which one was pressed.
If I connect the add button with a seque, the function prepareForSeque(...) delivers a sender of type UIBarButtomItem, but there is no connection to the header cell from were the event was triggered.
If I connect an IBAction to the UITableViewController, the received sender is also of type UIBarButtomItem, and there is no connection to the header cell, too.
If I connect an IBAction to my CustomHeaderCell:UITableViewCell class, I am able to identify the right header cell.
This line of code returns the header cell title:
if let produktTyp = (sender.target! as! CustomHeaderCell).headerLabel.text
However, now the CustomHeaderCell class has the information I need.
But this information should be available in the UITableViewController.
I couldn't find a way to feed the information back to the UITableViewController.
import UIKit
class CustomHeaderCell: UITableViewCell {
#IBOutlet var headerLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
#IBAction func neuesProdukt(sender: UIBarButtonItem) {
if let produktTyp = (sender.target! as! CustomHeaderCell).headerLabel.text
{
print(produktTyp)
}
}
}
Here's how I typically handle this:
Use a Closure to capture the action of the item being pressed
class CustomHeaderCell: UITableViewCell {
#IBOutlet var headerLabel: UILabel!
var delegate: (() -> Void)?
#IBAction func buttonPressed(sender: AnyObject) {
delegate?()
}
}
When the Cell is created, create a closure that captures either the Index Path or the appropriate Section.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let section = indexPath.section
let cell = createCell(indexPath)
cell.delegate = { [weak self] section in
self?.presentAlertView(forSection: section)
}
}

swift couldn't store data back to array, even during seguing

I have two viewcontrollers, one is rootviewcontroller, the other one is selectorviewcontroller. In rootvc, there is a textfield and button, when the button is clicked, it takes us to the selectorvc where we can choose and if necessary add a new item (area) and then choose the item, after we choose it, it takes us back to the rootvc, and display the selected item in the textfield. I understand that if we don't use data persistence measures, the data added in won't persist after we recommence the app. Although I can add in new item to the selectorvc, but the newly added data just gone even after we unwind the segue back to rootvc and re-enter the selectorvc. I am not sure where I did wrong, as the data storing array is mutable. It is great if you could pointing me to the right direction. Thanks a lot.
A simple array is defined to store the data,
import UIKit
class AreaClass {
var areaName: String
init? (areaName: String) {
self.areaName = areaName
if areaName.isEmpty {
return nil
}
}
}
This is the unwind segue in the rootvc,
#IBAction func unwindWithSelectedArea(segue:UIStoryboardSegue) {
if let SelectorViewController = segue.sourceViewController as? SelectorViewController,
selectedArea = SelectorViewController.selectedArea
{
AreaSelectedTextField.text = selectedArea
}
}
This is the declaration and addnewitem in the selectorvc,
var selectedArea: String?
var selectedAreaIndex: Int?
var areas = [AreaClass]()
var newarea = AreaClass?()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
areaNewnSelectedTF.delegate = self
saveButton.enabled = false
loadSample()
// Do any additional setup after loading the view.
}
func loadSample (){
let area1 = AreaClass(areaName: "TopHill")!
let area2 = AreaClass(areaName: "Foothill")!
let area3 = AreaClass(areaName: "Summit")!
let area4 = AreaClass(areaName: "Riverside")!
areas += [area1, area2, area3, area4]
}
#IBAction func addNewArea(sender: UIBarButtonItem) {
var dupli = false
if saveButton == sender {
let areaname = areaNewnSelectedTF.text ?? ""
newarea = AreaClass(areaName: areaname)
for var index = 0; index < areas.count; ++index {
if areaname == areas[index].areaName {
dupli = true
// Mark: alert for duplicate inputs
let alert = UIAlertController(title: "Duplicate", message: "Can't have same items", preferredStyle:.Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
presentViewController(alert, animated: true, completion: nil)
}
}
if dupli == false {
let newIndexPath = NSIndexPath(forRow: areas.count, inSection: 0)
areas.append(newarea!)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
}
}
This is the PrepareforSegue in selectorvc
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "saveSelectionSegue" {
if let cell = sender as? UITableViewCell {
let indexPath = tableView.indexPathForCell(cell)
if let index = indexPath?.row {
selectedArea = areas[index].areaName
}
}
}
}
You need to use a delegate to pass data from a childViewController to your rootViewController.
Below there are ViewController(root) and SelectTableViewController(selector), they implement a simple example of what you look for. I used a struct to make it simple.
At first, you need to create a protocol using the role of your delegate => ViewControllerDelegate with a simple function with an argument "AreaStruct".
This protocol needs to be implemented by the "root". When you pressed the button, you performSegueWithIdentifier that will call prepareForSegue. In it, you pass yourself(ViewControllerDelegate) to the destinationViewController.
In your destinationViewController, the user select one row. By selecting, it triggers didSelectRowAtIndex. In it, you get the selectedArea (the concern AreaStruct) and with your delegate, you call choseArea(...). After that you pop the ViewController to go back to the root.
When you call choseArea, it will put the areaName into the label to display what you selected as shown by the implementation of choseArea(areaStruct : AreaStruct) in your RootViewController.(In this function, you can do whatever you want with your areaStruct)
------ EDIT ------
In your code, your "selectorviewcontroller" create the data => loadSample. So no matter what happened when you go to "selectorviewcontroller", you will always loadSample even if you added new "areas" before. I updated my example code based on your example.
ViewController -> press button -> SelectTableViewController -> add area -> select area -> back to ViewController
To summarise,
in ViewController, I setupListData when viewDidLoad is called
I pressed the button to go to SelectTableViewController
3 I passed myself(delegate) and dataList to SelectTableViewController in prepareForSegue
SelectTableViewController is loaded and display my list based on dataList !
I pressed addNewArea, it append a new area to "areas" that is only local to SelectTableViewController !
My list is refreshed and displays my new area
I select an area
I call the delegate by passing 2 arguments : What I selected and my updated areas(List); I pop SelectViewController => Popping SelectViewController, it disappear and all data are lost
When I called the delegate, I updated my label based on what I selected and also I updated dataList in ViewController with areas (from SelectTableViewController).
When I click on my button, see point 2.1. => You should understand
If you want to persist data, you could use different techniques such as singleton, core data, NSUserDefaults, etc.
ViewController.swift
import UIKit
import Foundation
struct AreaStruct {
var areaName : String
}
protocol ViewControllerDelegate : class{
func choseArea(areaStruct : AreaStruct, areas : [AreaStruct])
}
class ViewController: UIViewController, ViewControllerDelegate { //1. Implement the delegate here.
#IBOutlet weak var infoLabel: UILabel!
#IBOutlet weak var btnToVC: UIButton!
var dataList : [AreaStruct] = [AreaStruct]()
override func viewDidLoad() {
super.viewDidLoad()
self.setupListData()
}
func setupListData(){
for i in 0...5{
dataList.append(AreaStruct.init(areaName: "Coucou \(i)"))
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SelectTableViewController" {
let destinationViewController = segue.destinationViewController as! SelectTableViewController
destinationViewController.delegate = self //2. Here you passed itself to the destinationViewController so it can know how to call you !
destinationViewController.areas = dataList //
}
}
func choseArea(areaStruct : AreaStruct, areas : [AreaStruct]) {
self.infoLabel.text = areaStruct.areaName
dataList = areas
}
#IBAction func pushToSelectTableViewController(sender: AnyObject) {
//0. When pressed, you want to go to SelectTableViewController
self.performSegueWithIdentifier("SelectTableViewController", sender: nil)
}
}
SelectTableViewController.swift
import UIKit
class SelectTableViewController: UITableViewController {
var areas : [AreaStruct] = [AreaStruct]()
weak var delegate : ViewControllerDelegate? // <-- Delegate to send a mess. to ViewController
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addNewArea(sender: UIBarButtonItem) {
let hello_world = "Hello World" //For the example !
areas.append(AreaStruct.init(areaName: hello_world))
self.tableView.reloadData()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return areas.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
cell.textLabel?.text = areas[indexPath.row].areaName
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedArea = areas[indexPath.row]
self.delegate?.choseArea(selectedArea, areas: areas) //3. When you select, you pass the data to ViewController via the delegate
self.navigationController?.popViewControllerAnimated(true)//4. You dismiss itself
}
}

Present AlertView from UICollectionViewCell

I have a button in my UICollectionViewCell.swift: I want, based on certain parameters for it to be able to present an alert.
class CollectionViewCell: UICollectionViewCell {
var collectionView: CollectionView!
#IBAction func buttonPressed(sender: UIButton) {
doThisOrThat()
}
func doThisOrThat() {
if x == .NotDetermined {
y.doSomething()
} else if x == .Denied || x == .Restricted {
showAlert("Error", theMessage: "there's an error here")
return
}
}
func showAlert(title: String, theMessage: String) {
let alert = UIAlertController(title: title, message: theMessage, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
self.collectionView!.presentViewController(alert, animated: true, completion: nil)
}
On the self.collectionView!.presentViewController line I get a break:
fatal error: unexpectedly found nil while unwrapping an Optional value
I'm guessing that this has someting to do with how CollectionView is being used - I don't totally understand optionals yet. I know that a UICollectionCell can't .presentViewController - which is why I'm trying to get UICOllectionView to do it.
How do I make this work? I've thought of using an extension but don't know how to make UICOllectionViewCell adopt .presentViewController
Any ideas?
The collection view cell should not depend on knowledge of its parent view or view controller in order to maintain a functional separation of responsibilities that is part of a proper app architecture.
Therefore, to get the cell to adopt the behavior of showing an alert, the delegation pattern can be used. This is done by adding a protocol to the view controller that has the collection view.
#protocol CollectionViewCellDelegate: class {
func showAlert()
}
And having the view controller conform to that protocol:
class MyViewController: UIViewController, CollectionViewCellDelegate {
Also add a delegate property to the cell:
weak var delegate: CollectionViewCellDelegate?
Move the showAlert() function inside your collection view controller.
When you make a cell, assign the delegate for the cell to the collection view controller.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// Other cell code here.
cell.delegate = self
When it is time to show the alert, have the cell call
delegate.showAlert()
The alert will then be presented on the collection view controller because it has been set as the delegate of the collection view cell.
With me, it happens a little difference in present method:
self.window?.rootViewController?.present(alert, animated: true, completion: nil)
self.window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
You can use the parent controller's class. Please upvote the other post.
(Taken from How to present an AlertView from a UICollectionViewCell)

Resources