Properly segue from collectionview inside tableviewcell to another viewcontroller - ios

I am trying to segue (pass data) from a collectionview inside a tableviewcell to a another viewcontroller. I tried using the didselect delegate but stuck on how to pass the data properly. It seems i kind of somehow hack my way around it but i would like to learn the proper way. Below is my code:
My Main view controller:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segue" {
let vc = segue.destination as! DetailViewController
vc.text = "Hello World"
}
}
func segue() {
self.performSegue(withIdentifier: "segue", sender: self)
}
}
My Table View:
import UIKit
class MainTableView: UITableView, UITableViewDelegate, UITableViewDataSource {
override func awakeFromNib() {
self.delegate = self
self.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MainTableViewCell", for: indexPath) as! MainTableViewCell
return cell
}
}
My Collection View:
import UIKit
class MoviesCollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
override func awakeFromNib() {
self.delegate = self
self.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MoviesCollectionViewCell", for: indexPath) as! MoviesCollectionViewCell
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = ViewController()
vc.segue()
}
}
The View controller I am trying to segue to:
class DetailViewController: UIViewController {
#IBOutlet weak var label: UILabel!
var text: String?
override func viewDidLoad() {
super.viewDidLoad()
self.label.text = text
// Do any additional setup after loading the view.
}
}
My tableviewcell and collectionviewcell are empty at the moment.

Your question is a little hard to answer as currently stated because it's not entirely clear what you are doing. For example, it's not clear if you are using Storyboards or not.
If you are, then you probably want to define your segues in the storyboard and let UIKit invoke them for you. This documentation provides the overview that will hopefully help you. In particular, you don't have to do the segue manually because UIKit will do it for you once you've set it up in the Storyboard (emphasis mine):
You do not need to trigger segues programmatically. At runtime, UIKit
loads the segues associated with a view controller and connects them
to the corresponding elements. When the user interacts with the
element, UIKit loads the appropriate view controller, notifies your
app that the segue is about to occur, and executes the transition. You
can use the notifications sent by UIKit to pass data to the new view
controller or prevent the segue from happening altogether.
In figure 9-4 you will see the event flow of a segue process. In particular, note that if you override prepareForSegue:sender: in your source view controller for the segue then that is your opportunity to prepare data and send it to the destination view controller (either view setting the representedObject for the destination view controller, or via a custom setter method defined for your destination view controller class). The description text below that figure spells it out:
The prepareForSegue:sender: method of the source view controller lets
you pass data from the source view controller to the destination view
controller. The UIStoryboardSegue object passed to the method contains
a reference to the destination view controller along with other
segue-related information.
If you aren't using storyboards, then you're essentially doing the same thing by hand. You allocate the UIViewController subclass you want to show in response to the touch in the cell item, and then set its representedObject to the data it should display (or call a custom method defined by that class and pass in the data the view controller needs to display), then you show the view controller. To present it manually you'll want to read the Presenting a View Controller document from Apple. In particular:
Presenting a View Controller
There are several ways to initiate the presentation of a view controller:
Use a segue to present the view controller automatically. The segue
instantiates and presents the view controller using the information
you specified in Interface Builder. For more information on how to
configure segues, see Using Segues. Use the showViewController:sender:
or showDetailViewController:sender: method to display the view
controller. In custom view controllers, you can change the behavior of
these methods to something more suitable for your view controller.
Call the presentViewController:animated:completion: method to present
the view controller modally.
Hopefully that's enough to get you going. If not, maybe clarify your question with a bit more context and we'll try again.

Related

Updating tableView from another ViewController using segues

