In VC#1, I have a UITableView. When I tap on a cell, I am brought to VC#2 where information about that cell is displayed.
I want to be able to press a button in VC#2 which changes the title of the cell it corresponds with in VC#1, but I am confused on how to do this?
Should I create a variable in VC#2 to save the indexPath for the cell that was tapped, and then call a function in VC#1 from VC#2 that uses that indexPath to update the cell? If I did this, wouldn't VC#1 need to be static so I know I'm modifying the right instance of VC#1? I'm using a push segue and a navigation controller to go back, so creating a new instance of VC#1 wouldn't reference the same VC im trying to modify as I believe?
Is there an easier way to do this?
You should use the delegate pattern.
VC1 should know what cell that VC2 is showing. You should have an IndexPath property in VC1 that stores what cell is VC2 currently displaying, right?
Now, create a protocol called VC2Delegate:
protocol VC2Delegate : class {
func titleDidChange(_ vc2: VC2, to title: String)
}
Now, add this property in VC2:
weak var delegate: VC2Delegate?
Now, when you think the title of the cell should change, call the delegate:
delegate?.titleDidChange(self, to: "Some Title")
That's all for VC2.
Make VC1 conform to VC2Delegate:
extension VC1: VC2Delegate {
func titleDidChange(_ vc2: VC2, to title: String) {
// set the text of the table cell here...
}
}
Now, when you are passing data to VC2 from VC1, probably in the prepareForSegue method, do
vc2.delegate = self
Learn more about delegates here.
You can pass every data you want through view controllers using delegates
First create a protocol whatever you want
protocol ViewControllerDelegate {
func getSelected(value:Int)
}
Create a variable from your ViewController you want pass the data
var delegate: ViewControllerDelegate?
On didSelectRowAt method you will do
if delegate != nil {
delegate.getSelected(value: indexPath.row)
}
self.navigationController?.popViewController(animated: true)
On ViewController that will receive data you have to do this
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? SecondViewController {
vc.delegate = self
}
}
extension YourViewController: ViewControllerDelegate {
fun getSelected(value:Int) {
// Get value from another view controller and manage it
}
}
This code is in Swift 4
If you don't understand something let me know
It's wrong approach you are pursuing. You must separate your data layer from your presentation layer. So in VC#2 you edit your visualized data, then VC#1 reloads the data to update its view.
Short answer: You should not do that at all.
View controllers should not modify other view controller's views.
You should modify the data model in VC2, then send a message back to VC1 telling it to update the cell.
(In the push segue you can set up VC1 to be VC2's delegate, then define a protocol that VC2 uses to notify VC1 about the indexPath's of the data model that need to be updated.)
Related
I'm making an expense tracking app. I have added a table view in the main view controller, and added an "add" button in the navigation controller. On clicking this, it shows a view controller in which you type in the data.
On clicking add at the end, it should save the entered data through coreData, and then be presented in the tableView, but my app crashes saying that a nil value was found, even though I have integrated the "??" safe guard."
You tableview in startingViewController is nil this is the problem. When you call the MainVC.getAllItems() your tabview is not initialized. Probably you are re creating startingViewController on your second controller to reach it getAllItems function but it is a wrong approach. You need to update previous viewController datas with protocols or notifications.
First you need to create a protocol like below
protocol AddViewControllerDelegate {
func updateTableView()
}
After that you need to define a variable in your addViewcontroller with this protocol type and call protocol's function when user adds new expense.
class AddViewController: UIViewController {
var delegate: AddViewControllerDelegate?
func callUpdateTableView() {
delegate?.updateTableView()
}
}
In your StartingViewController must conform this protocol. So you need to add updateTableView function. Also you need to say the delegate of your second class is your first class in where you show your addViewController.
class StartingViewController: UIViewController, AddViewControllerDelegate {
func goToAddViewController() {
let vc = AddViewController()
vc.delegate = self
show(vc, sender: nil)
}
func updateTableView() {
// Reload Tableview
}
}
So basically, when you call the protocol function from your secondViewController, your firstViewController's updateTableView function called and you can reload your tableview in this function.
I have a pretty complicated setup in terms of view controllers. I have reasons for it that are kind of out of the scope of this question. So I have 3 view controllers.
ViewControllerA is the main view controller in this case. ViewControllerB is a container view controller that is displayed from ViewControllerA. ViewControllerB has a button that has a segue to display ViewControllerC. Then in ViewControllerC there is a button to dismiss to go back.
ViewController's A and B can be different. Depending on if the user is editing an object or creating a new object. The things I'm talking about remain constient between those two cases.
Basically my goal is when a user dismisses ViewControllerC it changes a button text on ViewControllerB. Depending on the users actions on ViewControllerC.
I was thinking about using self.presentingViewController somehow or something along those lines but I can't figure out how to access that specific button within ViewControllerB.
Any ideas of how I can achieve this?
I suggest you use a protocol to define a common method to update button text. Both ViewControllerB's can then conform to this protocol. Then use a delegate callback approach to call these methods from your ViewControllerC.
When you present ViewControllerC from ViewControllerB you can set the delegate property to self before presenting it. You would do this in different places depending on how you are presenting ViewControllerC. As you said you're using a segue to do it, then you should do this in the prepareForSegue method.
Declare a protocol that defines a method to update the button's text like this:
protocol ChangeableButtonTextViewController {
func updateButtonText(newText: String)
}
Then make your EditViewControllerB and CreateViewControllerB conform to this protocol to update the button text:
class EditViewControllerB: UIViewController, ChangeableButtonTextViewController {
func updateButtonText(newText: String) {
button.text = newText
}
// Other stuff in your ViewController
}
Add a delegate property to ViewControllerC like this:
var delegate: ChangeableButtonTextViewController?
Add a prepareForSegue method to EditViewControllerB and CreateViewControllerB which would look something like:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
segue.destination as! ViewControllerC).delegate = self
}
You can then do something like this in ViewControllerC:
func dismiss() {
delegate.updateButtonText("NewText")
}
Let me know if you need any further clarifications.
I have built an app that centers around a pretty tableview that I've built.
I have setup a secondary view controller as a menu that is presented modally, and I would like to filter the tableview by selecting one of the buttons on the secondary view controller.
For example, each cell has a City assigned to it. In the menu, I'd like to be able to click a city and filter the tableview to only show cells with that city.
I have too much code to paste, and I'm confident I can solve this problem with a smidge of direction.
Thanks for your help!
You can do this with an unwind segue from your second view controller's buttons back to your table view controller.
In your table view controller, say,
func unwindToTableView(_ segue: UIStoryboardSegue) {
switch segue.identifier {
case "FilterNames":
filterByName()
etc…
}
}
or you could have different unwind funcs for each filter…
func unwindAndFilterName(_ segue: UIStoryboardSegue) {
filterByName()
}
etc
To hook up an unwind segue, just add the method to your table view controller, then in your storyboard, drag from the button on the second view controller to it's Exit icon. The segue func should appear in the list
To do this you would like to have separate DataSource layer in the application to have move clear code.I will write small example for you how it is possible to implement.
For example you have class DataSource. With information that you are showing. In my case it is cities. Now when I would like to do sorting I will call sortAlphabetically() and reload tableview. It is quite simple way and you're solution really depends on how you are working with UITableView.
class DataSource {
var cities = ["Lviv", "Lutsk", "Kiev", "Rivne"]
func sortAlphabetically() {
cities = cities.sorted { $0 < $1 }
//reload tableview hear
}
}
Best way to do so is to use delegates, add a protocol to your filter view controller and a delegate function in tableView that filters datasource for tableView. Don't forget to assign your table view controller as the delegate before you segue to the filter viewcontroller
Best way to do so is to use delegates, add a protocol to your filter view controller and a delegate function in tableView that filters datasource for tableView. Don't forget to assign your table view controller as the delegate before you segue to the filter viewcontroller
Before your filterViewController
protocol FilterViewControllerDelegate {
func tableViewCriteria(criteria: AnyObject)
}
In your filterViewController:
var delegate: FilterViewControllerDelegate?
In the class declaration of tableViewController, add FilterViewControllerDelegate
class MyTableViewController: UITableViewController, FilterViewControllerDelegate{
Don't forget to set the FilterViewControllerDelegate to self before you segue to the filterView:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showFilter" {
if let fvc = segue.destination as? FilterViewController{
fvc.delegate = self
}
}
}
Implement the func in tableView that will update tableView data source based on the chosen criteria:
//In myTableViewController
func tableViewCriteria(criteria: AnyObject) {
//update tableView data source base on criteria here
return
}
Finally, call the delegate function from filterView whenever you need to return to tableview:
self.delegate?.tableViewCriteria(criteria: foo)
Voila! :)
I have a searchViewController where I search for users and UITableView gets updated dynamically with user information. The cell for the UITableView is custom - it has a UIImage, the usernameLabel, and a button called "Add".
What I want is that if the user clicks on the add button of the cell, it should pass the user information on that cell (image and username) to another view controller that has a UITableView that is a friend list.
However, so far the only way I know is by using performSegue to pass the data on to the other viewController holding the friendlist UITable. But by this method, every time I click the add button it segues to the other view controller which I don't want. I want it to stay on the searchViewController when the add button is clicked - I only want the data to be passed.
Is there any way I can do this? Is using NSUserDefaults advisable for passing data of this sort?
For simplicity I will use FriendListVC and AddVC
If you are going to your AddVC from FriendListVC via a bar button item or something and your stack looks like:-
FriendListVC -> AddVC
There are two approaches you can use:-
1) Create a delegate of your friendListVC in your addVC and modify the friendListVC datasource on any changes there
2) Or, and I recommend this approach, just reload your FriendListVC datasource on it's viewWillAppear. viewWillAppear will get called even if you navigate back. Thus even if you add a deleteVC in the future and navigate back, the viewWillAppear will perform the updates and it will be independent of any other VC
Hope that helps
Use delegate for passing data between view controllers. you can find this useful
Passing data between 2 UIViewController using delegate and protocol
you can use NSUserDefaults but delegate pattern is better than this.
You can use callback method best and easy way to pass data one controller to another
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
let viewControllerB = segue.destinationViewController as! ViewControllerB
viewControllerB.callback = { message in
//Do what you want in here!
}
}
In ViewControllerB:
var callback : (String -> Void)?
#IBAction func search(sender: AnyObject) {
callback?("Pass data to view controller1")
self.dismissViewControllerAnimated(true, completion: nil)
}
The easiest way to do this is by making an instance of the view controller that you want to pass data to, in the current view controller. I will write you a sample code for this.
class yourTableViewController: UITableViewController {
var controllerToPassData: UIViewController()
func clickTableButton(sender: UIButton) {
controllerToPassData.count += 1
}
}
class controllerwhereDataisPassed: UIViewController {
var count: Int!
}
Pick the instance of the controller where you want to pass data to from the navigationController stack and use this code.
I've been looking into how delegation works. You define a protocol in controller A, create a delegate variable, and call the function through the delegate. Then, in controller B, you conform to the protocol, implement methods, and then use prepareForSegue to tell controller A that controller B is the delegate.
But this involves A -> B -> A. I need to know how to do A -> B. I've been trying to do this through the following code:
Declare the protocol in controller A
protocol CellDataDelegate {
func userDidTapCell(data: String)
}
Create a delegate variable in A
var cellDelegate: CellDataDelegate? = nil
Call the function in the delegate in A when cell tapped
if cellDelegate != nil {
let cellKey = keys[indexPath.row].cellKey
cellDelegate?.userDidTapCell(data: cellKey)
self.performSegue(withIdentifier: "showDetails", sender: self)
}
Add the delegate to controller B and conform to the method
class DetailsVC: UIViewController, CellDataDelegate
The function:
func userDidTapCell(data: String) {
useData(cellKey: data)
}
The problem here is the last part of the delegation process. I can't use prepareForSegue to do the controllerA.delegate = self part because I don't want to go back to controller A, I need to stay in controller B. So how do I tell controller A that B is the delegate?
Protocol Delegates are usually used to pass data to a previous UIViewController than the present one in the navigation stack(in case of popViewController) because the UIViewController to which the data is to be sent needs to be present in the memory. In your case you havn't initialised UIViewController B in memory for the method of protocol delegate to execute.
There are simple ways to send data to the next UIViewControllers in the navigation stack.
Your UIViewController B should have a receiving variable to store data sent from the UIViewController A
class DestinationVC : UIViewController
{
receivingVariable = AnyObject? // can be of any data type depending on the data
}
Method 1: Using Storyboard ID
let destinationVC = self.storyboard.instantiateViewControllerWithIdentifier("DestinationVC") as DestinationVC
destinationVC.receivingVariable = dataInFirstViewControllerToBePassed
self.navigationController.pushViewController(destinationVC , animated: true)
Method 2: Using prepareForSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!)
{
let destinationVC = segue.destinationViewController as DestinationVC
destinationVC.receivingVariable = dataInFirstViewControllerToBePassed
}
Multiple segues from UIViewController A to any other UIViewController will cause in execution of prepareForSegue every single time and might crash the application as other classes of UIViewControllers would have no such parameters as receivingVariable which is present in UIViewController B.
This can be easily countered; use of multiple segues can be done simply using if else or switch modules on segue.identifier which is a parameter of segue.
Note: UILabel, UIButton and another other UI element's attribute cannot be assigned in this manner because these element load in the memory in the func loadView() of UIViewController lifecycle as they are not set to initialise when you initialise the class of UIViewController B as mentioned above.
I don't think you need to use delegate pattern here. If you are trying to achieve this. You have some cells on view controller A and now you want to display details of cell(on click) in view controller B. You can declare cell key as the property in view controller B.
class B: UIViewController {
let cellKey: String!
}
And set the above key in prepare for segue method
if (segue.identifier == "segueToViewControllerB") {
let vc = segue.destinationViewController as B
vc.cellKey= "1"
}
I think you are misunderstanding the point of the question you referenced. The question above explained the what is happening in a lot of detail, but here is a short answer, for those who are lazy: do NOT you prepareForSegue to pass information bottom to top (i.e. from child view controller to parent), but most certainly DO use it to pass top to bottom.