Calling a function in a view controller from another view controller - swift

Here is the code with the delegate process suggested...
in main view controller...
protocol FilterDelegate: class {
func onRedFilter()
func onGreenFilter()
func onBlueFilter()
func onUnfiltered()
}
class ViewController: UIViewController, FilterDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
----
// Increase red color level on image by one.
func onRedFilter() {
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "filterSegue" {
let dest = segue.destinationViewController as! CollectionViewController
dest.filterDelegate = self
}
}
in collection view controller...
var filterDelegate: FilterDelegate?
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
print("Cell \(indexPath.row) selected")
guard let filterDelegate = filterDelegate else {
print("Filter delegate wasn't set!")
return
}
switch indexPath.row {
case 0:
filterDelegate.onRedFilter()
case 1:
filterDelegate.onGreenFilter()
case 2:
filterDelegate.onBlueFilter()
case 3:
filterDelegate.onUnfiltered()
default:
print("No available filter.")
}
Right now...the code stops at the guard block and prints the error message. The switch block is not executed on any press of a cell.

Your theory in your second last sentence is correct - when you call storyboard.instantiateViewControllerWithIdentifier in the "child" view controller, you are actually creating an entirely new instance of your main view controller. You are not getting a reference to the existing main view controller, which is why the methods you're calling are not having any effect.
There are several ways to achieve what you're trying to do, including the delegate pattern or using closures. Here's a sketch of what it could look like using a delegate protocol:
protocol FilterDelegate: class {
func onRedFilter()
func onGreenFilter()
func onBlueFilter()
func onUnfiltered()
}
class MainViewController: UIViewController, FilterDelegate {
// implement these as required
func onRedFilter() { }
func onGreenFilter() { }
func onBlueFilter() { }
func onUnfiltered() { }
// when we segue to the child view controller, we need to give it a reference
// to the *existing* main view controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dest = segue.destination as? ChildViewController {
dest.filterDelegate = self
}
}
}
class ChildViewController: UIViewController {
var filterDelegate: FilterDelegate?
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// ...
guard let filterDelegate = filterDelegate else {
print("Filter delegate wasn't set!")
return
}
switch indexPath.row {
case 0:
filterDelegate.onRedFilter()
case 1:
filterDelegate.onGreenFilter()
case 2:
filterDelegate.onBlueFilter()
case 3:
filterDelegate.onUnfiltered()
default:
print("No available filter.")
}
}
}
Another option would be to provide closures on ChildViewController for every function on MainViewController that the child needs to call, and set them in prepareForSegue. Using a delegate seems a bit cleaner though since there are a bunch of functions in this case.

Related

Delegate not get called

protocol WeatherManagerDelegate {
func didUpdateWeater(weather: ConsolidatedWeather)
func didFailWithError(error: Error)
}
ViewController: where i am setting value didSelectRowAt and using performSegue going to another viewController
class WeatherListViewController: UIViewController {
var delegate: WeatherManagerDelegate?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let index = weatherViewModel.didSelect(at: indexPath.row)
self.delegate?.didUpdateWeater(weather: index)
performSegue(withIdentifier: K.DetailsView.segueIndentifier, sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVc = segue.destination as! DetailsViewController
}
}
this is my ViewModel calss: from my ViewModel, I will send value to ViewController and Update UI
class DetailsWeatherViewModel{
var w = WeatherListViewController()
func a(){
print("aaa")
w.delegate = self
}
}
extension DetailsWeatherViewModel: WeatherManagerDelegate{
func didUpdateWeater(weather: ConsolidatedWeather) {
weatherData = weather
print("weatherData: \(String(describing: weatherData))")
}
func didFailWithError(error: Error) {
print(error)
}
}
what I am doing wrong...????
You should be careful of memory leaks when using delegate pattern. I think you can solve this problem by making protocol limit to class and declare property by weak var. Although WeatherListViewController disappeared, WeatherListViewController and DetailsWeatherViewModel are not likely to be deinit unless you use weak reference. Try this.
protocol WeatherManagerDelegate : class {
func didUpdateWeater(weather: ConsolidatedWeather)
func didFailWithError(error: Error)
}
weak var delegate: WeatherManagerDelegate?
If you are following MVVM architecture then you can create a viewModel object inside your viewcontroller and then use the updated values in VM directly using VM object.
Else if you want to use delegate then you need to write the protocols in viewModel and use it in VC. You shouldn't be creating Viewcontroller object inside the Viewmodel.

My protocol is not working, what is wrong with it?