I have a viewController with a tableView, and another viewController that contains data that I'm trying to pass to the tableView. To add entries to the tableView I used segues, but the problem with segues is that they don't update the tableView permanently. They merely create an instance of the ViewController and add the entry there, but the original object remains unchanged. Both ViewControllers are part of a tab bar controller. What I want is to update the table permanently. Meaning, I want to be able to navigate to the viewController where the table is defined and see that's an entry has been added. Here's the code for the viewController with the tableView:
import UIKit
class FavoritesViewController: UIViewController {
public var shops = [
"hello world",
"hello world",
"hello world"
]
#IBOutlet weak var table: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
table.delegate = self
table.dataSource = self
table.reloadData()
}
}
extension FavoritesViewController: UITableViewDelegate, UITableViewDataSource{
func add(_ shopName: String) {
print(shopName)
shops.append(shopName)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shops.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell",for: indexPath)
cell.textLabel?.text = shops[indexPath.row]
return cell
}
// define the action. In this case "delete"
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
// do the actual deleting
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
tableView.beginUpdates()
shops.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
}
}
And here's how I'm trying to update (in the case adding an entry) the tableView in the other viewController:
class MainViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate{
public var shopName:String?
// there are also many other vars, but they're irrelevant
public var favoritesDestinationVC = FavoritesViewController()
// prepares the data for the segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToFavs" {
favoritesDestinationVC = segue.destination as! FavoritesViewController
if let newShopName = shopName {
favoritesDestinationVC.shops.append(newShopName)
}
}
}
}
However, when I add the entry the to the table, the segue creates an instance of FavoritesViewController, adds that entry there, and then displays it in a popup window like this:
But when I dismiss this window the changes disappear (the tableView remains the same; 3 times "Hello World").
I want the changes to be saved in the tableView after dismissing this window. Any idea on how to do that? On how to make those changes permanent?
By your explanation, it looks like. Segue is defined as presenting FavoritesViewController modally. and this behavior is expected. By your implementation. Since you are not updating the view controller object which is part of the tab bar controller.
To make the changes in the controller in tab bar controller, Either you access that object using tabcontroller.viewcontrollers. Communicate using another way like a delegate or notification.
Edit:
It totally depends on where you want to access FavoritesViewController.
If you want to access from TabBarViewController (based on your view hierarchy it may change. be careful about index and typecasting):
let favVC = self.viewControllers?[1] as! FavoritesViewController
favVC.shops.append(newShopName)
If you want to access from a view controller which is part of same tab bar controller:
var favVC = self.tabBarController?.viewControllers?[1] as! FavoritesViewController
favVC.shops.append(newShopName)
Note: Viewcontrollers's index depends on viewcontroller order in your tab bar controller. And type casting depends upon your view hierarchy.

Perform a segue selection from a uicollectionview that is embedded in a tableview cell?

Currently we have a uicollectionview that is embedded in a tableview cell. When the collection view cell is selected it's suppose to initiate a push segue to another view controller. The problem is there is no option to perform the segue on the cell. Is there a way around it? Here is the cell:
class CastCell : UITableViewCell {
var castPhotosArray: [CastData] = []
let extraImageReuseIdentifier = "castCollectCell"
let detailToPeopleSegueIdentifier = "detailToPeopleSegue"
var castID: NSNumber?
#IBOutlet weak var castCollectiontView: UICollectionView!
override func awakeFromNib() {
castCollectiontView.delegate = self
castCollectiontView.dataSource = self
}
}
extension CastCell: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return castPhotosArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = castCollectiontView.dequeueReusableCell(withReuseIdentifier: extraImageReuseIdentifier, for: indexPath) as! CastCollectionViewCell
cell.actorName.text = castPhotosArray[indexPath.row].name
return cell
}
}
extension CastCell: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.castID = castPhotosArray[indexPath.row].id
performSegue(withIdentifier: detailToPeopleSegueIdentifier, sender: self) //Use of unresolved identifier 'performSegue' error
}
}
extension CastCell {
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let peopleVC = segue.destination as! PeopleDetailViewController
peopleVC.id = self.castID
}
}
The problem is there is no option to perform the segue on the cell
There is no such thing as a "segue on a cell". A segue is from one view controller to another. performSegue is a UIViewController method. So you cannot say performSegue from within your CastCell class, because that means self.performSegue, and self is a UITableViewCell — which has no performSegue method.
The solution, therefore, is to get yourself a reference to the view controller that controls this scene, and call performSegue on that.
In a situation like yours, the way I like to get this reference is by walking up the responder chain. Thus:
var r : UIResponder! = self
repeat { r = r.next } while !(r is UIViewController)
(r as! UIViewController).performSegue(
withIdentifier: detailToPeopleSegueIdentifier, sender: self)
1: A clean method is to create a delegate protocol inside your UITableViewCell class and set the UIViewController as the responder.
2: Once UICollectionViewCell gets tapped, handle the taps inside the UITableViewCell and forward the tap to your UIViewController responder through delegatation.
3: Inside your UIViewController, you can act on the tap and perform/push/present whatever you want from there.
You want your UIViewController to know what is happening, and not call push/presents from "invisible" subclasses that should not handle those methods.
This way, you can also use the delegate protocol for future and other methods that you need to forward to your UIViewController if needed, clean and easy.

