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)
Related
Why TableView doesn't shows the new string added? How to fix it?
In first VC:
#IBAction func saveButtonPressed(_ sender: UIButton) {
var textField = UITextField()
let alert = UIAlertController(title: "Save current run", message: "", preferredStyle: .alert)
let action = UIAlertAction(title: "Save", style: .default) { (action) in
RunHistoryTableViewController().runArray.append(textField.text!)
RunHistoryTableViewController().tableView.reloadData()
}
alert.addTextField { saveRunTextField in
saveRunTextField.placeholder = "Name your run"
textField = saveRunTextField
}
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
}
In second VC:
var runArray = ["BMW M3 Run 1", "BMW M3 Run 2", "Renault Megane RS"]
//MARK: TableView DataSource:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return runArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RunCell", for: indexPath)
cell.textLabel?.text = runArray[indexPath.row]
return cell
}
}
Bear in mind I'm a beginner, could use some help as none of my ideas seemed to work.
I think you have two ways to achieve this:
1- To use protocols and pass the data in this protocol
2- To use segue and pass the data in this segue
The most recommended solution in such a case is to use the protocol to get the result and to have a good performance.
Everytime you are calling RunHistoryTableViewController() you are instantiating a new VC. If you are showing only one VC at the time you can pass data between one to another using the prepareForSegue method and getting ahold of the actual instance of your destination VC in the destination argument of the method.
The code RunHistoryTableViewController() invokes the initializer for your view controller. It creates a brand new instance of the view controller that has nothing to do with any other instance.
This code:
RunHistoryTableViewController().runArray.append(textField.text!)
Creates a new instance of RunHistoryTableViewController, and appends textField.text to its runArray, and then on the next line, that new instance of RunHistoryTableViewControllergets thrown away. Same issue with the next line. You create a newRunHistoryTableViewController`, try to reference it's table view and tell it to reload. I would expect that line to crash, since the new instance's table view won't exist.
You need a way to point to the other view controller that you are going to display on the screen (or have already displayed.)
How are you navigating between view controllers? That will determine how you pass the data between them.
I am working in Swift and the function categoryPressedFunction is not being called.
Protocol:
protocol categoryPressed: class { //1 create a protocol with function that passes a string
func categoryPressedFunction(category: String)
}
View Controller 2:
//set the delegate
weak var delegate: categoryPressed?
//when cell is selected in tableview, grab the "category" which is a string and then dismiss
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let category = guideCategoryArray[indexPath.row] // gets category
delegate?.categoryPressedFunction(category: category) // sets delegate to know it was pressed
self.presentingViewController?.dismiss(animated: true, completion: nil) //dismisses
}
}
View Controller 1 (Previous)
//set class delegate
...categoryPressed
//add function that sets label to category and looks up items based off category
func categoryPressedFunction(category: String) {
print("categoryPressedFunctionPressed")
resourceArray.removeAll()
resourceLabel.text = category
getItems(item: category, { [self] in
print("got new items for \(category) and refreshed the tableview")
self.resourceTableView.reloadData()
})
}
When returning to ViewController 1, nothing happens. The tableview does not reload, nor does the label change to the category pressed. Am I missing something?
Delegates might be nil. Did you add this line in the ViewDidLoad method?
delegate = self
You might have missed assigning delegate to self while moving from VC1 to VC2.
Hope below code helps you.
//Some Navigation logic
let VC2 = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "VC2Identifier") as! VC2;
VC2.delegate = self
self.present(VC2, animated: true, completion: nil)
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.
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.
I am using a custom cell class with a button in it.
import UIKit
protocol TouchDelegateForShotsCell {
func touchedTwitterButton()
}
class ShotsCell : UITableViewCell {
let touchDelegateForShotsCell: TouchDelegateForShotsCell = MasterViewController()
#IBAction func twitterButtonPressed(sender: AnyObject) {
touchDelegateForShotsCell.touchedTwitterButton()
}
}
When the button is pressed I call a delegate function in the MasterViewController which contains the following standard code for sharing on Twitter:
func touchedTwitterButton() {
var shareToTwitter :SLComposeViewController = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
self.presentViewController(shareToTwitter, animated: true, completion: nil)
}
I receive the error :
"Attempt to present ViewController whose view is not in the window hierarchy!".
I've tried several workarounds. For example, creating a separate view controller and performing a segue from the button. That works in part, however, when I try to pass data I get another error :
"'NSInvalidArgumentException', reason: Receiver ViewController has no segue with identifier 'segueToTwitterShare'".
Which isn't true. For this I tried deleting my app and restarting xcode which worked for some people. However, the problem persists.
let touchDelegateForShotsCell: TouchDelegateForShotsCell = MasterViewController()
It's not the good delegate. When you write MasterViewController() you create a new instance of MasterViewController (so it is not in the window hierarchy) but you don't pass the intense who is executed.
I suggest you an example to better understand what is the problem and see where changes is needed.
I add an init() function into ShotsCell :
class ShotsCell : UITableViewCell {
var touchDelegateForShotsCell: TouchDelegateForShotsCell!
init(withDelegate delegate: MasterViewController){
super.init(style: UITableViewCellStyle.Default, reuseIdentifier: "cellIdentifier")
touchDelegateForShotsCell = delegate
}
#IBAction func twitterButtonPressed(sender: AnyObject) {
touchDelegateForShotsCell.touchedTwitterButton()
}
}
And pass your MasterViewController to your cell in cellForRowAtIndexPath :
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = ShotsCell(withDelegate: self)
return cell
}
Hope that helps. It works for me.