I have four view controllers and I am trying to send data between controllers via protocols although my two other protocols is working fine the last one is not working and I could not figure it out.
This is my first view controller that should send the data:
import UIKit
class ExercisesTableViewController: UITableViewController, ExerciseProtocol{
var exerciseToSend: Exercise? {
didSet{
print(exerciseToSend!) // This prints the result.
performSegue(withIdentifier: "showDetail", sender: self)
}
}
func getExercise() -> Exercise {
return exerciseToSend!
}
.
.
.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
exerciseToSend = exercises[indexPath.row]
}
.
.
.
}
This is the controller that should receieve the data
import UIKit
protocol ExerciseProtocol {
func getExercise() -> Exercise
}
class ExerciseDetailViewController: UIViewController {
var delegate:ExerciseProtocol?
override func viewDidLoad() {
print(delegate?.getExercise()) // This doesn't print the result.
super.viewDidLoad()
}
}
Add this inside ExercisesTableViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let des = segue.destination as? ExerciseDetailViewController else {return}
des.delegate = self
}

Sort cells inside a tableview alphabetically from a different viewcontroller

I have two scenes. Scene A is a tableview consisting of a list of fruits. Scene B has a segmented controller with one of the options to sort the fruit alphabetically.
scene A:
#IBOutlet weak var tableView: UITableView!
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showFilter" {
_ = segue.destinationViewController as! FilterViewController
}
}
?func alpheticalOrder(sender: AnyObject) {
fruits.sortInPlace({$0.name < $1.name }) } ?
scene B:
func tableView(tableView:UITableView, cellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier("FilterCell") as! FilterCell
cell.segmentedController.addTarget(self, action: "segmentedControllerActionChanged:", forControlEvents: .ValueChanged)
return cell
}
#IBAction func segmentedControllerActionChanged(sender: AnyObject) {
if sender.selectedSegmentIndex == 0 {
fruits.sortInPlace({$0.name < $1.name })
}
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: "close:")
greyView.addGestureRecognizer(tap)
}
func close(tap: UITapGestureRecognizer) {
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
It's the ViewController().tableView.reloadData() line that is incorrect here, although I put it in to help understand what I'm trying to achieve. Somehow I need to reload the tableview data to sort cells alphabetically by fruit name after exiting scene B to return to scene A.
Consider the line of code:
ViewController().tableView.reloadData()
That does not tell the existing ViewController to reload. The ViewController() creates a new instance of that view controller (which not connected to any storyboard scene; has no data; and will be immediately deallocated), and tells it to reload itself.
You need to have B refer to the existing A, not create a new one. Also, you presumably don't want to sort fruits in B, but rather back in A. Thus:
add a property in B that points back to A:
var sourceViewController: ViewControllerA!
in the prepareForSegue of A, you have to set this property in B, e.g.:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showFilter" {
let filterViewController = segue.destinationViewController as! FilterViewController
filterViewController.sourceViewController = self
}
}
implement method in A that sorts the data and reloads the table:
func sortFruitsAndReload() {
fruits.sortInPlace {$0.name < $1.name }
tableView.reloadData()
}
the segmentedControllerActionChanged in B should call that method in A:
#IBAction func segmentedControllerActionChanged(sender: AnyObject) {
if sender.selectedSegmentIndex == 0 {
sourceViewController.sortFruitsAndReload()
}
}
Or, even better, use a protocol to keep these two classes "weakly coupled" (i.e. opens the door to use this filter/sorting view controller in other situations):
add a protocol in B:
protocol FilterViewDelegate {
func sortAndReload()
}
add a property in B to maintain reference to this delegate:
var delegate: FilterViewDelegate?
specify the A will conform to this protocol:
class ViewControllerA: UIViewController, FilterViewDelegate { ... }
clearly, keep the name of A's view controller class and base class to whatever you are using now, but just add FilterViewDelegate to the class declaration;
implement method in A that sorts the data and reloads the table in order to satisfy the requirements of conforming to this protocol:
func sortAndReload() {
fruits.sortInPlace {$0.name < $1.name }
tableView.reloadData()
}
in the prepareForSegue of A, you set this delegate property in B, e.g.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showFilter" {
let filterViewController = segue.destinationViewController as! FilterViewController
filterViewController.delegate = self
}
}
the segmentedControllerActionChanged in B should call that method in A.
#IBAction func segmentedControllerActionChanged(sender: AnyObject) {
if sender.selectedSegmentIndex == 0 {
delegate?.sortAndReload()
}
}

Wait for user to dismiss Modal View before executing code (Swift 2.0)

