Save tableViewCell in an array, Cache UITableViewCell in an array - ios

I am developing a tableView where each cell consists of AVPlayer and has the height of view.frame.size.height and paging is also enabled. so essentially it will work similar to tiktok application feed. The problem I am having is that I've to destroy AVPlayer in prepareForReuse to show new video when a cellForRow is called otherwise if I don't destroy it and user scroll fast, the older video appear for a second and if I destroy player each time before using then AVPlayer takes a second to load and in between show black screen. It works but the result is not elegant.
So is there any way I can preload cells and save them in an array. Like an array which will consist of three independent cells. and we can change the value in them when a user scroll
For example
[] represents cell on screen
0 [1] 2
array[1] would always be the cell on screen
array[0] previous
array[2] next
if user scroll down then
array[0] = array[1]
array[1] = array[2]
array[2] = create next one (proactively)
if user scroll up then
let array1 = array[1]
array[1] = array[0]
array[0] = array1
array[2] = create new one

This sounds more like a job for a collection view. You should be able to achieve the smooth loading you want by prefetching and populating the cell for display once the data is received.
https://developer.apple.com/documentation/uikit/uicollectionviewdatasourceprefetching/prefetching_collection_view_data
Prefetching Collection View Data
Load data for collection view cells before they are displayed.
A collection view displays an ordered collection of cells in
customizable layouts. The UICollectionViewDataSourcePrefetching
protocol helps provide a smoother user experience by prefetching the
data necessary for upcoming collection view cells. When you enable
prefetching, the collection view requests the data before it needs to
display the cell. When it’s time to display the cell, the data is
already locally cached.
The image below shows cells outside the bounds of the collection view
that have been prefetched.
If this doesn't do what you are looking for, please post your code.

Update:
I spent a lot of time trying to solve this problem with tableView but was unable to do so. I ended up making a custom tableview with scrollview which have the similar implementation asked in the question. So it is doable with scrollview to preload cells. Code provided below is just for reference
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//Condition for first cell
if currentIndex == 0 && (scrollView.contentOffset.y > view.frame.minY) {
//pageIndex will only be one if the user completly scrolls the page, if he scroll half a returns it will remain 0
if pageIndex == 1 {
//so if the user completely scroll to page 1 the change the previous index to 0
//new current index will be 1
//call scrollForward which will prepare the cell for index 2
previousIndex = currentIndex
currentIndex = Int(pageIndex)
if shouldCallScrollMethods {
scrollForward()
}
guard let currentCell = getCellOnScreen() else { return }
currentCell.refreshBottomBarView()
}
//Condition for rest of the cells
} else {
//this condition checks if the user completly scroll to new page or just drag the scrollview a bit gets to old position
if (pageIndex != currentIndex) {
//Update the previous and current to new values
previousIndex = currentIndex
currentIndex = Int(pageIndex)
//Checks if the user is scroll down the calls scrollForwad else scrollBackward
if shouldCallScrollMethods {
if currentIndex > previousIndex {
scrollForward()
} else {
scrollBackward()
}
}
guard let currentCell = getCellOnScreen() else { return }
currentCell.refreshBottomBarView()
}
}
}
private func scrollForward() {
//print("scrollForward")
addCell(at: currentIndex + 1, url: getPlayerItem(index: currentIndex + 1))
removeCell(at: currentIndex - 2)
}
private func scrollBackward() {
//Condition to check if the element is not at 0
//print("scrollBackward")
addCell(at: currentIndex - 1, url: getPlayerItem(index: currentIndex - 1))
removeCell(at: currentIndex + 2)
}

Related

swift iOS filter on array containing uitableviews

