I'd like to use a CollectionView methods from another swift file instead of it's ViewController for some reason.
I have this in my ViewController:
#IBOutlet weak var collectionView: UICollectionView!
var broadcastColletionView = BroadcastCollectionView()
override func viewDidLoad() {
super.viewDidLoad()
broadcastColletionView = BroadcastCollectionView(eventItems: eventItems,collectionView: collectionView, broadastObject: broadastObject)
collectionView.dataSource = broadcastColletionView
collectionView.delegate = broadcastColletionView
}
And I have BroadcastCollectionView.swift which contains the CollectionView delegate methods:
class BroadcastCollectionView: NSObject,UICollectionViewDelegate, UICollectionViewDataSource {
var eventItems = [Eventtype]()
var alreadyChecked: Bool = false
var cellHistory: IndexPath = []
var collectionView: UICollectionView!
var broadastObject = Broadcast()
init(eventItems: [Eventtype],collectionView: UICollectionView,
broadastObject: Broadcast) {
self.eventItems = eventItems
self.collectionView = collectionView
self.broadastObject = broadastObject
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return eventItems.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "brCollectionView", for: indexPath) as! BroadcastCollectionViewCell
self.collectionView = collectionView
cell.eventImage.image = eventItems[indexPath.row].image
cell.eventType = eventItems[indexPath.row]
let tap = UITapGestureRecognizer(target: self, action: #selector(collectionViewTapped))
tap.numberOfTapsRequired = 1
cell.addGestureRecognizer(tap)
return cell
}
#objc func collectionViewTapped(sender: UITapGestureRecognizer) {
if let indexPath = self.collectionView?.indexPathForItem(at: sender.location(in: self.collectionView)) {
let cell : BroadcastCollectionViewCell = collectionView.cellForItem(at: indexPath)! as! BroadcastCollectionViewCell
print("item index")
} else {
print("collection view was tapped")
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("selected row is",indexPath.row)
}
I don't really understand why the delegate methods not called if I setted the collectionView.delegate and dataSource to the BroadcastCollactionViewclass. Please don't make me explain why would I like to separate this CollectionView it's not part of the question.
After setting datasource and delegate for the collection view in viewDidLoad() method, reload the collection view:
collectionView.reloadData()
This another class that you created is called CustomDataSource you can find a tutorial here
Try calling
collectionView.reloadData() after setting the dataSource and delegate.
and Make sure eventItems is not empty.
Hope it Helps!
Apart from the issue of not working in your case which could be due to not configuring the collection view properly which can be resolved using the answers you got above from other users, There is an approach for your question of "Use CollectionView methods from another swift file"
You can make use of the concept called "extension", I will explain you how.
Create a new class for handling the collection view methods as follows,
class MyDataSource: NSObject {
var eventItems: Array<Any>?
init(events: Array<Any>?) {
self.eventItems = events
}
}
extension MyDataSource: UITableViewDataSource, UITableViewDelegate {
// MARK: - Table view data source methods
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier")
return cell
}
// Add additional data source methods & delegate methods as per your need
// MARK: - Table view delegate methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Perform your action
// Use delegate methods(protocols) to do your actions from the viewController class
}
}
And in the viewController file assign the datasource & delegate method for collection view as follows,
class ViewController: UIViewController {
var datasource: MyDataSource?
override func viewDidLoad() {
super.viewDidLoad()
// Initialize datasource & delegate for collectionView with the extension datasource
datasource = MyDataSource(events: EVENTITEMS_ARRAY)
collectionView?.dataSource = datasource
collectionView?.delegate = datasource
}
}
NOTE
Update the data types & collection view properties like cell identifier as per your need.
Related
I am trying to implement a collection view inside a table view cell.
My table view cell is a xib, and I've dragged a collection view into it.
Then, I created a class and xib for the collection view cell:
class MyCollectionViewCell: UICollectionViewCell {
var media: Image?
#IBOutlet weak var imageView: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
func initialize(media: PostImage) {
self.media = media
if let url = media.url {
imageView.kf.setImage(with: URL(string: url))
}
}
}
And I've given the xib the class "MyCollectionViewCell" and also given it the identifier "MyCollectionViewCell".
Then, in my table view cell class, I have done the following:
class MyTableViewCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource {
var post: Post!
#IBOutlet weak var title: UILabel!
#IBOutlet weak var mediaCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
mediaCollectionView.delegate = self
mediaCollectionView.dataSource = self
let mediaCollectionViewCell = UINib(nibName: "MyCollectionViewCell", bundle: nil)
mediaCollectionView.register(mediaCollectionViewCell, forCellWithReuseIdentifier: "MyCollectionViewCell")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCollectionViewCell", for: indexPath as IndexPath) as? MyCollectionViewCell else {
fatalError("The dequeued cell is not an instance of MyCollectionViewCell.")
}
let media = post.images[indexPath.row]
cell.initialize(media: media)
return cell
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func initialize(post: Post) {
self.post = post
title.text = post.title
self.mediaCollectionView.reloadData()
}
}
The problem is, the collection view never shows when I run this. The title label text shows fine, but the collection view does not show, and I don't know what I'm doing wrong.
cellForItemAt doesn't even seem to get called, because when I add print("hello") at the top of the function, it never shows up in the console.
What am I doing wrong?
I think the problem is the height of the collection view is very small that it isn't shown.
Try to set the height for the table view cell:
func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat {
return 100
}
where the 100 should be bigger than the collection view
I'm trying to load in some data in a feed based off of a user's data through Firebase, however, it isn't working. My application is currently organized so that the user enters on CustomTabBarController and is verified for login and that a profile has been created, retrieving it if needed. Then, I send the user to the feed by:
// Go to home feed
let navController = self.viewControllers![0] as? UINavigationController
let feedVC = navController?.topViewController as? FeedViewController
if feedVC != nil {
feedVC!.getProfilePhotos()
}
My first question - is this the correct way to load in the FeedViewController on a CustomTabBarController? I also make a call to get the profile data ahead of time.
The getProfilePhotos is a set of delegate and protocols, and returns the following way (I have verified that it correctly retrieves photoURLs). The debugger then thinks that there are no more methods to fire after this.
func feedProfilePhotosRetrieved(photoURLs: [String]) {
// Set photos array and reload the tableview
self.photoURLs = photoURLs
cardCollectionView.reloadData()
}
Here is my FeedViewController class, it's properties and viewDidLoad()/viewDidAppear()
var feedModel = FeedViewModel()
var associates = UserProfile.shared().associates
var photoURLs = [String]()
#IBOutlet weak var cardCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
associates = UserProfile.shared().associates
feedModel.delegate = self
cardCollectionView.delegate = self
cardCollectionView.dataSource = self
cardCollectionView.register(CardCollectionViewCell.self, forCellWithReuseIdentifier: "sectionCell")
cardCollectionView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
getProfilePhotos()
}
This is where I create the cells in the collection view. I put a breakpoint at the declaration of "cell", but it isn't firing.
extension FeedViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return associates.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "sectionCell", for: indexPath) as! CardCollectionViewCell
let card = UserProfile.shared().associates[indexPath.row]
cell.name.text = card.name
cell.poscomp.text = card.title + ", " + card.company
// Photo that we're trying to display
let p = photoURLs[indexPath.row]
// Display photo
cell.downloadPhoto(p)
cell.layer.transform = animateCell(cellFrame: cell.frame)
return cell
} }
Are there any blatantly visible errors that I'm missing? Do I have to call the above function when reloadingData() as well? Thanks for your help and let me know if you need additional information.
The method numberOfItemsInSection should return photoURLs.count.
extension FeedViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoURLs.count
}
I need to present a new ViewController when selecting a UICollectionView Cell and pass the data from the entity used to fill selected cell.
Here is the code used to fill cell data:
let pets = PersistenceManager.shared.fetch(Pet.self)
var _fetchResultsController: NSFetchedResultsController <Pet>?
var fetchResultsController: NSFetchedResultsController <Pet>?{
get{
if _fetchResultsController == nil {
let moc = PersistenceManager.shared.context
moc.performAndWait {
let fetchRequest = PersistenceManager.shared.petsFetchRequest()
_fetchResultsController = NSFetchedResultsController.init(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: nil, cacheName: nil) as? NSFetchedResultsController<Pet>
_fetchResultsController?.delegate = self
do {
try self._fetchResultsController?.performFetch()
}catch {
}
}
}
return _fetchResultsController
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionViewHorizontal.dequeueReusableCell(withReuseIdentifier: "HorCell", for: indexPath) as! PRMainHorizontalCollectionViewCell
if let pet= self.fetchResultsController?.fetchedObjects, indexPath.row < pet.count{
let _pet= fetchResultsController!.object(at: indexPath)
// cell UI goes here
}
return cell
}
I understand I need to use didSelectItemAt, I just don't know what information needs to go in the function. Please let me know of anything else needed to better help answer this question. Thank you.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Added the line below based on karthik's answer. But I am unsure how to implement it.
let selectedObj = fetchResultsController!.object(at: indexPath)
let vc = self.storyboard?.instantiateViewController(withIdentifier: "selectedPetViewController") as! PRSelectedPetViewController
navigationController?.pushViewController(vc, animated: true)
}
I prefer the following architecture:
This is the main controller with data.
For a better understanding, I will simplify the data source.
class ViewController: UIViewController {
// code ...
#IBOutlet var collectionView: UICollectionView!
fileprivate var data = [Pet]()
}
extension ViewController: UICollectionViewDataSource {
// code ...
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HorCell", for: indexPath) as? PRMainHorizontalCollectionViewCell else {
return UICollectionViewCell()
}
let pet = data[indexPath.row]
// TODO: configure cell using pet ...
return cell
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let row = indexPath.row
let pet = data[row]
// TODO: Get the child controller in any way convenient for you.
let childController = ChildViewController()
// With the help of the delegate, we will receive notification of changes pet.
childController.delegate = self
// Thus, we pass the data to the child controller.
childController.pet = pet
childController.indexPath = indexPath
// TODO: Present the view controller in any way convinient for you.
}
}
extension ViewController: ChildViewControllerDelegate {
func saveButtonPressed(_ controller: ChildViewController) {
guard let pet = controller.pet, let indexPath = controller.indexPath else {
return
}
// We save data and reload the cell whose data we changed.
self.data[indexPath.row] = pet
collectionView.reloadItems(at: [indexPath])
}
func cancelButtonPressed(_ controller: ChildViewController) {
// Do something if necessary...
}
}
In addition to the controller, the child controller also provides a delegate protocol for notification of changes.
protocol ChildViewControllerDelegate {
func saveButtonPressed(_ controller: ChildViewController)
func cancelButtonPressed(_ controller: ChildViewController)
}
// This is the controller you want to show after selecting a cell.
// I assume that in the child controller there is a button to save and cancel.
class ChildViewController: UIViewController {
var delegate: ChildViewControllerDelegate?
// The object whose data we are editing in the controller.
var pet: Pet!
// The location of the object in the main controller.
var indexPath: IndexPath!
override func viewDidLoad() {
// TODO: Configure user interface using self.pet
}
#IBAction func saveButtonPressed(_ button: UIButton) {
delegate?.saveButtonPressed(self)
}
#IBAction func cancelButtonPressed(_ button: UIButton) {
delegate?.cancelButtonPressed(self)
}
}
you can follow this to pass information to another view controller.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selectedObj = fetchResultsController!.object(at: indexPath)
// instantiate presenting view controller object
// add one property (manange object) in your presenting viewcontroller
// assign the selected object to that property
// present the view controller
}
I am trying to pass this string from one UICollectionViewCell to UICollectionViewController. I would like the "Let's Get Started!" to go on my navigation title... Below is my code and I can't figure out why the string isn't getting passed.
// UICOLLECTIONVIEWCELL --> This is the first UICollectionViewCell
#objc func getStartedAction() {
let confirmingTapActionButton1 = "Let's Get Started!"
let signUpFlowController1 = SignUpFlowController()
signUpFlowController1.welcomeCellPassedStringForAction1 = confirmingTapActionButton1
}
// UICollectionViewController --> This is the second UICollectionViewController
class SignUpFlowController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
var welcomeCellPassedStringForAction1: String? = nil
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .white
// NAV BAR STUFF BELOW
self.title = welcomeCellPassedStringForAction1
}
You can do this using a protocol
Make a protocol:
protocol NavTitleProtocol{
func setNavTitle(title: String)
}
Conform your CollectionViewController to the protocol and override the setNavTitle method:
extension YourCollectionViewController: NavTitleProtocol{
func setNavTitle(title: String) {
self.title = title
}
}
In your cell, have a delegate property of type NavTitleProtocol:
class YourCollectionViewCell: UICollectionViewCell{
var delegate: NavTitleProtocol?
#objc func getStartedAction() {
let confirmingTapActionButton1 = "Let's Get Started!"
// let signUpFlowController1 = SignUpFlowController()
// signUpFlowController1.welcomeCellPassedStringForAction1 = confirmingTapActionButton1
delegate?.setNavTitle(title: confirmingTapActionButton1)
}
}
Assign your collectionViewController as the delegate when you create the collectionView cell:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YourIdentifier", for: indexPath) as! YourCollectionViewCell
cell.delegate = self
}
When you perform the selector in your cell, the delegate property will be accessed and the method that you have overriden in your CollectionViewController will be called.
First this line
let signUpFlowController1 = SignUpFlowController()
creates a new instance other than the shown one , so you have to use the delegate to catch the presented instance
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
cell.myInstance = self
}
class customCell:UICollectionViewCell {
var myInstance:SignUpFlowController!
#objc func getStartedAction() {
let confirmingTapActionButton1 = "Let's Get Started!"
myInstance.welcomeCellPassedStringForAction1 = confirmingTapActionButton1
}
}
This is a problem that I always stay in dilemma when I creating cells.
Let's say I have a custom cell (PlaceCell) which I'm using in different controllers and collectionviews. It has a label which identify the place name (PlaceNameLabel) and will navigate to place detail when user taps on it.
This doesn't matter controller, collectionviews or wherever cell is used, this is independent of where it is used.
So I have to put the PlaceNameLabel's UITapGestureRecognizer code inside the PlaceCell class.
class PlaceCell: UICollectionViewCell {
#IBOutlet weak var placeName: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
initEventListeners()
}
func initEventListeners() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
action: #selector(handlePlaceNameTouch))
}
func handlePlaceNameTouch() {
// I have to push a new view controller here
}
}
But if I want to push the place detail view controller I need to access the controller. If I want to access the controller I have two options and this is where I stay in dilemma, I have read lots of SO questions answers most of them suggest the second option, but I think the first one is better approach. But I don't know if it's problem to reference the controller inside the cell.
Could you please share your opinion or any other method to handle this problem?
FIRST OPTION
Referencing the controller inside the cell,
extension MyController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlaceCell", for: indexPath) as! PlaceCell
cell.controller = self
return cell
}
}
class PlaceCell: UICollectionViewCell {
var controller: UIViewController?
#IBOutlet weak var placeName: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
initEventListeners()
}
func initEventListeners() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
action: #selector(handlePlaceNameTouch))
}
func handlePlaceNameTouch() {
controller.navigationController.pushViewController(PlaceDetailController(),
animated: true)
}
}
SECOND OPTION
Create protocol and delegate, pass event to controller and do whatever you want,
(I think this is not well, because action is about the cell and I have to create protocol function multiple times because I use this Cell in different Controllers)
protocol PlaceCellDelegate {
func handlePlaceNameTouch()
}
class PlaceCell: UICollectionViewCell {
var delegate: PlaceCellDelegate?
#IBOutlet weak var placeName: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
initEventListeners()
}
func initEventListeners() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
action: #selector(handlePlaceNameTouch))
}
func handlePlaceNameTouch() {
delegate?.handlePlaceNameTouch()
}
}
extension MyController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, PlaceCellDelegate {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlaceCell", for: indexPath) as! PlaceCell
cell.delegate = self
return cell
}
func handlePlaceNameTouch() {
self.navigationController.pushViewController(PlaceDetailController(), animated: true)
}
}
You no need to handle specially to cell selection. No need to use UITapGestureRecognizer or no need implement your own protocol to detect cell selection.
UICollectionView and UITableView has its own protocols for that.
Cell selection delegate for UICollectionView - UICollectionViewDelegate protocol
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
Cell selection delegate for UITableView - UITableViewDelegate protocol
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
Confirm protocol & set delegate on your UIViewController
class viewcontroller : UICollectionViewDelegate {
#IBOutlet weak collectionView: UICollectionView!
func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
}
}
And implement the protocol method inside a view controller, which I mentioned above.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//indexPath - Index path of selected cell.
// Add you cell selection logic here.
}
This method will be triggered when you tap on the cell. Inside this method fetch the model from datasource array. and navigate to detail view based on which cell (model) selected by user.
While both your options are possible, if you are hesitating I would recommend the delegate approach as it is a more "swifty" way of doing it and makes the cell reusable.
Here is a third option using an onTapHandler which doesn't use the delegate pattern and still keeps the cell reusable:
class PlaceCell: UICollectionViewCell {
// A handler called when the button is pressed
var onTapHandler: (() -> Void)?
#IBOutlet weak var placeName: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
initEventListeners()
}
func initEventListeners() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
action: #selector(handlePlaceNameTouch))
}
func handlePlaceNameTouch() {
// Make sure the handler exists, else...
guard let handler = self.onTapHandler else {
return
}
// Execute handler
handler()
}
}
You would then use it like so:
extension MyController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, PlaceCellDelegate {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlaceCell", for: indexPath) as! PlaceCell
cell.onTapHandler = { [weak self] in
self?.navigationController.pushViewController(PlaceDetailController(), animated: true)
}
return cell
}
}