I'm building an app that asks users to select a location if they don't allow access to their current location using a Modal that Presents Modally as soon as the user clicks 'Deny'. This modal has information displayed as a TableView, and the modal dismisses as soon as the user selects a row. I save this selection in a variable called selectedStop. I want the app to pause until the user selects a location, then as soon as the user selects a location, the app continues and the setUpMap() function executes. I've tried using an infinite while loop in setUpMap() and using a boolean to break out of it as soon as a user selects a row, but the while loop executes before the Modal even pops up.
ViewController.swift
class ViewController: UIViewController {
var selectedStop: Int!
override func viewDidLoad() {
super.viewDidLoad()
// If we don't have access to the user's current location, request for it
if (CLLocationManager.authorizationStatus() != CLAuthorizationStatus.AuthorizedWhenInUse) {
locationManager.requestWhenInUseAuthorization()
}
}
func setUpMap() {
// do stuff with var selectedStop
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
switch status {
case .Denied:
// if user denies access, display modal
self.performSegueWithIdentifier("NotifyModally", sender: self)
setUpMap() // need this func to execute AFTER location is selected
break
case .AuthorizedWhenInUse:
setUpMap()
break
default:
break
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "NotifyModally") {
let destViewController:ModalViewController = segue.destinationViewController as! ModalViewController
// send selectedStop var to ModalViewController
destViewController.selectedStop = selectedStop
}
}
}
ModalViewController.swift
class ModalViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var busStops = ["Stop 1", "Stop 2", "Stop 3"]
var selectedStop: Int!
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return busStops.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel!.text = busStops[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedStop = indexPath.row
dismissViewControllerAnimated(true, completion: nil)
}
}
Using a Int variable to pass information will not working since it's a value type which will get copied every time you pass it around. So that means when you change the selectedStop in the didSelectRowAtIndexPath method, the original selectedStop inside ViewController will still be nil or whatever it was.
And then, to answer your question. There are several ways to solve this.
You can either pass a block (instead an int) to the ModalViewController like this:
var stopSelectedHandler: (Int) -> Void = { selectedStop in
// Do something here.
// setUpMap()
}
You'll call this block inside the completion handler of dismissViewControllerAnimated.
You can use notification.
// Do this inside `ViewController`.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "setupMap:", name: "UserDidSelectStop", object: nil)
// And then post the notification inside `didSelectRowAtIndexPath`
NSNotificationCenter.defaultCenter().postNotificationName("UserDidSelectStop", object: nil, userInfo: ["selectedStop": 2])
// Change your setupMap to this
func setupMap(notification: NSNotification) {
guard let selectedStop = notification.userInfo?["selectedStop"] as? Int else { return }
// Now you can use selectedStop.
}
You can also use KVO, delegate, etc. Use whatever suits you.
Put the block like this:
class ViewController: UIViewController {
var stopSelectedHandler: (Int) -> Void = { selectedStop in
// Do something here.
// setUpMap()
}
....
}

How do you access a UIViewController function from within a UICollectionCell?

I have a function within a UICollectionViewCell that requires access to the
hosting UIViewController. Currently 'makeContribution()' can't be accessed:
What is the proper way of accessing the host UIViewController that has the desired function?
Thanks to the insightful responses, here's the solution via delegation:
...
...
...
{makeContribution}
This is a mildly controversial question - the answer depends a little on your philosophy about MVC. Three (of possibly many) options would be:
Move the #IBAction to the view controller. Problem solved, but it might not be possible in your case.
Create a delegate. This would allow the coupling to be loose - you could create a ContributionDelegate protocol with the makeContribution() method, make your view controller conform to it, and then assign the view controller as a weak var contributionDelegate: ContributionDelegate? in your cell class. Then you just call:
contributionDelegate?.makeContribution()
Run up the NSResponder chain. This answer has a Swift extension on UIView that finds the first parent view controller, so you could use that:
extension UIView {
func parentViewController() -> UIViewController? {
var parentResponder: UIResponder? = self
while true {
if parentResponder == nil {
return nil
}
parentResponder = parentResponder!.nextResponder()
if parentResponder is UIViewController {
return (parentResponder as UIViewController)
}
}
}
}
// in your code:
if let parentVC = parentViewController() as? MyViewController {
parentVC.makeContribution()
}
Well, CollectionView or TableView?
Anyway, Set your ViewController as a delegate of the cell. like this:
#objc protocol ContributeCellDelegate {
func contributeCellWantsMakeContribution(cell:ContributeCell)
}
class ContributeCell: UICollectionViewCell {
// ...
weak var delegate:ContributeCellDelegate?
#IBAction func contributeAction(sender:UISegmentedControl) {
let isContribute = (sender.selectedSegmentIndex == 1)
if isContribute {
self.delegate?.contributeCellWantsMakeContribution(self)
}
else {
}
}
}
class ViewController: UIViewController, UICollectionViewDataSource, ContributeCellDelegate {
// ...
func collectionView(_ collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
cell = ...
if cell = cell as? ContributeTableViewCell {
cell.delegate = self
}
return cell
}
// MARK: ContributeCellDelegate methods
func contributeCellWantsMakeContribution(cell:ContributeCell) {
// do your work.
}
}