Trigger a function when user taps `Back` in detail view controller - ios

I need to refresh my collectionView when user returns to that VC because what he/she did in the detailVC has affect on the previous VC data. I tried collectionView.reloadData() in both viewDidLoad() and viewDidAppear() of my VC has the collectionView in it. And It came up that when user taps the 'Back' in detailVC both viewDidLoad() and viewDidAppear() do not work. So, I tried to call one of them in detailVC with instantiate the firstVC(which has the collectionView)
then I got an runtime error which said collectionView is nil. Any thoughts? (BTW, the segue between them is ShowPush, and I can not change it because I have to have the transition of this segue in my app.)
Here is the firstVC:
class SkillsController: UIViewController{
#IBOutlet weak var collectionView: UICollectionView!
var TAGS: [TAG] = []
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "TagCell", bundle: nil)
collectionView.register(nib, forCellWithReuseIdentifier: "tagCell")
self.sizingCell = (nib.instantiate(withOwner: nil, options: nil) as NSArray).firstObject as! TagCell?
self.loadMore()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("back to skills")
self.TAGS = TagManager.shared.tagList
collectionView.reloadData()
}
}
TAGS is my data which is stored in Realm database.
Here is the detailVC:
class SeeSelectedController: UICollectionViewController {
var TAGS: [TAG] = []
#IBOutlet weak var layout: FSQCollectionViewAlignedLayout!
override func viewDidLoad() {
super.viewDidLoad()
if currentTab.shared.isSkill {
self.title = "Selected Skills"
//init tags
let list = RealmManager.shared.skills
if let list = list {
for element in list {
TAGS.append(TAG(n: element.value!, iS: true))
}
}
collectionView?.reloadData()
}else{
self.title = "Selected Needs"
//init tags
let list = RealmManager.shared.needs
if let list = list {
for element in list {
TAGS.append(TAG(n: element.value!, iS: true))
}
}
collectionView?.reloadData()
}
let nib = UINib(nibName: "TagCell", bundle: nil)
collectionView?.register(nib, forCellWithReuseIdentifier: "tagCell")
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let item = TAGS[indexPath.row].name!
let currentState = TAGS[indexPath.row].isSelected!
TAGS[indexPath.row].isSelected = currentState ? false:true
if currentState {
print("deselect")
//remove from realm
RealmManager.shared.deleteItemFromList(type: getTypeOfTag(isSkill: currentTab.shared.isSkill), item: item)
}else{
print("select")
//add to realm
RealmManager.shared.addItemToList(type: getTypeOfTag(isSkill: currentTab.shared.isSkill), item: item)
}
if currentTab.shared.isSkill {
let VC: SkillsController = storyboard?.instantiateViewController(withIdentifier: "SkillsController") as! SkillsController
VC.viewDidAppear(true)
}
collectionView.reloadData()
//addd
}
}
So how it is working? in the SkillsVC user can select some tags from a pool, in the detailVC which is SeeSelecteVC he/she can drop selected tags. It is constantly changing in the Realm as you can see. The problem when user has dropped some tags in detailVC and press the Back button, the dropped tags are still looking as selected in SkillsVC. However when if user goes another VC and comes back to SkillsVC (by this way the viewDidLoad() is gonna work) the dropped tags are seems to be unselected. That's all.

If what you are looking for is just to reload on back button
What you can do is create your own custom UIBarButtonItem that will make you navigate backwards from your "detail view controller". What you should do next after adding your own back button is add an IBAction for UIBarBUttonItem and pop your "detail view controller".
Right before you do this, you should create a delegate that will be executed before the popping happens that will reload your UICollectionView.
The following is not the best way to achieve what you want:
In your didSelectItem for your second view controller, you are creating a new view controller here and you shouldn't force call viewDidAppear. Since you are creating a new UIViewController, you are not referencing the previous UIViewController that you came from and soo your UICollectionView is nil.
if currentTab.shared.isSkill {
//remove the below lines and call the delegate here
let VC: SkillsController = storyboard?.instantiateViewController(withIdentifier: "SkillsController") as! SkillsController
VC.viewDidAppear(true)
}
collectionView.reloadData()
What you should be doing is:
You should use delegates to send callbacks to previous view controllers or perform actions.
To create a delegate-
Using the first approach (using your own back button)-
protocol delegateVC{
func reloadCollectionView()
}
class SeeSelectedController: UICollectionViewController{
//add this inside this class
var delegate : delegateVC?
...
//implement your IBAction for back button and inside it-
... {
self.delegate.reloadCollectionView()
}
}
OR the second approach i pointed out (Just change your didSelectItem and it will reload the collectionView, no need to fret about back button at all and save the hassle, i strongly recommend this approach)
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let item = TAGS[indexPath.row].name!
let currentState = TAGS[indexPath.row].isSelected!
TAGS[indexPath.row].isSelected = currentState ? false:true
if currentState {
print("deselect")
//remove from realm
RealmManager.shared.deleteItemFromList(type: getTypeOfTag(isSkill: currentTab.shared.isSkill), item: item)
}else{
print("select")
//add to realm
RealmManager.shared.addItemToList(type: getTypeOfTag(isSkill: currentTab.shared.isSkill), item: item)
}
if currentTab.shared.isSkill {
self.delegate.reloadCollectionView()
}
}
}
And in your first view controller-
func reloadCollectionView(){
collectionView.reloadData()
}
Note: In your prepareForSegue remember to set the delegate of your detail view controller to be your first view controller

