Delegate won't work using three view controllers - ios

Is use delegate to pass data between three view controllers. I set delegate for both view controllers I want to pass data from to self inside of MapViewController - MapViewController shall receive data from both VCs.
I also added weak var delegate: MapViewController? to both VCs - but somehow it's only working for one of them.
MapViewController:
var newStartItem: MKMapItem?
override func viewDidLoad() {
super.viewDidLoad()
searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)
searchVC.delegate = self
newVC = (storyboard?.instantiateViewController(withIdentifier: "newLocation") as! NewLocationTableViewController)
newVC.delegate = self
...
}
func addStartAnnotationToMap() {
guard let item = newStartItem else { return }
guard let coordinates = item.placemark.location?.coordinate else { return }
addPin(title: item.name!, subtitle: "", coordinates: coordinates)
}
}
NewLocationViewController: (this one is not working)
class NewLocationTableViewController: UIViewController {
weak var delegate: MapViewController?
...
func passData() {
guard let mapItem = starts?.first else { return }
delegate?.newStartItem = mapItem
delegate?.addStartAnnotationToMap()
}
Xcode won't return any errors either!

You should declare a protocol:
protocol MapViewControllerDelegate: class {
func addStartAnnotationToMap()
}
Then change your delegate variable to:
weak var delegate: MapViewControllerDelegate?
And make MapViewController conform to MapViewControllerDelegate protocol like this:
class MapViewController: UIViewController, MapViewControllerDelegate {

Your delegate must be a reference from the type of your protocol
In this case: weak var delegate: MapViewControllerDelegate?

For this
newVC = (storyboard?.instantiateViewController(withIdentifier: "newLocation") as! NewLocationTableViewController)
newVC.delegate = self
and
weak var delegate: MapViewController?
to work you need to verify that at some point inside the map vc you present that location vc like
self.present(newVC,animated:true,completion:nil)
or push it , plus print it to verify it's not nil and guard doesn't return if starts is empty
func passData() {
print("delegate \(delegate) item \(starts?.first)")
guard let mapItem = starts?.first else { return }
delegate?.newStartItem = mapItem
delegate?.addStartAnnotationToMap()
}

Related

Value pass from delegate method is nil Swift

I try to send data from one view controller (ItenaryVC) through delegate to another view controller (ItenaryFloatingPanelVC). The VC that initiates ItenaryVC is DiscoverVC. When ItenaryVC load up it will make API calls to get some data, and I want to pass the data received from API calls to the delegate that I created which will pass the data to ItenaryFloatingPanelVC. But the data is not received and it is nil. I also actually using FloatingPanel Library to add the panel in ItenaryVC.
Flow
DiscoverVC -> ItenaryVC + ItenaryFloatingPanelVC (as Flaoting Panel)
DiscoverVC.swift
protocol DiscoverVCDelegate : AnyObject {
func didSendSingleLocationData(_ discoverVC : DiscoverVC , location : Location)
}
class DiscoverVC : UIViewController {
//MARK:- IBOutlets
#IBOutlet weak var collectionView: UICollectionView!
weak var delegate : DiscoverVCDelegate?
private var locationResult = [Location]()
private var selectedAtRow : Int!
//MARK:- Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
renderView()
getLocations()
}
private func renderView() {
collectionView.register(UINib(nibName: R.nib.discoverCell.name, bundle: nil), forCellWithReuseIdentifier: R.reuseIdentifier.discoverCell.identifier)
collectionView.delegate = self
collectionView.dataSource = self
}
private func getLocations(location : String = "locations") {
NetworkManager.shared.getLocations(for: location) { [weak self] location in
switch location {
case .success(let locations):
self?.updateDiscoverUI(with: locations)
case .failure(let error):
print(error.rawValue)
}
}
}
private func updateDiscoverUI(with locations : [Location]) {
DispatchQueue.main.async { [weak self] in
self?.locationResult.append(contentsOf: locations)
self?.collectionView.reloadData()
}
}
}
//MARK:- Delegate
extension DiscoverVC : UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedAtRow = indexPath.row
self.performSegue(withIdentifier: R.segue.discoverVC.goToDetails, sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destinationVC = segue.destination as? ItenaryVC else { return}
destinationVC.locationDetails = locationResult[selectedAtRow]
destinationVC.imageURL = locationResult[selectedAtRow].image
destinationVC.getItenaries(at: locationResult[selectedAtRow].itenaryName)
delegate?.didSendSingleLocationData(self, location: locationResult[selectedAtRow])
// Remove tab bar when push to other vc
destinationVC.hidesBottomBarWhenPushed = true
}
}
ItenaryVC.swift
import UIKit
import FloatingPanel
protocol ItenaryVCDelegate : AnyObject {
func didSendItenaryData(_ itenaryVC : ItenaryVC, with itenary : [[Days]])
}
class ItenaryVC: UIViewController {
#IBOutlet weak var backgroundImage: UIImageView!
var imageURL : URL!
var locationDetails: Location! {
didSet {
getItenaries(at: locationDetails.itenaryName)
}
}
weak var delegate : ItenaryVCDelegate?
var itenaries = [[Days]]()
//MARK:- Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
setupCard()
setupView()
}
func getItenaries(at itenaries : String = "Melaka"){
NetworkManager.shared.getItenaries(for: itenaries) { [weak self] itenary in
switch itenary {
case .success(let itenary):
// print(itenary)
DispatchQueue.main.async {
self?.itenaries.append(contentsOf: itenary)
self?.delegate?.didSendItenaryData(self! , with: itenary)
}
print(itenaries.count)
case .failure(let error):
print(error.rawValue)
}
}
}
}
//MARK:- Private methods
extension ItenaryVC {
private func setupView() {
backgroundImage.downloaded(from: imageURL)
backgroundImage.contentMode = .scaleAspectFill
}
private func setupCard() {
guard let itenaryFlotingPanelVC = storyboard?.instantiateViewController(identifier: "itenaryPanel") as? ItenaryFloatingPanelVC else { return}
let fpc = FloatingPanelController()
fpc.set(contentViewController: itenaryFlotingPanelVC)
fpc.addPanel(toParent: self)
}
}
ItenaryFloatingPanelVC.swift
import UIKit
class ItenaryFloatingPanelVC: UIViewController{
//MARK:- Outlets
#IBOutlet weak var sloganLabel: UILabel!
#IBOutlet weak var locationLabel: UILabel!
#IBOutlet weak var locDesc: UITextView!
#IBOutlet weak var itenaryTableView: UITableView!
#IBOutlet weak var locDescHC: NSLayoutConstraint!
var itenaries = [[Days]]()
let itenaryVC = ItenaryVC()
let discoverVC = DiscoverVC()
override func viewDidLoad() {
discoverVC.delegate = self
itenaryVC.delegate = self
itenaryTableView.dataSource = self
itenaryTableView.delegate = self
locDescHC.constant = locDesc.contentSize.height
itenaryTableView.register(UINib(nibName: R.nib.itenaryCell.name, bundle: nil), forCellReuseIdentifier: R.nib.itenaryCell.identifier)
}
}
//MARK:- DiscoverVCDelegate
extension ItenaryFloatingPanelVC : DiscoverVCDelegate {
func didSendSingleLocationData(_ discoverVC: DiscoverVC, location: Location) {
print(location)
locationLabel.text = location.locationName
}
}
//MARK:- ItenaryVC Delegate
extension ItenaryFloatingPanelVC : ItenaryVCDelegate {
func didSendItenaryData(_ itenaryVC: ItenaryVC, with itenary: [[Days]]) {
DispatchQueue.main.async {
print(itenary)
self.itenaries.append(contentsOf: itenary)
self.itenaryTableView.reloadData()
print("itenary \(self.itenaries.count)")
}
}
}
Image of ItenaryVC and ItenaryFloatingPanel
Your code doesn't work because you have set the delegate of an newly created ItenaryVC instance:
let itenaryVC = ItenaryVC()
// ...
itenaryVC.delegate = self
itenaryVC here is not the VC that presented self, the ItenaryFloatingPanelVC. It's a brand new one that you created.
Instead of doing that, you can set the delegate in ItenaryVC when you are about to present ItenaryFloatingPanelVC:
private func setupCard() {
guard let itenaryFlotingPanelVC = storyboard?.instantiateViewController(identifier: "itenaryPanel") as? ItenaryFloatingPanelVC else { return }
let fpc = FloatingPanelController()
// here!
self.delegate = itenaryFlotingPanelVC
fpc.set(contentViewController: itenaryFlotingPanelVC)
fpc.addPanel(toParent: self)
}
self is now the ItenaryVC, and itenaryFlotingPanelVC is the VC that self is presenting.
I don't see you call getItenaries() in ItenaryVC. Please check again.

