Can't access to function in parent view controller from a child via delegate/protocol. print() doesn't print.
In Child VC I have this:
protocol MyViewControllerDelegate: class {
func requestExpandedView()
}
class MyViewController: UIViewController {
weak var delegate: MyViewControllerDelegate?
...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("\(indexPath) didSelectItemAt")
delegate?.requestExpandedView()
}
}
In Parent VC I have this:
extension MessagesViewController: MyViewControllerDelegate {
func requestExpandedView() {
print("Done") // doesn't print anything
requestPresentationStyle(.expanded)
}
}
What's wrong?
You need to set the delegate of MyViewController when creating its instance, i.e.
let vc = self.storyboard?.instantiateViewController(withIdentifier: "MyViewController") as! MyViewController
vc.delegate = self
Related
Please assist. What I'm trying to achieve
is that when I tap on a specific collectionViewCell the ReportDetailsMapController is pushed and my reports[indexPath.item] is sent from MainController to the ReportDetailsMapController.
PROTOCOL:
protocol MainControllerDelegate: class {
func sendReport(data: ReportModel)
}
FIRST VC:
class MainController: UIViewController, UICollectionViewDelegate,UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var delegate: MainControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let report = reports[indexPath.item]
//When I print(report.name) here. Everything executes correctly
self.delegate?.sendReport(data: report)
self.navigationController?.pushViewController(ReportDetailsMapController(), animated: true)
}
}
SecondVC:
class ReportDetailsMapController: UIViewController, MainControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let mc = MainController()
mc.delegate = self
}
func sendReport(data: ReportModel) {
print(data.name)//This does not execute when ReportDetailsMapController is loaded.
print("Report sent")
}
}
ReportModel:
class ReportModel: NSObject {
var name: String?
var surname: String?
var cellNumber: String?
}
That method is not called because you didn't assign the view controller you're pushing to the delegate property.
When the cell is selected, you could do in this order: initialize the view controller and assign it to the delegate, then call the delegate method and push that view controller:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let report = reports[indexPath.item]
//When I print(report.name) here. Everything executes correctly
let viewController = ReportDetailsMapController()
self.delegate = viewController
self.delegate?.sendReport(data: report)
self.navigationController?.pushViewController(viewController, animated: true)
}
However, I think a simpler and more elegant way would be to simply create that property on the ReportDetailsMapController and inject it before pushing it.
There's a similar question/answer related to that here: passing data from tableview to viewContoller in swift
I have a collection view controller. In collectionView cell I have label which I made clickable to push to the nextViewController.
I know that problem in navigationController. But I'm new in swift so can't fix. Hope you guys can help me.
Here's my SceneDelegate:
let layout = UICollectionViewFlowLayout()
// Create the root view controller as needed
let nc = UINavigationController(rootViewController: HomeController(collectionViewLayout: layout))
let win = UIWindow(windowScene: winScene)
win.rootViewController = nc
win.makeKeyAndVisible()
window = win
and my label:
let text = UILabel()
text.text = "something"
text.isUserInteractionEnabled = true
self.addSubview(text)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(PopularCellTwo.labelTapped))
text.addGestureRecognizer(gestureRecognizer)
}
#objc func labelTapped() {
let nextVC = NextViewController()
self.navigationController?.pushViewController(nextVC, animated: true)
print("labelTapped tapped")
}
I also added screenshot. When I click on "Something" It should go next page.
[1]: https://i.stack.imgur.com/4oYwb.png
You can use delegate or closure to do this
class ItemCollectionViewCell: UICollectionViewCell {
var onTapGesture: (() -> ())?
}
Then in your function you do
#objc func labelTapped() {
onTapGesture?()
}
And in your controller
class HomeController: UICollectionViewController {
//...
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = // dequeue cell
cell.onTapGesture = { [unowned self] in
let nextVC = NextViewController()
self.navigationController?.pushViewController(nextVC, animated: true)
}
return cell
}
}
self.navigationController?.pushViewController(nextVC, animated: true)
what self are you referring to ? because you cant make push in child class
you have HomeController i assume its your parent controller .
just try to debug what self is this could attempt by debugging or debug by condition
print (self)
if (self.isKind(of: YourParentController.self)) {
// make push
}
or try to check , see if navigationcontroller somehow has nil value
Here is how you do it using closures. I've created a closure parameter in UICollectionViewCell sub-class. When the label gesture target is hit I call the closure which then executed the navigation in HomeController.
class HomeController: UICollectionViewController {
//...
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = // dequeue cell
cell.labelTap = { [weak self] in
guard let self = self else { return }
let nextVC = NextViewController()
self.navigationController?.pushViewController(nextVC, animated: true)
print("navigated")
}
return cell
}
}
class CollectionViewCell: UICollectionViewCell {
var labelTap: (() -> Void)?
#objc func labelTapped() {
print("labelTapped tapped")
labelTap?()
}
}
From First ViewController I press a Button and open a SecondViewController that view a collection of Image, when I tap on image the view show a full screen image and return at the collection View. Can I pass the image to the first (starting) viewcontroller to use it as a background?
class SecondViewController: UIViewController {
#IBAction func returnHome(_ sender: UIButton) {
//self.dismiss(animated: true, completion: nil)
self.performSegue(withIdentifier: "backToHome", sender: nil)
}
var images = ["bg13", "bg11", "bg6", "bg10", "bg12", "bg5", "bg3", "bg4", "bg2", "bg7", "bg8", "bg9", "bg1", "bg14"]
// ...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let imageViewCollection = UIImageView(image:UIImage(named: images [indexPath.item]))
imageViewCollection.frame = self.view.frame
imageViewCollection.backgroundColor = .black
imageViewCollection.contentMode = .top
imageViewCollection.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreenImage))
imageViewCollection.addGestureRecognizer(tap)
imageViewCollection.addGestureRecognizer(tap)
imageViewCollection.isUserInteractionEnabled = true
self.view.addSubview(imageViewCollection)
}
#objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
sender.view?.removeFromSuperview()
}
}
You can use Delegate to pass the image to the first view controller.
Write this protocol on the top of the Second View Controller.
protocol SecondViewControllerDelegate: NSObject {
func sendToFirstViewController(image: UIImage)
}
Then create a variable inside Second View Controller
class SecondViewController: UIViewController {
....
weak var customDelegate: SecondViewControllerDelegate?
....
}
Call this from collectionView:didSelectItemAt:indexPath method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let image = UIImage(named: images [indexPath.item])
customDelegate?.sendToFirstViewController(image: image)
...
}
In the First viewController, in where you instantiate the second view controller, put this line
secondVc.customDelegate = self
then create an extension to implement the delegate method
extension FirstViewController: SecondViewControllerDelegate {
func sendToFirstViewController(image: UIImage){
// use this image as your background
}
}
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.
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.
}
}