Change label text when selecting UICollectionViewCell - ios

I have a class in which I define a CollectionView which I use as a custom TabBar. It has three cells in it, each representing another tab. When I select a tab (which is thus a cell of the CollectionView), I want to update the text of a label inside my view.
In tabs.swift (where all the magic happens to set up the custom tabbar), I added the following function:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let uvController = UserViewController()
uvController.longLabel.text = "Test"
}
In UserViewController, I call it like this:
let ProfileTabs: profileTabs = {
let tabs = profileTabs()
return tabs
}()
It shows all the tabs I want, but when I select it, the label doesn't update. However, when I perform a print action, it does return the value of the label:
print(uvController.longLabel.text)
This returns the value I defined when I set up the label, so I can in fact access the label, but it doesn't update as I want it to do. Any insight on why this is not happening?

let uvController = UserViewController()
This line is the problem.
You instantiate the a new UserViewController instead of referencing to your current UserViewController, so that the label is not the same one. You can print(UserViewController) to check it out, the address should be different.
My suggestion for you can define a protocol in Tabs.swift and make your UserViewController a delegate of it, to receive the update action.
In the same time, let ProfileTabs: profileTabs is not a good naming convention as well, usually custom class should be in Capital letter instead of the variable.

This line - let uvController = UserViewController() creates a new instance of UserViewController which is not on the screen. You need to reference the one already shown to the user. You can do something like this :
The fastest way.
Just pass the instance in ProfileTabs initializer. Something like this:
class ProfileTabs {
let parentViewController: UserViewController
init(withParent parent: UserViewController) {
self.parentViewController = parent
}
// and then change to :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
parentViewController.longLabel.text = "Test"
}
}
The cleaner way. Use delegates.
protocol ProfileTabsDelegate: class {
func profileTabs(didSelectTab atIndex: Int)
}
class ProfileTabs {
weak var delegate: ProfileTabsDelegate?
init(withDelegate delegate: ProfileTabsDelegate) {
self.delegate = delegate
}
// and then change to :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.profileTabs(didSelectTab: indexPath.item)
}
}
And then in UserViewController
extension UserViewController: ProfileTabsDelegate {
func profileTabs(didSelectTab atIndex: Int) {
longLabel.text = "Test"
}
}

Related

Different Classes with Similar Methods

Let's say I am making scrollable pages with a UICollectionView. The pages are all different and are populated by a pages array like the one below:
let pages = [GreenPage(), YellowPage(), OrangePage(), YellowPage(), GreenPage()]
So, to clarify, there would be a page that's green, then followed by yellow, then orange ...
Now, let's say I want to make it so that when one is tapped, it runs a function called tapped() which occurs in each GreenPage(), YellowPage(), and OrangePage().
Now, the only way I see to do this would be the following:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let greenPage = collectionView.cellForItem(at: indexPath) as! GreenPage {
greenPage.tapped()
} else if let yellowPage = collectionView.cellForItem(at: indexPath) as! YellowPage {
yellowPage.tapped()
} else if let orangePage = collectionView.cellForItem(at: indexPath) as! OrangePage {
orangePage.tapped()
}
}
This seems super redundant. Is there another way to do this assuming the tapped function for each class does the same thing?
This is a good example for a protocol. Create it
protocol Tappable {
func tapped()
}
adopt the protocol
class GreenPage : Tappable { ...
class YellowPage : Tappable { ...
class OrangePage : Tappable { ...
This reduces the code in didSelectItemAt considerably
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
(collectionView.cellForItem(at: indexPath) as? Tappable)?.tapped()
}
This is a great time to use protocols. If they all conform to a protocol that has tapped() as a requirement. You then say the array of pages is an array of tour protocol with this:
let pages: [YourProtocol] = [...]
It then your usage be getting the cell and calling tapped()
For more on protocols read this
Also sorry for formatting, I’m on my phone.

Taking the value on didSelectItemAt for indexPath and add it to a delegate / protocol to populate a header cell

Using protocol / delegate & retrieve the data from didSelectItemAt (collectionViews).
// This class have the data
// WhereDataIs: UICollectionViewController
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//Pass some data from the didSelect to a delegate
let test = things[indexPath.item]
guard let bingo = test.aThing else { return }
print("bingo: ", bingo)
That bingo is printing the value that I need. So pretty good right there.
Now, I can't use the method of the protocol inside of that function, that's bad execution so the compiler or Xcode says hey, you will declare the method as a regular method, not the nested way.
//Bridge
protocol xDelegate {
func x(for headerCell: HeaderCell)
}
//This is the class that need the data
//class HeaderCell: UICollectionViewCell
var xDelegate: xDelegate?
//it's init()
override init(frame: CGRect) {
super.init(frame: frame)
let sally = WhereDataIs()
self.xDelegate = sally
xDelegate?.x(for: self)
}
// This extension is added at the end of the class WhereDataIs()
// Inside of this class is it's delegate.
var xDelegate: xDelegate? = nil
extension WhereDataIs: xDelegate {
func x(for headerCell: HeaderCell) {
//Smith value will work because it's dummy
headerCell.lbl.text = "Smith"
// What I really need is instead of "Smith" is the value of didselectItemAt called bingo.
headerCell.lbl.text = bingo
}
}
Hat's off for anyone who would like to guide me in this.
Not using delegates, but it will work
Solved by:
1) go to the controller of the collectionView. make a new variable to store the items.
// Pass the item object into the xController so that it can be accessed later.
//(didSelectItemAt)
xController.newVar = item
//xController class: UICollectionView...
// MARK: - Properties
var newVar: Thing?
Finally in that class, you should have the the method viewForSupplementaryElementOfKind in which you register the HeaderCell and then just add...
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! HeaderCell
header.lbl.text = newVar.whatEverYourDataIs
Little generic but it works. :)
I think you can just add this to the end of your current code in didSelectItemAt indexPath:
guard let header = collectionView.supplementaryView(forElementKind: "yourElementKind", at: indexPath) as? HeaderCell else { return }
header.lbl.text = bingo
collectionView.reloadData()
Edit: Keep everything else for now to ensure you get a good result first.
Let me know if this works, happy to check back.