Perform Segue from Collection View Cell

import UIKit
class ActionCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var actionGIF: UIImageView!
#IBAction func actionPressed(sender: AnyObject) {
print(myLabel.text)
Global.actionButtonIndex = myLabel.text!.toInt()! - 1
print(actionGIF.image)
ActionViewController.performSegueWithIdentifier("showActionPreview", sender: nil)
}
}
I am trying to perform a Segue after User Clicking on One of the Cell in my Collection View. Can't seem to do that using performSegueWithIdentifier. App Screenshot
Here's an elegant solution that only requires a few lines of code:
Create a custom UICollectionViewCell subclass
Using storyboards, define an IBAction for the "Touch Up Inside" event of your button
Define a closure
Call the closure from the IBAction
Swift 4+ code
class MyCustomCell: UICollectionViewCell {
static let reuseIdentifier = "MyCustomCell"
#IBAction func onAddToCartPressed(_ sender: Any) {
addButtonTapAction?()
}
var addButtonTapAction : (()->())?
}
Next, implement the logic you want to execute inside the closure in your
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.reuseIdentifier, for: indexPath) as? MyCustomCell else {
fatalError("Unexpected Index Path")
}
// Configure the cell
// ...
cell.addButtonTapAction = {
// implement your logic here, e.g. call preformSegue()
self.performSegue(withIdentifier: "your segue", sender: self)
}
return cell
}
You can use this approach also with table view controllers.
Instance method performSegue is not available from a UICollectionViewCell:
Since an UICollectionViewCell is not an UIViewController, you can not use performSegue(withIdentifier:sender:) from it. You may prefer use delegates to notify your parent view controller and then, performSegue from there.
Take a look at the details of this answer. The question is slightly different but the solution lies in the same pattern.
Have you set the segue identifier by exactly named "showActionPreview". Moreover, ensure that your segue linked from your parent view controller to your destination view controller in storyboard. Hope this would be help.

How to add a segue programmatically?