Swift: using delegates to send data to another view controller

How do I use delegates to send data to another view controller and then display it in the collection view? My problem is with moving the array across using delegates.
Below is an example of what I am working on.
When I use usersList in the ThirdViewController, I get an error that says 'Unexpectedly found nil while implicitly unwrapping an Optional value'
protocol ExampleDelegate {
func delegateFunction(usersArray: Array<User>)
}
class ViewController: UIViewController {
private var model: Users = ViewController.createAccount()
var exampleDelegate: ExampleDelegate?
#IBAction func ShowUsers(_ sender: UIButton) {
let ShowUsersVC = storyboard?.instantiateViewController(identifier: "ThirdViewController") as! ThirdViewController
var userList: Array<User> = model.listOfUsers
exampleDelegate?.delegateFunction(usersArray: userList )
present(ShowUsersVC, animated: true)
}
}
class ThirdViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var usersList: Array<User>!
override func viewDidLoad() {
super.viewDidLoad()
let GetUsersVC = storyboard?.instantiateViewController(identifier: "ViewController") as! ViewController
GetUsersVC.showMomentsDelegate = self
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ThirdViewController: ExampleDelegate {
func delegateFunction(usersArray: Array<User>)
usersList = usersArray
}
You don't need delegates in this case. You are sending data forwards, so just do it like this:
class ViewController: UIViewController {
private var model: Users = ViewController.createAccount()
var exampleDelegate: ExampleDelegate?
#IBAction func showUsers(_ sender: UIButton) {
let showUsersVC = storyboard?.instantiateViewController(identifier: "ThirdViewController") as! ThirdViewController
var userList: Array<User> = model.listOfUsers
showUsersVC.usersList = userList /// pass the data!
present(showUsersVC, animated: true)
}
}
Also in Swift you should lowercase objects like userList, as well as functions like showUsers.