func convertPointToIndexPath(_ point: CGPoint) -> (UITableView, IndexPath)? {
if let tableView = [tableView1, tableView2, tableView3].filter({ $0.frame.contains(point) }).first {
let localPoint = scrollView.convert(point, to: tableView)
let lastRowIndex = focus?.0 === tableView ? tableView.numberOfRows(inSection: 0) - 1 : tableView.numberOfRows(inSection: 0)
let indexPath = tableView.indexPathForRow(at: localPoint) ?? IndexPath(row: lastRowIndex, section: 0)
return (tableView, indexPath)
}
return nil
}
So i got this method, which converts a CGPoint into the indexPath of the given uitableView. I struggle with the filter-Method on the array which contains uitableViews.
I got an array outside of this method which contains any number of uitableViews. For example:
public var littleKanbanColumnsAsTableViews: [UITableView] = []
So i got to make a change inside of the method. Like this:
if let tableView = littleKanbanColumnsAsTableViews.filter({ $0.frame.contains(point)}).first { ... }
Now when i click on any tableView on the gui, i track the coordinates of the point and transform it on the belonging tableview with the frame.contains(point) method.
My problem is that the filter is not working, it always gives me the first tableView back, no matter which tableview is clicked. Why it doesn't work with my littleKanbanColumnsAsTableViews-Array?
One hint:
let tableView = littleKanbanView.littleKanbanColumnsAsTableViews[3]
When i indexing its direct then it works. But i want it depending on which tableView is containing the clicked point.
Here is my array with the tableViews, in this case the array contains 5 tableviews.
array containing tableviews
Now i want to filter the tableView out of them, which includes the point from tapping on this tableView. How can i achieve this?
For more understanding, i add the ui, here is it:
UI of my app
When i click on this tableView it works, because it is the first element in my array of tableViews. So for this case the convertPointToIndexPath-Method is working.
But when i scroll horizontally to the second tableView for example and click on that, it doesn't work. Because I think the method gives me always the first element back, but i thought it filters it with the given condition.
What is the problem, why doesn't work the condition{ $0.frame.contains(point)}? It have to localize the tableView when the coordinates of the point are tracked.
Preferred Solution:
In this case the moment the first satisfying condition is met, the rest of the elements are not traversed.
if let tableView = littleKanbanColumnsAsTableViews.first(where: { $0.frame.contains(point) }) {
}
Not so efficient solution:
In this case all the elements in the array are traversed to build an array of table views that satisfy the condition. Then the first element of that filtered array is chosen.
if let tableView = (littleKanbanColumnsAsTableViews.filter{ $0.frame.contains(point)}).first {
}

Adding new data to the top of a UICollectionView without animation

I have a UICollectionView that is displaying data that has been saved locally to the device. Once this data is shown, I call to the server to retrieve any new data.
If the server has new data, I need to insert it at the beginning of the array datasource since it is the newest data. I then need to refresh the UICollectionView so that it knows new data has been inserted in the beginning / at the top.
This all works fine, but the problem is that an animation happens and it is noticeable to the user. If the user has scrolled down into some of the cached data, and then the new data is added to the top, the cells they are seeing on screen will reload/animate.
I need to keep all of the above just without the animation. The new cells should be added at the top without changing any of the current cells the user is viewing.
Here is the code I am using to insert and handle the new data:
self.collectionView.performBatchUpdates({
var index = 0
var indexPaths = [NSIndexPath]()
for newObject in objects {
// Update datasource
self.datasource.insert(newObject, atIndex: index)
// Create index paths for new objects
let indexPath = NSIndexPath(forItem: index, inSection: 0)
indexPaths.append(indexPath)
// Update index
index += 1
}
self.collectionView.insertItemsAtIndexPaths(indexPaths)
}, completion: { (completed) in
})
I have tried wrapping this code in a UIView animation block with a duration of 0, in a UIView.performWithoutAnimation function, and have also implemented the following in my UICollectionViewFlowLayout subclass:
public override func initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return nil
}
None of this works. The animation always happens. The cells the user is viewing flash / animate / etc. when the new data is inserted and added.
How can I accomplish this without an animation?

How to detect when UITableView has finished loading all the rows? [duplicate]