Related

Second ViewController won't open properly Swift

i'm having a problem where my first view controller is just repeating itself and not showing the second view controller, I've watched videos on how to pass data from one view controller to another and i have it all set up the way its supposed to be. it transfers the data to the second view controller properly and I've tested it with Printing the information I'm passing, but any other ui elements won't show up on the second view controller, i think they are being covered by the table view but it doesn't make sense to me and I'm not sure how to test this.
when i press on a table view cell its supposed to open the second view controller
this is the code that sends and presents the second view controller:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
//open another view contoller and show the recipe
let secondvc = self.display![indexPath.row]
let secondvcresources = secondvc.resource
let secondvcdirections = secondvc.directions
let secondvcname = secondvc.name
let vc = CustomSecondViewController(resources: secondvcresources!, directions: secondvcdirections!, name: secondvcname!)
present(vc,animated: true)
}
this is the second view controller:
import UIKit
class CustomSecondViewController: ViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGray
title = name.uppercased()
let textview = UITextView()
textview.frame = view.bounds
}
private let name: String
private let directions: String
private let resources: String
init(resources: String, directions: String, name: String ){
self.resources = resources
self.directions = directions
self.name = name
super.init(nibName: nil, bundle: nil)
print(resources)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Your current code doesn’t work because your CustomSecondViewController‘s init method,
init(resources:directions:name:)
…doesn’t do anything to load the view controller’s views. A CustomSecondViewController you create with that init won’t have any views, and won’t display to the screen.
If you want to load your CustomSecondViewController’s views from a storyboard, you need to use the function instantiateViewController(withIdentifier:) to create it.
Your rewritten tableView(_:didSelectRowAt:) function might look like this:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
//open another view contoller and show the recipe
let secondvc = self.display![indexPath.row]
let secondvcresources = secondvc.resource
let secondvcdirections = secondvc.directions
let secondvcname = secondvc.name
guard let vc = self.storyboard?.instantiateViewController(withIdentifier: “CustomSecondViewController”) else {
fatalError(“CAn’t load view controller from storyboard”)
}
// you’ll need to refactor your CustomSecondViewController so it’s properties are public vars, not private “lets”
vc.directions = secondvc.directions
vc.resources = seconded.resources
vc.name = secondvc.name
present(vc,animated: true)
}

how to push ViewController from xib tableview Cell?

I have tableView within collectionView and i created using xib. I want to pushViewController when item is selected .I tried pushing the view controller in itemForRowAt method but it's not possible
class SecondTableView: UITableViewCell , UIcollectionViewDelegate ,UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// let vc = DataTableViewController()
// vc.delegate = self
// vc.pushNav()
let memeVC = storyboard?.instantiateViewController(withIdentifier: "MemeViewController") as! MemeViewController
memeVC.imagePassed = image
navigationController?.pushViewController(memeVC, animated: true)
print("item tapped\(indexPath)")
}
}
errors
Use of unresolved identifier 'storyboard'; did you mean 'UIStoryboard'?
Add segue as mentioned in the image and select show
Set segue identifier in your storyboard as mentioned in the image
Add below Protocol:
protocol CollectionViewCellDelegate: class {
func userDidTap()
}
Add below property in your table view class where the collection view delegate returns.
weak var delegate: CollectionViewCellDelegate?
Update delegate method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate.userDidTap()
print("item tapped\(indexPath)")
}
}
confirm CollectionViewCellDelegate delegate to your first view controller & add this method:
func userDidTap() {
performSegue(withIdentifier: "showMemeViewController", sender: nil)
}
Add prepare for segue in your first VC.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showMemeViewController" {
let memeVC: MemeViewController = segue.destination as! MemeViewController
memeVC.imagePassed = image
}
}
Please try this let me know if you faced any other issue. Thanks.
You're trying to access storyboard property in a tableview cell, which isn't there in the first place, it's a UIViewController property not a UITableViewCell property, hence the error, you'll have to delegate your selection to the viewController then let the controller do the segue normally.
You can start doing that via delegates as I mentioned above, for example if you're passing image to the controller you wanna segue to:
protocol ImageTableViewCellDelegate: class {
// pass the the image or any variable you want in the func params
func userDidTap(_ image: UIImage)
}
then you'd add it to the cell and confirm the controller to it
class SecondTableView {
weak var delegate: ImageTableViewCellDelegate?
}
In your controller:
extension MyViewController: ImageTableViewCellDelegate {
func userDidTap(_ image: UIImage){
let memeVC = storyboard?.instantiateViewController(withIdentifier: "MemeViewController") as! MemeViewController
memeVC.imagePassed = image
navigationController?.pushViewController(memeVC, animated: true)
print("item tapped\(indexPath)")
}
}
this should work, let me know if you had any further problems