Changing Label text on main controller after modal closed swift macOS

I am using delegates to get a string value from my modal. When the modal closes I am trying to update Label text using that string. However, I am getting error: Unexpectedly found nil while implicitly unwrapping an Optional value: file. I am not sure how to fix this. I think it's happening because the view is not yet active.
import Cocoa
class ViewControllerA: NSViewController, SomeDelegate {
#IBOutlet weak var msgLabel: NSTextField!
var s: String = "";
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func setDetails(s: String) {
self.user = s;
print("Notified", self.s) // <-- prints: Notified hello again
msgLabel.stringValue = self.s <-- DOESN'T WORK
}
func showModal() -> Void {
msgLabel.stringValue = "hello" // <--- WORKS
let cbvc: NSViewController = {
return self.storyboard!.instantiateController(withIdentifier: "ControllerBVC")
as! NSViewController
}()
self.presentAsModalWindow(cbvc);
}
#IBAction func onBtn(_ sender: Any) {
self.showModal();
}
}
protocol SomeDelegate {
func setDetails(s: String)
}
class ViewControllerB: NSViewController {
#IBOutlet weak var textF: NSTextField!
var delegate: SomeDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
let vc = ViewControllerA()
self.delegate = vc
}
#IBAction func onBtn(_ sender: Any) {
DispatchQueue.main.async {
self.delegate?.setDetails(s: self.textF.stringValue)
self.dismiss("ControllerAVC")
}
}
}
You have a number of problems.
In ViewControllerB.viewDidLoad you are assigning a new instance of ViewControllerA to the delegate property. Don't do that. Your viewDidLoad method should look like this:
override func viewDidLoad() {
super.viewDidLoad()
}
In the showModal method ViewControllerA should assign itself as the delegate on ViewControllerB before ViewControllerB it is presented.
func showModal() -> Void {
let cbvc: NSViewController = {
let vc = self.storyboard!.instantiateController(withIdentifier: "ControllerBVC")
as! ViewControllerB
vc.delegate = self
return vc
}()
self.presentAsModalWindow(cbvc);
}
In the setDetails method just assign the string to your text field directly:
func setDetails(s: String) {
msgLabel.stringValue = s
}

MapKit map returns nil inside of function being called inside of different ViewController