This question already has answers here:
How to detect the end of loading of UITableView
(22 answers)
Closed 6 years ago.
I need to call a function after the UITableView has been loaded completely. I know that in most cases not every row is displayed when you load the view for the first time, but in my case, it does as I only have 8 rows in total.
The annoying part is that the called function needs to access some of the tableView data, therefore, I cannot call it before the table has been loaded completely otherwise I'll get a bunch of errors.
Calling my function in the viewDidAppear hurts the user Experience as this function changes the UI. Putting it in the viewWillAppear screws up the execution (and I have no idea why) and putting it in the viewDidLayoutSubviews works really well but as it's getting called every time the layout changes I'm afraid of some bugs that could occur while it reloads the tableView.
I've found very little help about this topic. Tried few things I found here but it didn't work unfortunately as it seems a little bit outdated. The possible duplicate post's solution doesn't work and I tried it before posting here.
Any help is appreciated! Thanks.
Edit: I'm populating my tableView with some data and I have no problems with that. I got 2 sections and in each 4 rows. By default the user only sees 5 rows (4 in the first section, and only one in the second the rest is hidden). When the user clicks on the first row of the first section it displays the first row of the second section. When he clicks on the second row of the first section it displays two rows of the second section, and so on. If the user then clicks on the first row of the first section again, only one cell in the second section is displayed. He can then save his choice.
At the same time, the system changes the color of the selected row in the first section so the users know what to do.
Part of my issue here is that I want to update the Model in my database. If the users want to modify the record then I need to associate the value stored in my database with the ViewController. So for example, if he picked up the option 2 back then, I need to make sure the second row in the first section has a different color, and that two rows in the second sections are displayed when he tries to access the view.
Here's some code :
func setNonSelectedCellColor(indexPath: NSIndexPath) {
let currentCell = tableView.cellForRowAtIndexPath(indexPath)
currentCell?.textLabel?.textColor = UIColor.tintColor()
for var nbr = 0; nbr <= 3; nbr++ {
let aCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: nbr, inSection: 0))
let aCellIndexPath = tableView.indexPathForCell(aCell!)
if aCellIndexPath?.row != indexPath.row {
aCell?.textLabel?.textColor = UIColor.blackColor()
}
}
}
func hideAndDisplayPriseCell(numberToDisplay: Int, hideStartIndex: Int) {
for var x = 1; x < numberToDisplay; x++ {
let priseCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: x, inSection: 1))
priseCell?.hidden = false
}
if hideStartIndex != 0 {
for var y = hideStartIndex; y <= 3; y++ {
let yCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: y, inSection: 1))
yCell?.hidden = true
}
}
}
These two functions are getting called every time the user touches a row :
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let path = (indexPath.section, indexPath.row)
switch path {
case(0,0):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(1, hideStartIndex: 1)
data["frequencyType"] = Medecine.Frequency.OneTime.rawValue
case(0,1):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(2, hideStartIndex: 2)
data["frequencyType"] = Medecine.Frequency.TwoTime.rawValue
case(0,2):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(3, hideStartIndex: 3)
data["frequencyType"] = Medecine.Frequency.ThreeTime.rawValue
case(0,3):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(4, hideStartIndex: 0)
data["frequencyType"] = Medecine.Frequency.FourTime.rawValue
default:break
}
}
I store the values in a dictionary so I can tackle validation when he saves.
I'd like the first two functions to be called right after my tableView has finished loading. For example, I can't ask the data source to show/hide 1 or more rows when I initialize the first row because those are not created yet.
As I said this works almost as intended if those functions are called in the viewDidAppear because it doesn't select the row immediately nor does it show the appropriate number of rows in the second sections as soon as possible. I have to wait for 1-2s before it does.
If you have the data already that is used to populate the tableView then can't you use that data itself in the function? I am presuming that the data is in the form of an array of objects which you are displaying in the table view. So you already have access to that data and could use it in the function.
But if that's not the case then and if your table view has only 8 rows then you can try implementing this function and inside that check the indexPath.row == 7 (8th row which is the last one).
tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
Since all your rows are visible in one screen itself without scrolling you could use this function to determine that all the cells have been loaded and then call your function.

Why am I getting this error: fatal error - array index out of range?

I have 2 view controllers: a cameraVC and a detailVC. The camera is an AVFoundation camera that can capture and then select pictures, which then populates 4 small imageViews on my detailVC. I am trying to swipe between the two VCs so that when you hit 'yes' on the cameraVC, that then adds that to one of the 4 imageViews. I am currently using for-loops to achieve this, and below is the code I have in my detail VCs.
It works when I first load the app and take a few pics, but when I swipe back to my cameraVC and then back to my detailVC, the app crashes and logs error above.
In my code, I am attempting to set the imageViewArray to however many images there are in my imageArray. Then each time I go back to the detailVC, I am to clear my imageView.images and re-assign them. For some reason, it's not working. Any ideas why?
override func viewWillAppear(animated: Bool) {
if imageArray.count == 1 {
imageViewArray += [imageView1]
} else if imageArray.count == 2 {
imageViewArray += [imageView1,imageView2]
} else if imageArray.count == 3 {
imageViewArray += [imageView1,imageView2,imageView3]
} else if imageArray.count == 4 {
imageViewArray += [imageView1,imageView2,imageView3,imageView4]
}
for ima in imageViewArray {
if ima.image != nil {
ima.image = nil
}
}
for (index, imageView) in imageViewArray.enumerate() {
imageView.image = imageArray[index]
}
}
Add the line
imageViewArray = []
in the beginning of your viewWillAppear.
Or change your assignment to the imageViewArray in all 4 branches to something like:
imageViewArray = [imageView1,imageView2,imageView3,imageView4] // removed the "+"
The problem you are encountering here is that the method gets called when the view gets presented the first time AND when some presented view gets dismissed / popped again causing your view to appear again.
The result of that is that the imageViewArray still contains the imageviews from the last iteration. Adding the images again causes the array to get larger.
Assume that you had 3 images the first time, now the second time you have 4 images. Therefore you enter the last else section and add 4 new elements to the array, causing it to have 7 elements while the imageArray still only has 4. Now you iterate over all imageViewArray entries and try to access the imageArray at up to index 6 while the array only contains value until index 3 -> crash.
Note that it was annoying writing this answer because you have too many variables that all sound and look the same imageView, image, imageArray, imageViewArray, ima, imageViewX. Please try to find better names.