How to send actions from one ViewController to another ViewController (active together) [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
I know this question has been asked so many many times. But here the scenario is changed. I am using MMParallaxView and that looks pretty awesome.
But Now at this point, I want to communicate between these two View controllers. Let me tell you that this github code helps us to make a view like ios Map App. I mean you can have Map (as 1st view controller) and you can have a list of places view controller on top of 1st View Controller. Just same like Map app.
Now I want to click on the UITableView cell and want to navigate on map. For this I know how to catch delegated method. And I am successfully getting of taps on the UItableView cell. But How to send that clicked item data to 1stView Controller so that It can show or mark selected area on Map.
I know this can also be done. But how?
To modify the example app for MMParallaxView...
In ChildBottomViewController.swift
Add this at the top (outside of the class):
protocol CellTapDelegate {
func didSelectRow(indexPath: IndexPath)
}
Add this line at the beginning of the class:
var cellTapDelegate: CellTapDelegate?
Change didSelectRowAt function to:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
DispatchQueue.main.async { [weak self] in
self?.cellTapDelegate?.didSelectRow(indexPath: indexPath)
}
}
In MapViewController.swift
Change the class declaration to:
class MapViewController: UIViewController, CellTapDelegate {
and add this function:
#objc func didSelectRow(indexPath: IndexPath) {
print("Delegate got didSelectRow for: \(indexPath)")
}
Then, in SecondViewController.swift
Add this at the end of viewDidLoad():
var mapVC: MapViewController?
var bottomVC: ChildBottomViewController?
for vc in self.childViewControllers {
if vc is MapViewController {
mapVC = vc as? MapViewController
} else if vc is ChildBottomViewController {
bottomVC = vc as? ChildBottomViewController
}
}
// if we found valid child view controllers, set the delegate
if mapVC != nil && bottomVC != nil {
bottomVC?.cellTapDelegate = mapVC
}
Now, selecting a row from the "slide up from bottom" table view will send the selected indexPath to its delegate - the map view controller.
protocol CellTapDelegate {
func didTapOnItem(obj: MyObject)
}
class MyViewControllerContainingTheTableView : UIViewcontroller {
weak var delegate: CellTapDelegate?
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
let item = arrayOfObjects[indexpath.row]
self.delegate.didTapOnItem(item)
}
}
//View Controller where you you will have the listner
class FirstViewController : UIViewController {
func setupParalax() {
// you have to modify this according to your need
let vc = MyViewControllerContainingTheTableView()
vc.delegate = self
}
}
extension FirstViewController: CellTapDelegate {
func didTapOnItem(obj: MyObject) {
// you have your required object here
}
}
This should be possible by implementing your own delegate, but my knowledge of MMParrallaxView is that there is only 1 view controller which displays two views meaning the view controller should be able to pass things between the two views as it is. In this case the view controller would implement the table view delegate methods and add the table view to the bottom view. Then add the map to the top view. This should allow you to catch the table view cell selection and update the map in the top view accordingly.
Example based on the structure I believe you are using:
public class FirstViewController {
public let parallaxView = MMParallaxView()
private var secondViewController: SecondViewController
private var thirdViewController: ThirdViewController
override open func viewDidLoad() {
secondViewController = SecondViewController()
thirdViewController = ThirdViewController()
super.viewDidLoad()
self.view.addSubview(parallaxView)
thirdViewController.delegate = secondViewController
parallaxView.parallaxTopView = secondViewController.view
self.addChildViewController(secondViewController)
secondViewController.didMove(toParentViewController: self)
parallaxView.parallaxBottomView = thirdViewController.view
self.addChildViewController(thirdViewController)
thirdViewController.didMove(toParentViewController: self)
}
}
public class SecondViewController: ThirdViewControllerDelegate {
public func updateMap() {
// Update map
}
}
public class ThirdViewController: UITableViewDelegate {
weak var delegate ThirdViewControllerDelegate?
private var table: UITableView = UITableView()
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.updateMap()
}
}
protocol ThirdViewControllerDelegate: class {
public func updateMap() {}
}
if the view controllers are instantiated through the Storyboard then you can try this:
public class MasterViewController {
var secondViewController: SecondViewController?
override func viewDidLoad() {
super.viewDidLoad()
self.performSegue(withIdentifier: "MMParallaxTop", sender: nil)
self.performSegue(withIdentifier: "MMParallaxBottom", sender: nil)
}
override open func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let secondVC = segue.destination as? SecondViewController {
secondViewController = secondVC
}
if let thirdVC = segue.destination as? ThirdViewController, let second = secondViewController {
thirdVC.delegate = second
}
}
}