I'm currently trying to implement a Map connected with a search function. For the overlay containing the table view, I've decided to go for a library called FloatingPanel.
I have to ViewControllers, namely MapViewController and SearchTableViewController - as the name already says, MapViewController contains a mapView. I assume since FloatingPanel adds SearchTableViewController (STVC) to MapViewController (MVC), that STVC is MVC's child.
Now whenever I want to call the MapViewController's function to add annotation inside of SearchTableViewController, MapViewController's mapView returns nil - calling it inside of MapViewController works fine.
class MapViewController: UIViewController, FloatingPanelControllerDelegate, UISearchBarDelegate {
var fpc: FloatingPanelController!
var searchVC = SearchResultTableViewController()
let locationManager = CLLocationManager()
let regionInMeters: Double = 10000
#IBOutlet private var mapView: MKMapView!
var mapItems: [MKMapItem]?
override func viewDidLoad() {
super.viewDidLoad()
checkLocationServices()
fpc = FloatingPanelController()
fpc.delegate = self
fpc.surfaceView.backgroundColor = .clear
fpc.surfaceView.cornerRadius = 9.0
fpc.surfaceView.shadowHidden = false
searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)
fpc.set(contentViewController: searchVC)
fpc.track(scrollView: searchVC.tableView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fpc.addPanel(toParent: self, animated: true)
fpc.move(to: .tip, animated: true)
searchVC.searchController.searchBar.delegate = self
}
func checkLocationServices() {
if CLLocationManager.locationServicesEnabled() {
setupLocationManager()
checkLocationAuthorization()
} else {
}
}
func setupLocationManager() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
func checkLocationAuthorization() {
switch CLLocationManager.authorizationStatus() {
case .authorizedWhenInUse:
centerViewOnUserLocation()
break
case .denied:
break
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
break
case .restricted:
break
case .authorizedAlways:
break
#unknown default:
break
}
}
func centerViewOnUserLocation() {
if let location = locationManager.location?.coordinate {
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMeters, longitudinalMeters: regionInMeters)
mapView.setRegion(region, animated: true)
}
}
#IBAction func click(_ sender: Any) {
}
func addPin(title: String, subtitle: String, coordinates: CLLocationCoordinate2D) {
let destination = customPin(pinTitle: title, pinSubTitle: subtitle, location: coordinates)
mapView.addAnnotation(destination)
}
func addAnnotationToMap() {
guard let item = mapItems?.first else { return }
guard let coordinates = item.placemark.location?.coordinate else { return }
addPin(title: item.name!, subtitle: "", coordinates: coordinates)
}
}
SearchTableViewController's function:
func passData() {
guard let mapViewController = storyboard?.instantiateViewController(withIdentifier: "map") as? MapViewController else { return }
guard let mapItem = places?.first else { return }
mapViewController.mapItems = [mapItem]
mapViewController.addAnnotationToMap()
}
This
guard let mapViewController = storyboard?.instantiateViewController(withIdentifier: "map") as? MapViewController else { return }
guard let mapItem = places?.first else { return }
creates a new seperate object other than the actual one that you have to access , use delegate to do it here
searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)
searchVC.delegate = self // here
fpc.set(contentViewController: searchVC)
then declare
weak var delegate:MapViewController?
inside SearchViewController and use it
func passData() {
guard let mapItem = places?.first else { return }
delegate?.mapItems = [mapItem]
delegate?.addAnnotationToMap()
}
Several things:
When you use a third party library in your project and your readers might want to know about that library in order to understand your problem, you should include a link to the library in your question. I did a Google search and was able to find what I think is the correct library.
The sample code for that library has you call fpc.addPanel() in your view controller's viewDidLoad(), not in viewDidAppear().
The viewDidLoad() function is only called once in the lifetime of a view controller, but viewDidAppear() is called every time a view controller get's re-shown (like when it is redisplayed after being covered by a modal and then uncovered again.) The two are not interchangeable for that reason. I suggest moving that call back to viewDidLoad().
Next, as others have mentioned, your SearchTableViewController's passData() function is wrong. It creates a new, throw-away MapViewController every time it is called. It does not talk to the hosting MapViewController at all.
You should refactor your viewDidLoad() to set up the MapViewController as the delegate of the SearchTableViewController.
Define a protocol (possibly in a separate file
protocol SearchTableViewControllerDelegate {
var mapItems: [MapItem] //Or whatever type
func addAnnotationToMap()
}
Some changes to MapViewController
class MapViewController:
SearchTableViewControllerDelegate,
UIViewController,
FloatingPanelControllerDelegate,
UISearchBarDelegate {
//Your other code...
}
override func viewDidLoad() {
super.viewDidLoad()
checkLocationServices()
fpc = FloatingPanelController()
fpc.delegate = self
fpc.surfaceView.backgroundColor = .clear
fpc.surfaceView.cornerRadius = 9.0
fpc.surfaceView.shadowHidden = false
searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)
//--------------------------
searchVC.delegate = self //This new line is important
//--------------------------
fpc.set(contentViewController: searchVC)
fpc.track(scrollView: searchVC.tableView)
fpc.addPanel(toParent: self) //Probably can't be animated at this point
}
And in SearchTableViewController:
class SearchTableViewController: UITableViewController, <Other protocols> {
weak var delegate: SearchTableViewControllerDelegate?
// other code...
func passData() {
guard let mapItem = places?.first else { return }
delegate?.mapItems = [mapItem]
delegate?.addAnnotationToMap()
}
}
You're instantiating a new MapViewController instead if passing data to the one that exists. There are three ways to do this:
Delegation
Closures
Notifications
An example using a closure, in SearchViewController:
var passData: ((MKMapItem) -> ())?
In MapViewController provide it a closure:
searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)
searchVC.passData = { mapItem in
self.mapItems = [mapItem]
}
In SearchViewController call the closure:
passData?(mapItem)

