Something that bother me for a long time.
Is there a way to preload UICollectionViewCell ?
something to prepare before loading the data, so the cell will not be create while user is scrolling.
Is there a way to take Full control on WHEN to create the UICollectionViewCell and WHEN to destroy the UICollectionViewCell.
From Apple documentation:
You typically do not create instances of this class yourself. Instead, you register your specific cell subclass (or a nib file containing a configured instance of your class) with the collection view object. When you want a new instance of your cell class, call the dequeueReusableCell(withReuseIdentifier:for:) method of the collection view object to retrieve one.
The cell itself is not a heavy resource, so it makes a little sense to customize its lifetime.
I think that instead of searching for a way to take a control over cell creation, you should ask yourself: why do you want to preload a cell? What kind of heavy resource would you like to preload?
Depending on the answer you can try following optimizations:
If you have complex view hierarchy in your cell, consider refactoring from Autolayout to manual setting frames
If your cell should display results of some complex computations or remote images, you would like to have a separate architecture layer for loading these resources and shouldn't do it in cell class anyway. Use caching when necessary.
You can blackout your screen while you enter into scene and navigate (scroll) to each cell type. When you did scroll each cell you should hide blackout view and present collection view on first cell (top).
It is some kind of workaround. My cells have a lot resources like images, which lag while scrolling collection view.
I can add some actual approach
If you need to preload some data from internet / core data checkout https://developer.apple.com/documentation/uikit/uicollectionviewdatasourceprefetching/prefetching_collection_view_data
tl;dr
first you need your collection view datasource to conform UICollectionViewDataSourcePrefetching in addition to UICollectionViewDataSource protocol
then prefetch your data and store it in cache
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
// Begin asynchronously fetching data for the requested index paths.
for indexPath in indexPaths {
let model = models[indexPath.row]
asyncFetcher.fetchAsync(model.id)
}
}
and finally feed your cell with your cached data
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else {
fatalError("Expected `\(Cell.self)` type for reuseIdentifier \(Cell.reuseIdentifier). Check the configuration in Main.storyboard.")
}
let model = models[indexPath.row]
let id = model.id
cell.representedId = id
// Check if the `asyncFetcher` has already fetched data for the specified identifier.
if let fetchedData = asyncFetcher.fetchedData(for: id) {
// The data has already been fetched and cached; use it to configure the cell.
cell.configure(with: fetchedData)
else
... async fetch and configure in dispatch main asyncd
Also u can approach some workaround using delegate methods to handle willDisplay, didEndDisplay events e.g. https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/1618087-collectionview?language=objc
Related
I've got a collection view with multiple cells created programically. I want to update a variable to a different value when the user taps a specific cell. So eg: Cell 1 is tapped -> var test = "cell1" , cell2 is tapped var test = "cell2". Usually I'd just create an IBAction by dragging from the storyboard but I'm not sure how to do it in this case.
I'm using Swift 3.1
To add interactivity to UITableViews, UICollectionViews, and other kinds of views which display collections of data, you can't use Storyboard actions, as the content is generated dynamically during runtime, and the Storyboard can only work for static content.
Instead, what you need to do is set your UICollectionView's delegate property to an object that implements the UICollectionViewDelegate protocol. One of the methods defined as part of the protocol is the collectionView(_:didSelectItemAt:) method. This method will get called whenever the user selects (taps) a collection view cell with the IndexPath to that cell as an argument. You can update your variable in that method. Just remember to deselect the cell after handling the tap by using the deselectItem(at:) method on your UICollectionView.
There are UICollectionView delegate that you need to implement. It goes like this
did select item at index path will give index path of the cell that was selected. Using indexpath or any other property of the data source array you are using, you can modify the variable value.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if let cell = collectionView.cellForItemAtIndexPath(indexPath) {
//check your condition and modify the variable value depending on index path or any other property you are referring to.
}
}
I segued to a new View Controller. This view controller contains array data from the corresponding CollectionViewCell. How do I change the data to present the previous cell array? Without having to go back to the last view controller and selecting the cell from there? I'm using Swift 3 Below is an image that explains what I mean
The first image is in the first View Controller. The second and third images are what's displayed when either of the first two in the list are tapped. You can go to the next index using the arrow keys in the second View Controller. How do I achieve this functionality?
let dataSource = DataSource()
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if inSearchMode {
return filteredAnimal.count
}
return dataSource.anim.filter{ $0.isDefault! }.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnimIndex", for: indexPath as IndexPath) as! AnimIndex
let animal: Animal
if inSearchMode {
animal = filteredAnimal[indexPath.row]
} else {
animal = dataSource.anim.filter{ $0.isDefault! }[indexPath.row]
}
cell.configureCell(animal)
return cell
}
This is a very common UI pattern. You should search on master/detail. You should be able to find lots of sample projects that give examples of this UI pattern.
Don't think in terms of cells. Think in terms of a model object - a store for your app data. You need a model object that represents the entire list of data you present in your first view controller.
That model object should be reachable from all view controllers that need to display information from it. You can either pass around a reference to the model object or make it a singleton. There are advantages to either approach.
For collection views and table views, which are organized by section and row, an array of arrays of data objects is often a good structure for your model. If your data is in a single section then you can use a single array.
When the user taps on a cell in your first view controller, you would invoke the second view controller (Either from a segue or by manually invoking it.) You'd pass the second view controller the indexPath of the selected object, and might also pass a reference to your model object (or, like I said, you could make the model object a globally accessable singleton, in which case you wouldn't need to pass it.)
Then, in the second (or 3rd) view controller, if the user taps the up/down button, you have access to the model object that stores ALL the data your app is presenting, so you can navigate to the next object from within any view controller. If your data is organzied in sections and rows or rows and columns you might want to add next/previous item methods to your data model so you only have to write that logic once.
I have a program that will search a database and populate the collection view. That currently works. Every item has a "follow" button, and when that button is clicked I need it to perform an action, but I need that action to have access to the indexPath.item. The reason why the program is crashing is because the function for the button is hidden in the collection view method, but this is the only way I can gain access to the indexPath.item. How do I fix the crashing and still have access to the indexPath.item?
func followuser(sender: UIButton){
let pointInTable: CGPoint = sender.convertPoint(sender.bounds.origin, toView: self.collectionview)
if var indexPath :NSIndexPath = self.collectionview.indexPathForItemAtPoint(pointInTable){
print(pointInTable)
}
var relation: PFRelation = (PFUser.currentUser()?.relationForKey("Friendship"))!
//relation.addObject(users[indexPath.item])
print(relation)
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionview.dequeueReusableCellWithReuseIdentifier("searchcell", forIndexPath: indexPath) as! searchcustomcell
cell.follow.addTarget(self, action: "followuser:", forControlEvents: UIControlEvents.TouchUpInside)
I'm not sure what you mean by indexPath.item. NSIndexPath does not have a property item, so that code can't possibly work unless you've created a category of NSIndexPath.
And I'm not sure where you are getting the value for your indexPath variable.
You have a couple of challenges. The first challenge is to the get the indexPath of the cell that contains the button that was tapped.
One way you do that is to use the UICollectionView method indexPathForItemAtPoint. You'd take your button's bounds and use one of the UIView methods for converting points between coordinate systems to convert the button's origin from it's coordinate system to the collection view's coordinate system. You'd then take that converted coordinate and use it in a call to indexPathForItemAtPoint to get the indexPath that contains the button.
Next challenge: What is it you want to look up by indexPath?
Table views and collection views typically store their model data in either a 1 dimensional array (for a single row of items) or a 2 dimensional array (for a grid of cells.)
You need to take the index path and get either the row or the row and section and use that to index into your model to fetch whatever it is you need to fetch.
However we can't help you with that until you tell us how your collection view is organized and how the model that represents your cell data is stored.
So I have this issue where I have to load one entity on two collection views, and one table view. The thing is, primarily I just had to load one entity on the VC where the tableview is located. And that data entity has these parameters for i.e.:
Engine type (diesel or petrol)
Car color
Max.velocity
Year of production
Now, the main point was, to color the cells on the tableview depending on the type of the car engine. So, if the data Entity "Car" has a Bool value of "isEngineDiesel" = true, then the cell would be orange, if false, then it would be light blue. And this worked just fine, a simple if statement on the table view delegate method for loading such cells. But, now I had to implement another VC which has two collection views, in which, the first one loads ONLY Diesel engine Car entity's, and the other Petrol type.
So I guess the issue is already clear here. How can I accomplish this? Because after countless hours of experimenting the only idea I had was two make TWO entity's in which the one is DieselCar and the other PetrolCar. But that means changing the complete data structure, and also, instead of one table view I would actually need two, which doesn't seem like a good idea due the fact that it would "overflow" with all the data there is.
So...any ideas gents?
EDIT:
So far I've only managed to get the cell titles, but the return value of number of cells is still a mystery on how to solve.
The Code for collection view delegate:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! CollectionViewCell
if let carData = fetchedResultsController?.objectAtIndexPath(indexPath) as? Car {
cell.layer.cornerRadius = 20
cell.layer.masksToBounds = true
if collectionView == collectionViewDiesel {
if carData.isEngineDiesel == true {
cell.backgroundColor = UIColor(netHex: 0x8DF060)
// Display the cell name
cell.cellTitle.text = carData.cellTitle
}
}else if collectionView == collectionViewPetrol {
if carData.isCarDiesel == false {
cell.backgroundColor = UIColor(netHex: 0xEB9B2D)
// Display the cell name
cell.cellTitle.text = carData.cellTitle
}
}
}
return cell
}
And this is the method I need answered:
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
On the other VC you need data structure for two collection views so I think make a dictionary from your data base on the isEngineDiesel if true add objects in one array and if false in another array then set those arrays for the respective diesel and petrol keys .
In you collection view delegate there always a UICollectionView argument place check on that (you can use tags or even == operator if you have outlet) to load the data from dictionary base on keys .
I am displaying data in a collection view, I know how to pass the data on with prepareForSegue function but am trying to have the app determine which segue to use depending on the cell property data. (Each segue goes to a different view controller to display relevant information.)
For e.g.
If the cell.type is equal to "1" then perform segueOne if it is of type "2" then perform segueTwo.
I was trying to do something like this;
func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CollectionViewCell
if cell[indexPath].type = "1" {
performSegueWithIdentifier("showPage1", sender: self)
} else if self.cell[indexPath].type = "2" {
performSegueWithIdentifier("showPage2", sender: self)
} else { println("error when selecting cell to segue") }
}
However with this I get an error;
'CollectionViewCell' does not have a member named Subscript
Has anybody got any ideas ?
Assuming the items in your collection view can be re-arranged (or might be some time in the future), the indexPath will not be sufficient to give you the information which cell was selected. Thus, IMO your idea to give the cell a property is a feasible one.
The easiest "quick and dirty" way is to simply hardcode the segue identifier string into your cell. This is not the best design because you are introducing dependencies between app elements that should know of each other.
class MyCell : UICollectionViewCell {
var segue = "DefaultSegue"
}
Now calling the appropriate segue is really easy in didSelectItemAtIndexPath...
self.performSegueWithIdentifier(cell.segue, sender:cell)
It would of course be preferable to use an enum. Safer, more readable and better maintainable.
enum Segue : String {
case ToInfo = "SegueToInfo"
case ToLogin = "SegueToLogin"
// etc.
}
The ivar for MyCell would now be var : Segue = SomeDefaultValue and you can call it the same way.
BTW: Regarding your original question please note the following: as has been pointed out, you cannot subscript a cell. (UICollectionViewCell is not a Dictionary, so cell["key"] does not make sense.) Also, I am not a fan of dequeueing the cell in more than one place - instead you could call cellForItemAtIndexPath or do the work in that method in the first place, as I have suggested.
You're trying to index into a UICollectionViewCell, but of course that class is not an array, so you can't 'subscript' it.
My suggestion is to refactor your code. Whatever data you're storing in your cell you can presumably get from your data model, because that's where it originally came from. You are probably putting that in your cell in cellForIndexPath.
If that is the case, then there is no reason you can't get the same data from the same place in your func ... shouldSelectItemAtIndexPath ... -> Bool. I'd suggest doing it there. Your cell should only contain the data it needs to properly render itself to the screen.
See if that helps.