Has no segue with identifier 'searchSegue' through xib file's class

I am using a segue(searchSegue) to connect my search screen (SearchViewController) to the search result page (PhotoStreamViewController).
SearchViewController is a collection view. Every collection view cell has a design in Xib file. (Xib file's class is SearchCategoryRowCell)
Currently, when I triggered searchSegue through SearchViewController it works fine. But whenever I trigger the same segue through SearchCategoryRowCell, I am getting Has no segue with identifier 'searchSegue'
SearchViewController.swift
class SearchViewController: UIViewController,UISearchBarDelegate,UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
...
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchRedirect(keyword: searchBar.text!)
}
func searchRedirect(keyword:String) {
performSegue(withIdentifier: "searchSegue", sender:self)
PhotoStreamViewController.searchString = keyword
navigationController?.setNavigationBarHidden(false, animated: true)
}
...
}
SearchCategoryRowCell.swift
class SearchCategoryRowCell: UICollectionViewCell {
//the method below is triggered by a click action
#objc func searchRedirection(_ sender:UIButton) {
print("we")
print(subCategoryArray[sender.tag].name)
let searchViewcontroller = SearchViewController(nibName: "SearchViewController", bundle: nil)
searchViewcontroller.searchRedirect(keyword: "otherSearchKeyword")
}
...
}
You've explained the reason perfectly. You are saying:
let searchViewcontroller = SearchViewController(nibName: "SearchViewController", bundle: nil)
searchViewcontroller.searchRedirect(keyword: "otherSearchKeyword")
So searchViewcontroller is a completely new view controller instantiated right there in your code. Thus, it has no segues. The view controller with the segues is the one in the storyboard, which is a completely different view controller.
This
let searchViewcontroller = SearchViewController(nibName: "SearchViewController", bundle: nil)
is not the currently presented VC , you have to use delegate or declare self as a property inside the cell class
class SearchCategoryRowCell: UICollectionViewCell {
var sear:SearchViewController?
}
ans set it inside cellForItem in SearchViewController
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellID", for: indexPath) as! cellClassName
cell.sear = self
return cell;
}
//
then you can use the presented object
#objc func searchRedirection(_ sender:UIButton) {
print("we")
print(subCategoryArray[sender.tag].name)
sear?.searchRedirect(keyword: "otherSearchKeyword")
}

Segue from UITableViewCell by tapping on an image inside of cell

