Is there any way to write concise code for below case - ios

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
}
}

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.

Change label text when selecting UICollectionViewCell

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"
}
}

What should I use to make this kind of cell, so I can get each of them to segue to different ViewController?

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)

How to pass a collection view cell's indexPath to another view controller

I'm following a tutorial for making an Instagram-esque app, and the tutorial goes through how to display all data (image, author, likes, etc) all in one collection view. I'm trying to do it a little bit differently so only the images are displayed in the collection view, then if an image is tapped, the user is taken to a different view controller where the image plus all the other data is displayed.
So in my view controller with the collection view (FeedViewController), I have my array of posts declared outside the class (Post is the object with all the aforementioned data):
var posts = [Post]()
Then inside the FeedViewController class, my cellForItemAt indexPath looks like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postCell", for: indexPath) as! PostCell
// creating the cell
cell.postImage.downloadImage(from: posts[indexPath.row].pathToImage)
photoDetailController.authorNameLabel.text = posts[indexPath.row].author
photoDetailController.likeLabel.text = "\(posts[indexPath.row].likes!) Likes"
photoDetailController.photoDetailImageView.downloadImage(from: posts[indexPath.row].pathToImage)
return cell
}
And then I also obviously have a function to fetch the data which I can post if necessary, but I think the problem is because PhotoDetailController doesn't know the indexPath (although I may be wrong).
When I run the app and try to view the FeedViewController I get a crash fatal error: unexpectedly found nil while unwrapping an Optional value, highlighting photoDetailController.authorNameLabel line.
Am I correct in assuming the problem is because the indexPath is available only in the collection view data sources within FeedViewController? If so, how can I pass that indexPath to PhotoDetailController so my code works correctly?
Thanks for any advice!
EDIT: Edited my didSelectItem method:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let photoDetailController = self.storyboard?.instantiateViewController(withIdentifier: "photoDetail") as! PhotoDetailController
photoDetailController.selectedPost = posts[indexPath.row]
self.navigationController?.pushViewController(photoDetailController, animated: true)
}
and cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postCell", for: indexPath) as! PostCell
cell.postImage.downloadImage(from: posts[indexPath.row].pathToImage)
return cell
}
And in PhotoDetailController:
var selectedPost: Post! inside the class, then:
override func viewDidLoad() {
super.viewDidLoad()
self.authorNameLabel.text = selectedPost[indexPath.row].author
self.likeLabel.text = "\(selectedPost[indexPath.row].likes!) Likes"
self.photoDetailImageView.downloadImage(from: selectedPost[indexPath.row].pathToImage)
}
But still getting error use of unresolved identifier "indexPath
EDIT 2: Storyboard
You are getting this nil crash because this photoDetailController is not yet loaded so all its outlet are nil also the way currently you are doing is also wrong.
You need to add the controller code in didSelectItemAt method and perform the navigation.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let photoDetailController = self.storyboard?.instantiateViewController(withIdentifier: "YourIdentifier") as! PhotoDetailController
photoDetailController.selectedPost = posts[indexPath.row]
self.navigationController?.pushViewController(photoDetailController, animated: true)
}
Now simply create one instance property of type Post with name selectedPost in PhotoDetailController and set all the detail from this selectedPost object in viewDidLoad of PhotoDetailController.
Edit: Set your viewDidLoad like this in PhotoDetailController
override func viewDidLoad() {
super.viewDidLoad()
self.authorNameLabel.text = selectedPost.author
self.likeLabel.text = "\(selectedPost.likes!) Likes"
self.photoDetailImageView.downloadImage(from: selectedPost.pathToImage)
}

Current Index associating with wrong data

I have a collection view that scrolls horizontally and each cell pushes to a detail view upon a tap. When I load the app, I have it print the object at index. At first it will load the one cell it is supposed to. But when I scroll over one space it prints off two new ids, and then begins to associate the data of the last loaded cell with the one currently on the screen, which is off by one spot now. I have no clue how to resolve this, my best guess is there is some way to better keep up with the current index or there is something in the viewDidAppear maybe I am missing. Here is some code:
open var currentIndex: Int {
guard (self.collectionView) != nil else { return 0 }
return Int()
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ExpandCell", for: indexPath) as! ExpandingCollectionViewCell
let object = self.imageFilesArray[(indexPath).row] as! PFObject
cell.nameLabel.text = object["Name"] as! String
whatObject = String(describing: object.objectId)
print(whatObject)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let object = self.imageFilesArray[(indexPath as NSIndexPath).row]
guard let cell = collectionView.cellForItem(at: indexPath) as? ExpandingCollectionViewCell else { return }
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "EventDetailViewController") as! EventDetailViewController
nextViewController.lblName = nameData
nextViewController.whatObj = self.whatObject
self.present(nextViewController, animated:false, completion:nil)
}
Does anyone know a better way to set the current index so I am always pushing the correct data to the next page?
The data source that is setting the elements of the cell can keep count of index that can also be set as a property and can be used to retrieve back from the cell to get correct current index.
EventDetailViewController is your UICollectionViewController subclass I assume.
If this is correct then you need to implement on it:
A method that tells the collection how many items there are in the datasource.
//1
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1 // you only have 1 section
}
//2
override func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return array.count// this is the number of models you have to display... they will all live in one section
}
A method that tells the collection which cellView subclass to use for that given row.
-A method that populates the cell with it's datasource.
Follow this tutorial from which I extracted part of the code, they convey this core concepts pretty clearly.
Reusing, as per Apple's docs happens for a number of cells decided by UIKit, when you scroll up a little bit 2 or N cells can be dequed for reusing.
In summary, with this three methods you can offset your collection into skipping the one record that you want to avoid.
Happy coding!

Resources