I'm using an external delegate file to handle all a UICollectionView's processing and I'm struggling to get the collection view cell to perform a segue based on the selected cell, via the delegate file.
Here's what I currently have inside the delegate file:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
let mainController = MainController()
mainController.performSegue(withIdentifier: "detailSegue", sender: cell)
}
and on the Main Controller I have:
override func performSegue(withIdentifier identifier: String, sender: Any?) {
if identifier == "detailSegue" {
let detailController = DetailController()
self.present(detailController, animated: true)
}
}
The error I'm getting:
Warning: Attempt to present <DetailController: 0x7fbed8e6e4b0> on <MainController: 0x7fbedda79830> whose view is not in the window hierarchy!
I thought I could call up the reference via the delegate and it would present the controller.
Thanks
The reference of MainController that you are trying to get like below is wrong.
let mainController = MainController()
This will not give you the loaded/previewing instance of the object instead of this it will create a new object of MainController for you which is not in the previewing Hierarchy. So you need an actual previewing instance.
Solution
Create a global object of of UIViewController type in your delegate class and pass you previewing class reference so that you may use it when you need.
Example.
class DelegateDataSource: NSObject {
var viewController: UIViewController?
//Other Methods/Objects that you need.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
if let mainController = viewController as? MainController {
mainController.performSegue(withIdentifier: "detailSegue", sender: cell)
}
}
}
class MainController: UIViewController {
var delegateDataSource: DelegateDataSource?
func initializeDelegates() {
//Initialize you datasource object and do some tasks that you need and then assign your current instance to this class like this.
delegateDataSource.viewController = self
}
}
let mainController = MainController()
mainController.performSegue(withIdentifier: "detailSegue", sender: cell)
This will not work, either use delegate pattern or use singleton pattern for your MainController.
Only an already presented controller can present another controller. Is the class implementing didSelectItemAt a UIViewController? If so, why not just do:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
performSegue(withIdentifier: "detailSegue", sender: cell)
}
Related
I have a custom UICollectionViewCell with a button inside it. When I tap the button, an event is fired inside that subclass. I want to then trigger an event on the UICollectionView itself, which I can handle in my view controller.
Pseudo-code:
class MyCell : UICollectionViewCell {
#IBAction func myButton_touchUpInside(_ sender: UIButton) {
// Do stuff, then propagate an event to the UICollectionView
Event.fire("cellUpdated")
}
}
class MyViewController : UIViewController {
#IBAction func collectionView_cellUpdated(_ sender: UICollectionView) {
// Update stuff in the view controller
// to reflect changes made in the collection view
}
}
Ideally, the event I define would appear alongside the default action outlets in the Interface Builder, allowing me to then drag it into my view controller code to create the above collectionView_cellUpdated function, similar to how #IBInspectable works in exposing custom properties.
Is there any way to implement a pattern like this in native Swift 3? Or if not, any libraries that make it possible?
I don't understand your question completely but from what I got, you can simply use a closure to pass the UIButton tap event back to the UIViewController.
Example:
1. Custom UICollectionViewCell
class MyCell: UICollectionViewCell
{
var tapHandler: (()->())?
#IBAction func myButton_touchUpInside(_ sender: UIButton)
{
tapHandler?()
}
}
2. MyViewController
class MyViewController: UIViewController, UICollectionViewDataSource
{
//YOUR CODE..
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
cell.tapHandler = {
//Here you can do your custom handling
}
return cell
}
}
Let me know if you face any issues.
Best thing to do is to make a custom protocol for your custom cell class
protocol CustomCellProtocolDelegate {
func custom(cell: YourCellClass, hadButton: UIButton, pressedWithInfo : [String:Any]?)
}
Make this cell class have this protocol as a peculiar delegate, and to trigger this delegate:
class YourCellClass: UICollectionViewCell {
var delegate : CustomCellProtocolDelegate?
var indexPath : IndexPath? //Good practice here to have an indexPath parameter
var yourButton = UIButton()
init(frame: CGRect) {
super.init(frame: frame)
yourButton.addTarget(self, selector: #selector(triggerButton(sender:)))
}
func triggerButton(sender: UIButton) {
if let d = self.delegate {
d.custom(cell: self, hadButton: sender, pressedWithInfo : /*Add info if you want*/)
}
}
}
In your controller, you conform it to the delegate, and you apply the delegate to each cell in cellForItem: atIndexPath:
class YourControllerThatHasTheCollectionView : UIViewController, CustomCellProtocolDelegate {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "identifier", for: indexPath) as! YourCellClass
cell.delegate = self
cell.indexPath = indexPath
return cell
}
func custom(cell: YourCellClass, hadButton: UIButton, pressedWithInfo : [String:Any]?) {
//Here you can process which button was selected, etc.. and apply your changes to your collectionview
}
}
Best practice is to pass the cell's indexPath parameter in the delegate method inside of pressedWithInfo. It saves you the trouble of calculating which cell actually was pressed; hence why i usually add an indexPath element to each of my UICollectionViewCell subclasses. Better yet, include the index inside the protocol method:
protocol CustomCellProtocolDelegate {
func custom(cell: YourCellClass, hadButton: UIButton, pressedAt: IndexPath, withInfo : [String:Any]?)
}
func triggerButton(sender: UIButton) {
if let d = self.delegate {
d.custom(cell: self, hadButton: sender, pressedAt: indexPath!, withInfo : /*Add info if you want*/)
}
}
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
I have a query regarding Collection View in Swift 4.
I have a collection view name it as Category-Collection-View-Controller.
In this, each Category has a unique ID which is coming from the back-end. When I click on the specific category it will navigate to the other collection view which is named as SubCategories-CollectionView-Controller. and display all the information of the "Category-Collection-View-Controller". Please, anyone, explain to me how I pass the "ID" which is coming from the backend as a parameter in Alamofire function.
You can pass the data from one view controller to another with the following ways:
Delegates
Segue
Notification
I will suggest you use segue since you must have performed segue action on category tap.
Coding Example:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// You can use a traditional push view controller method.
//let id = dataSourceArray[indexPath.item]
//let vc = self.storyboard?.instantiateViewController(withIdentifier: "SubCategories-CollectionView-Controller"") as! SubCategories-CollectionView-Controller
//vc.id = id
// self.navigationController?.pushViewController(vc, animated: false)
// Get the category id and pass it through segue
performSegue(withIdentifier: "identifier", sender: id)
}
and in prepare for segue method, you can just pass this id to next view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "identifier" {
let senderID = sender as? Int
let vc = segue.destination as? SubCategories-CollectionView-Controller
vc.id = senderID
}
}
Declare id in CategoryCollectionViewController and than fetch subcategory details using this id.
class CategoryCollectionViewController: UIViewController {
var id : Int?
override func viewDidLoad() {
Almofire.FetchSubcategory(catid:id) { catgorydata in
subcategoryCollectionView.reloadData()
}
}
}
in your categoryCollectionView Controller pass Category id from CollectionView didselect methods
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let categoryID = categoryArray[indexPath.row]
let subcategorycontroller = SubCategoryController()
subcategorycontroller.id = categoryID
self.present(subcategorycontroller, animated: false, completion: nil)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let id = dataArray[indexPath.item]
let vc = self.storyboard?.instantiateViewController(withIdentifier: "SubCategories-CollectionView-Controller") as! SubCategories-CollectionView-Controller
vc.id = id
self.navigationController?.pushViewController(vc, animated: false)
}
Here dataArray is your array of data through which you are showing data in Category-Collection-View-Controller. Now when you user tap on it you can get the data in the didselectitem method and pass it to next controller.
Here is what I want to do (UI is showing down below), I want to perform different segue when I taped different cell in it. But I don't know what component I should use to get this done
I think to use the Collection View Controller, but it seems doest support static cells, I want to use Customized UIVIew to do this, but it couldn't be dragged to another VC in StoryBoard, should I adopt some sort of Protocols of something to do this?
Please answer in Swift
After been helped
Turns out I just need to show the ViewController and haven't need segue yet, so here is the code.
let categories: [(name: String, pic: String, identifier: String)] = [
("人物", "Wilson_head", "CharactersVC"),
("动物", "Pig", "MobsVC"),
("植物", "Bamboo_Patch", "PlantsVC"),
("食谱", "Crock_Pot_Build", "RecipesVC")
]
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let currentCategory = categories[indexPath.row]
let destVC: UIViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: currentCategory.identifier)
navigationController!.pushViewController(destVC, animated: true)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
if(indexPath.row == 0)
{
let objstory = self.storyboard?.instantiateViewController(withIdentifier: “your Story bord identifier”) as! Your View_ViewController
self.navigationController?.pushViewController(objstory, animated: true)
}
}
Collection View Controller doesn't support static cell.
But you can add static cell programmatically.
set your cell data and target(segue id) for each cell.
ex:
cellData[0].segue = "segueToHuman"
cellData[1].segue = "segueToAnimal"
add segues depend on your view controller(not on cell) like below:
create segue
segue setting
perform particular segue in didSelectItemAt indexPath
let segueID = cellData[indexPath.row].segue
performSegue(withIdentifier: segueID, sender: nil)
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.