Been trying to figure this out for a while now and after a couple hours of searching for a solution I decided it was time to ask.
I have a tableview that gets populated by custom UITableViewCells and currently when you tap on a cell it takes you to a detail view.
Within the custom cell there is an image, I would like the user to be able to tap on that image and segue to a popover VC that shows the image.
What I'm having trouble with is creating the segue when the image is tapped.
In the file for the custom cell, I've set up a tap gesture recognizer on the image (pTap):
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: #selector(PostCell.voteTapped(_:)))
let ptap = UITapGestureRecognizer(target: self, action: #selector(PostCell.imageTapped(_:)))
tap.numberOfTapsRequired = 1
ptap.numberOfTapsRequired = 1
voteImage.addGestureRecognizer(tap)
voteImage.userInteractionEnabled = true
featuredImg.addGestureRecognizer(ptap)
featuredImg.userInteractionEnabled = true
}
I also have a function in the custom cell file for the tap:
func imageTapped(sender: UIGestureRecognizer) {
print("image tapped")
}
In my view controller file I've added a segue in did select row at index path:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let post: Post!
if inSearchMode {
post = filteredVenues[indexPath.row]
} else {
post = posts[indexPath.row]
}
print(post?.venueName)
performSegueWithIdentifier("imageTapped", sender: nil)
performSegueWithIdentifier("DetailsVC", sender: post)
}
Also, in the storyboard I have created a segue from the VC that holds the tableview with the custom cells to the VC I'd like to show when the image is tapped.
I've tried several different methods of getting this to work and haven't had any luck, the code you see above are what remains after my many failed attempts. I feel that the function for the tap in the custom cell file and the segue in the VC file are a part of the solution so that is why I have left them in.
Any help would be appreciated. Thanks!
Updates to code from answers below:
Added protocol
protocol ImageSegueProtocol: class {
func imageTapped(row: Int)
}
class PostCell: UITableViewCell {
Added IAB Func
#IBAction func imageTapped(sender: UIGestureRecognizer) {
guard let row = row else { return }
delegate?.imageTapped(row)
print("image tapped func")
}
Declared delegate in the Main VC
weak var delegate:postCell?
Assigned Delgate
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//let post = posts[indexPath.row]
if let cell = tableView.dequeueReusableCellWithIdentifier("PostCell") as? PostCell {
var img: UIImage?
var vImg: UIImage?
postCell?.delegate = self
Added extension function
extension FeedVC: ImageSegueProtocol {
func imageTapped(row: Int) {
if inSearchMode == true {
let object = filteredVenues[row]
performSegueWithIdentifier("imageTapped", sender: object)
print("ext func")
} else {
let object = posts[row]
performSegueWithIdentifier("imageTapped", sender: object)
print("ext func")
}
}
You could do it like this:
Within the cell file, declare a protocol at the top, and then set up some properties within the cell class itself and the delegate behaviour in response to the image being tapped:
protocol SomeCellProtocol: class {
func imageTapped(row: Int)
}
class SomeCell: UITableViewCell {
weak var delegate: SomeCellProtocol?
var row: Int?
#IBAction func imageTapped() {
guard let row = row else { return }
delegate?.imageTapped(row)
}
}
You must then make your ViewController adopt SomeCellProtocol and call the segue within like so:
extension SomeViewController: SomeCellProtocol {
func imageTapped(row: Int) {
//get object from array and call segue here
}
}
And you must set your ViewController as the delegate of your cells by calling:
someCell.delegate = self
and pass the row to the cell:
someCell.row = indexPath.row
within your cellForRowAtIndexPath method of the ViewController.
So, when the button is tapped within the cell (or you can do it with a GestureRecognizer on the ImageView if you want) it will force the delegate (the ViewController) to call its imageTapped function, passing a row parameter which you can use to determine which object in the table (its corresponding data array) should be passed via the Segue.
As OhadM said, create a button and set the background image as the image you want displayed. From there, you don't even need an IBAction. Control-drag from the button to the next view controller and create the segue.
Then, if you want to do any setup before the segue, in the first VC you'd have something like:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "imageButtonPressed" { // Make sure you name the segue to match
let controller = segue.destinationViewController as! SecondVC
controller.someText = "Hi"
controller.someInt = 5
}
}
What you need to do is to create a button instead of the image and this button will hold the actual image:
The rest is easy, ctrl drag an IBAction into your custom cell file.
Now you need to communicate with your View Controller in order to invoke your segue method.
You can achieve that using 2 design patterns:
Using post notification:
Inside your View Controller add an observer:
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(YourViewController.cellSelected(_:)),
name: "CellSelectedNotificationName",
object: nil)
Your method cellSelected inside your View Controller should look like this:
func cellSelected(notification : NSNotification)
{
if let passedObject = notification.object as? YourCustomObject
{
// Do what ever you need with your passed object
// When you're done, you can invoke performeSegue that will call prepareForSegue
// When invoking performeSegue you can pass your custom object
}
}
Inside your CustomCell class at the IBAction method of your button:
#IBAction func buttonTapped()
{
// Prepare the object you want to pass...
NSNotificationCenter.defaultCenter().postNotificationName("CellSelectedNotificationName", object: YourCustomObjectYouWantToPass)
}
In a nut shell, a button inside a cell was tapped, you created an object inside that cell and you pass it to a View Controller using the notification centre. Now, you can segue with your custom object.
NOTE: If you don't need to pass an object you can basically ctrl + drag from your UIButton (at your storyboard) to another View Controller.
Using a delegate that is pointing to the View Controller.
Good luck.

Resources