I know, I know this has been asked a lot of times. I also found this question but the solution it suggested did not work for me.
I am just trying to build an app to demonstrate how to use those things in UIKit (in case I want to use them later on. I can just copy the code).
I have created a View Controller with a table view in it. I wrote a class called PrototypeTableController to act as the view controller class for the view controller I created in the storyboard.
When the user taps on one of the cells, I want another view controller to show, called Prototype Table Content. And different text will be shown if you tap on different cells.
In the storyboard, it's like this:
The text of the label in Prototype Table Content will be different when the user taps on a different cell. This means I need to send data from one view controller to another.
The post mentioned above suggested that I should give the segue an identifier, so I did:
Here is my code:
View controller class for the table view:
class PrototypeTableController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let data = ["Cell1", "Cell2", "Cell3", "Cell4", "Cell5"]
let contents = ["Hello", "Nice", "OMG", "Jesus", "Peace"]
var content: String?
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = data[indexPath.row]
return cell
}
func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return "This is a prototype table view created by Sweeper"
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "my table"
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
content = contents[indexPath.row]
tableView.deselectRowAtIndexPath(indexPath, animated: true)
performSegueWithIdentifier("showContent", sender: tableView)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showContent" {
let destination = segue.destinationViewController as! PrototypeTableContentViewController
destination.contentString = content
}
}
}
View controller class for Prototype Table Content view:
class PrototypeTableContentViewController: UIViewController {
#IBOutlet var tableContent: UILabel!
var contentString: String?
override func viewDidLoad() {
super.viewDidLoad()
tableContent.text = contentString
}
}
I think I did all the things suggested in the post mentioned above. I added an identifier, I called performSegueWithIdentifier
, I also deselected the cell after the tapping.
However, it just doesn't go to the other view controller! It stays on the same controller! Like this:
When the user taps on one of the cells, I want another view controller to show, called Prototype Table Content. And different text will be shown if you tap on different cells.
While you can programmatically call performSegueWithIdentifier, it's a lot of effort that the storyboard can automatically handle for you. Just use a show storyboard segue from your prototype cell to PrototypeTableContentViewController.
prepareForSegue knows which cell you selected because the cell is the sender. All you have to do is set the destination view controller's contentString.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let controller = (segue.destinationViewController as? PrototypeTableContentViewController where segue.identifier == "showContent", let cell = sender as? UITableViewCell, textLabel = cell.textLabel else {
return
}
controller.contentString = textLabel.text
}
This is very similar to how a template like Master-Detail segues from a cell to show details about a cell (although Apple uses indexPathForSelectedRow to pass the cell's details):
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
In either case, the SDK performs the storyboard segue for you; a segue didn't need to be programmatically added or performed.
Make sure your tableview delegate is set. If you are using storyboard, make sure delegate outlet in your storyboard is connected properly. If you are creating tableview by code, then you should do tableView.delegate=self; to set the delegate.
Your code is fine.
And one more thing:
You might need to change this line:
performSegueWithIdentifier("showContent", sender: tableView)
you need to make the sender as the row but not the tableview,so that the prepare for segue will get the sender as row instead of whole tableview.
As you are calling the prepareForSegue overtime you select a row, it makes sense to make the row as sender in performSegueWithIdentifier.
So it would be:
let row=indexPAth.row
performSegueWithIdentifier("showContent", sender: row)

how to make my searchBar TableView perform a segue

I have created a TableView application following the "Beginning iPhone Development with Swift " book.The search Bar tableView is created with code and not within the storyboard.The book explains how to get search results and display the corresponding cells but I would like my app to perform a segue to a ViewController I have created in the storyBoard.How can I trigger a Segue with code ?
for more info , this is my file :
import UIKit
class SearchResultsController: UITableViewController , UISearchResultsUpdating{
let sectionsTableIdentifier = "section identifier"
var products = [product]()
var filteredProducts = [product]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.registerClass(UITableViewCell.self,
forCellReuseIdentifier: sectionsTableIdentifier)
}
// MARK: - Table view data source
override func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return filteredProducts.count
}
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(
sectionsTableIdentifier) as UITableViewCell
cell.textLabel!.text = filteredProducts[indexPath.row].name
return cell }
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "detailView"{
let index = self.tableView?.indexPathForSelectedRow()
var destinationViewController : infoViewController = segue.destinationViewController as infoViewController
destinationViewController.Title = filteredProducts[index!.row].title
destinationViewController.eam = filteredProducts[index!.row].energy
destinationViewController.fam = filteredProducts[index!.row].fat
destinationViewController.pam = filteredProducts[index!.row].protein
destinationViewController.cam = filteredProducts[index!.row].carbohydrates
destinationViewController.imgName = filteredProducts[index!.row].imgName
}
}
func updateSearchResultsForSearchController(
searchController: UISearchController) {
let searchString = searchController.searchBar.text
filteredProducts.removeAll()
for prod in products{
var name = prod.name.lowercaseString
if name.rangeOfString(searchString) != nil {
filteredProducts.append(prod)
}
}
tableView.reloadData()
}}
Because the controller is built in code, you need to use the SearchResultsController's tableView delegate method didSelectRowAtIndexPath to trigger the presentation of the next view controller.
Assuming that there is a table view controller underpinning the SearchResultsController, you could potentially use that as the delegate of the SearchResultsController. The main table view controller might already have the necessary code to segue when a cell is selected, in which case you need to check which tableView has been selected in order to correctly determine which product the cell represents.
To set the delegate, add the following line to the code (in your comment above) where you create the SearchResultsController:
resultsController.tableView?.delegate = self
Then amend the didSelectRowAtIndexPath method to test which tableView is triggering the method:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if (tableView == self.tableView) {
// use the existing code to present the detail VC, based on the data in the main table view
...
} else {
// use new code to present the detail VC, based on data from the SearchResultsController
...
}
}
If the main table view controller is in a storyboard, you can use a segue to present the detail VC. In this case you would use self.performSegueWithIdentifier() in the above code. If not, you would either use self.navigationController?.pushViewController() (if you are embedded in a navigation controller) or self.presentViewController() (to present the detail VC modally).
Another option would be to set the SearchResultsController's delegate to be self (in viewDidLoad), and then to implement didSelectRowAtIndexPath in the SearchResultsController class. In this case, you don't need to test which tableView has triggered the method, but you will not be able to use a segue.

Resources