Is there any way to write concise code for below case

I have an app which has two tabs. In the first BookVC tab, I use UICollectionViewController to show books and in didSelectItemAtIndexPath i call a function that push the BookDetailVC.
And in Bookmark tab, i want to show all books which was bookmarked and when user select certain book, i want to push BookDetailVC. I know it can be achieved by writing the same code as in BookVC. But i don't want to repeat the same code.
I'd tried to make BookmarkVC subclass of BookVC and ended up as showing the same book in both BookVC and BookmarkVC since i'm using the same one instance of UICollectionView from BookVC. Is there any way to override UICollectionView of BookVC or any other approach to solve. Sorry for my bad english. Thanks.
You are taking the wrong approach. The way you describe your bookmarks and your books View controller, it seems to me that they are identical, the only thing that changes is the content.
So, since collection views use data sources all you have to do is change the Data source based on whether you wanna show all books, or only the bookmarked ones.
Added code:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier :"secondViewController") as! UIViewController
self.present(viewController, animated: true)
I think you are doing wrong use just need to reload collection view based on which button is clicked take boolean
isBookMarkCliked:Bool
For Better Readablity create Model For Book
like
class Book {
var title: String
var author: String
var isBookMarked:Bool
init(title: String, author: String, isBookMarked:Bool) {
self.title = title
self.author = author
self.isBookMarked = isBookMarked
}
}
and declare two array globally with Book Model
arrForBooks:[Book] = []
arrForBookMarkedBooks:[Book] = []
Create CollectionViewDelegate methods using extension
extension YourClassVC: UICollectionViewDataSource,UICollectionViewDelegate
{
//MARK: UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
if isBookMarkClicked
{
return arrForBookMarkedBooks.count
}
return arrForBooks.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellIdentifier", for: indexPath) as! YourCellClass
var currentBook:Book = nil
if isBookMarkClicked
currentBook = arrForStoreDetails[indexPath.row]
else
currentBook = arrForBookMarkedBooks[indexPath.row]
//Set data to cell from currentBook
return cell
}
//MARK: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: false)
//Your code to push BookDetailVC
}
}

Swift UICollectionView - Add/remove data from another class

I have a main view controller executed first which looks something like below,
MainViewController
class MainViewController: UIViewController {
var collectionView: UICollectionView!
var dataSource: DataSource!
SomeAction().call() {
self.dataSource.insert(message: result!, index: 0)
}
}
DataSource of the collectionview
class DataSource: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var conversation: [messageWrapper] = []
override init() {
super.init()
}
public func insert(message: messageWrapper, index: Int) {
self.conversation.insert(message, at: index)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return conversation.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let textViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "textViewCell", for: indexPath) as! TextCollectionViewCell
let description = conversation[indexPath.row].description
textViewCell.textView.text = description
return textViewCell
}
}
So, when the MainViewController is executed there is one added to the datasource of the collectionview which works perfectly fine.
Problem
Now, I have another class which looks something like
SomeController
open class SomeController {
let dataSource: DataSource = DataSource()
public func createEvent() {
self.dataSource.insert(message: result!, index: 1)
}
}
When I add some data from the above controller, the conversation is empty which doesn't have the existing one record and throw Error: Array index out of range. I can understand that it is because I have again instantiated the DataSource.
How to add/remove data from other class?
Is it the best practice to do it?
Can I make the conversation as global variable?
The Datasource class had been re initialised with it's default nil value, you have to pass the updated class to the next controller to access its updated state.
How to add/remove data from other class?
You should use class Datasource: NSObject {
And your collection view delegates on your viewcontroller class.
pass your dataSource inside prepareForSegue
Is it the best practice to do it?
Yes
Can I make the conversation as global variable?
No, best to use models / mvc style. Data on your models, ui on your viewcontrollers.
It seems your initial count is 1 but you insert at index 1(out of index)
Use self.dataSource.insert(message: result!, index: 0) insteaded
Or use append.

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.

Resources