Iterate over all the UITableCells given a section id

Using Swift, how can I iterate over all the UITableCells given a section id (eg: all cells in section 2)?
I only see this method: tableView.cellForRowAtIndexPath, which returns 1 cell given the absolute index, so it doesn't help.
Is there an elegant and easy way?
Note: I want to set the AccesoryType to None for all of the cells in a section, programatically, say: after a button is clicked, or after something happends (what happends is not relevant for the question)
I have the reference for the UITableView and the index of the section.
You misunderstand how table views work. When you want to change the configuration of cells, you do not modify the cells directly. Instead, you change the data (model) for those cells, and then tell your table view to reload the changed cells.
This is fundamental, and if you are trying to do it another way, it won't work correctly.
You said "I need the array of cells before modifying them…" Same thing applies. You should not store state data in cells. As soon as a user makes a change to a cell you should collect the changes and save it to the model. Cells can scroll off-screen and their settings can be discarded at any time.
#LordZsolt was asking you to show your code because from the questions you're asking it's pretty clear you are going about things the wrong way.
EDIT:
If you are convinced that you need to iterate through the cells in a section then you can ask the table view for the number of rows in the target section, then you can loop from 0 to rows-1, asking the table view for each cell in turn using the UITableView cellForRowAtIndexPath method (which is different than the similarly-named data source method.) That method will give you cells that are currently visible on the screen. You can then make changes to those cells.
Note that this will only give you the cells that are currently on-screen. If there are other cells in your target section that are currently not visible those cells don't currently exist, and if the user scrolls, some of those cells might be created. For this reason you will need to save some sort of state information to your model so that when you set up cells from the target section in your datasource tableView:cellForRowAtIndexPath: method you can set them up correctly.
For Swift 4 I have been using something along the lines of the following and it seems to work pretty well.
for section in 0...self.tableView.numberOfSections - 1 {
for row in 0...self.tableView.numberOfRows(inSection: section) - 1 {
let cell = self.tableView.cellForRow(at: NSIndexPath(row: row, section: section) as IndexPath)
print("Section: \(section) Row: \(row)")
}
}
Im using same way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not
//get section of interest i.e: first section (0)
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
{
var indexPath = NSIndexPath(forRow: row, inSection: 0)
println("row")
println(row)
//following line of code is for invisible cells
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)
//get cell for current row as my custom cell i.e :roomCell
var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
}
* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible :D
To answer my own question: "how can I iterate over all the UITableCells given a section id?":
To iterate over all the UITableCells of a section section one must use two methods:
tableView.numberOfRowsInSection(section)
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))
So the iteration goes like this:
// Iterate over all the rows of a section
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
var cell:Cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?
// do something with the cell here.
}
At the end of my question, I also wrote a note: "Note: I want to set the AccesoryType to None for all of the cells in a section, programatically". Notice that this is a note, not the question.
I ended up doing that like this:
// Uncheck everything in section 'section'
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?.accessoryType = UITableViewCellAccessoryType.None
}
If there is a more elegant solution, go ahead and post it.
Note: My table uses static data.
Swift 4
More "swifty", than previous answers. I'm sure this can be done strictly with functional programming. If i had 5 more minutes id do it with .reduce instead. ✌️
func cells(tableView:UITableView) -> [UITableViewCell]{
var cells:[UITableViewCell] = []
(0..<tableView.numberOfSections).indices.forEach { sectionIndex in
(0..<tableView.numberOfRows(inSection: sectionIndex)).indices.forEach { rowIndex in
if let cell:UITableViewCell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: sectionIndex)) {
cells.append(cell)
}
}
}
return cells
}
Im using this way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not
//get section of interest i.e: first section (0)
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
{
var indexPath = NSIndexPath(forRow: row, inSection: 0)
println("row")
println(row)
//following line of code is for invisible cells
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)
//get cell for current row as my custom cell i.e :roomCell
var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
}
* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible
You can use
reloadSections(_:withRowAnimation:) method of UITableView.
This will reload all the cells in the specified sections by calling cellForRowAtIndexPath(_:). Inside that method, you can do whatever you want to those cells.
In your case, you can apply your logic for setting the appropriate accessory type:
if (self.shouldHideAccessoryViewForCellInSection(indexPath.section)) {
cell.accessoryType = UITableViewCellAccessoryTypeNone
}
I've wrote a simple extension based on Steve's answer. Returns the first cell of given type (if any) in a specified section.
extension UITableView {
func getFirstCell<T: UITableViewCell>(ofType type: T.Type, inSection section: Int = 0) -> T? {
for row in 0 ..< numberOfRows(inSection: section) {
if let cell = cellForRow(at: IndexPath(row: row, section: section)) as? T {
return cell
}
}
return nil
}
}

Resources