Protocol Delegate between XIB and View Controller

So I have XIB View and View Controller. I want when a button in my XIB clicked (didTapTryAgain Button), the called a function from my view controller. Then I tried to create a protocol and delegate for the class. But it still won't called my function. Here's my XIB view class:
import UIKit
protocol ErrorMessageDelegate {
func refresh(_sender: AnyObject)
}
class ErrorMessage: UIView {
#IBOutlet weak var imageViewError: UIImageView!
#IBOutlet weak var labelError: UILabel!
#IBOutlet weak var buttonTryAgain: UIButton!
static weak var shared: ErrorMessage?
var delegate: ErrorMessageDelegate?
static var message: String?
override func awakeFromNib() {
ErrorMessage.shared = self
labelError.text = ErrorMessage.message
}
#IBAction func didTapTryAgain(_ sender: UIButton) {
delegate?.refresh(_sender: buttonTryAgain)
}
}
And here's my View Controller class:
import Foundation
class BaseViewController: UIViewController, ErrorMessageDelegate {
func refresh(_sender: AnyObject) {
print("I hope my function work here")
}
var uiView = UIView();
override func viewDidLoad() {
super.viewDidLoad()
ErrorMessage.shared?.delegate = self
}
func getErrorMessage(message:String) {
super.viewDidLoad()
Dialog.dismiss()
ErrorMessage.message = message
guard let viewErrorMessage = Bundle.main.loadNibNamed("ErrorMessage", owner: self, options: nil)?.first as? ErrorMessage else { return}
self.view.addSubview(viewErrorMessage)
}
}
I'm following this answer for my code, and it still not working. Is anyone know how to do it? Thank you!
Your problem is that you set the delegate for a shared instance here
ErrorMessage.shared?.delegate = self / here shared?. is nil
but here
guard let viewErrorMessage = Bundle.main.loadNibNamed("ErrorMessage", owner: self, options: nil)?.first as? ErrorMessage else { return}
self.view.addSubview(viewErrorMessage)
you create a separate instance and add it
You need
var viewErrorMessage:ErrorMessage! // add to the vc
viewErrorMessage = Bundle.main.loadNibNamed("ErrorMessage", owner: self, options: nil)?.first as! ErrorMessage
viewErrorMessage.delegate = self
self.view.addSubview(viewErrorMessage)
Also completely git rid of
static weak var shared: ErrorMessage?
Simply use this code then your delegate method will be call.
func getErrorMessage(message:String) {
ErrorMessage.message = message
guard let viewErrorMessage = Bundle.main.loadNibNamed("ErrorMessage", owner: self, options: nil)?.first as? ErrorMessage else { return}
viewErrorMessage.delegate = self
self.view.addSubview(viewErrorMessage)
}
and call method where ever you want to open the popup
getErrorMessage(message: "Test Message")

Resources