CollectionView Cell and Progress Bar - Progress Bar showing in wrong Cell after scrolling - swift

Ive been searching for a answer to this one for days now and cant seem to figure it out. I have a Collection View with custom cell. When you double tap a cell in the Collection View it will either download a file or delete it if its been downloaded before.
During the download a progress bar displays the progress of the download then displays a small icon in the top left corner. When deleting it removes the icon.
If you download from one cell and delete from another while first download is in progress it works fine but only if both cells were visible within the collection view.
if i download from one cell, then scroll offscreen and delete from a cell that is not in same screen as the cell that is being download from, it removes the corner image as usual then displays the progress bar of the cell that is being download from.
I don't know if this is an error with how i am reusing cells??? It doesn't seem to have anything to do with how i am updating the cell or collection view which works in all cases except after scrolling.
Below is 2 functions that download or delete file:
func downloadDataToDevice(cell: JourneyCollectionViewCell, selectedIndexPath: IndexPath){
let downloadedAudio = PFObject(className: "downloadedAudio")
// save all files with unique name / object id
let selectedObjectId = self.partArray[selectedIndexPath.item].id
let selectedPartName = self.partArray[selectedIndexPath.item].name
let query = PFQuery(className: "Part")
query.whereKey("objectId", equalTo: selectedObjectId)
query.getFirstObjectInBackground { (object, error) in
if error != nil || object == nil {
print("No object for the index selected.")
} else {
//print("there is an object, getting the file.")
downloadedAudio.add(object?.object(forKey: "partAudio") as! PFFile, forKey: selectedPartName)
let downloadedFile = object?.object(forKey: "partAudio") as! PFFile
// get the data first so we can track progress
downloadedFile.getDataInBackground({ (success, error) in
if (success != nil) {
// pin the audio if there is data
downloadedAudio.pinInBackground(block: { (success, error) in
if success {
// reload the cell
self.reloadCell(selectedIndexPath: selectedIndexPath, hideProgress: true, hideImage: false, cell: cell)
self.inProgress -= 1
cell.isUserInteractionEnabled = true
}
})
}
// track the progress of the data
}, progressBlock: { (percent) in
self.activityIndicatorView.stopAnimating()
cell.progessBar.isHidden = false
//cell.progessBar.transform = cell.progessBar.transform.scaledBy(x: 1, y: 1.1)
cell.contentView.bringSubview(toFront: cell.progessBar)
cell.progessBar.setProgress(Float(percent) / Float(100), animated: true)
cell.isUserInteractionEnabled = false
})
}
}
}
func removeDataFromDevice(cell: JourneyCollectionViewCell, selectedIndexPath: IndexPath, object: PFObject) {
let selectedPartName = self.partArray[selectedIndexPath.item].name
// unpin the object from the LocalDataStore
PFObject.unpinAll(inBackground: [object], block: { (success, error) in
if success {
// reduce inProgress
self.inProgress -= 1
self.reloadCell(selectedIndexPath: selectedIndexPath, hideProgress: true, hideImage: true, cell: cell)
}
})
}
and this is how I'm reloading the cell
func reloadCell(selectedIndexPath: IndexPath, hideProgress: Bool, hideImage: Bool, cell: JourneyCollectionViewCell) {
cell.progessBar.isHidden = hideProgress
cell.imageDownloaded.isHidden = hideImage
self.collectionView.reloadItems(at: [selectedIndexPath])
}
----------- EDIT -------------
This is my cellForItem at function. Presently i am using a query to look on local drive and see if the file exists and then adding the corner image if it is. This is the first time i have used a query in this place, usually it is a query at login to populate an array but that is for a more static collection of data than what i am trying to achieve here by letting the user download and delete files.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: JourneyCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! JourneyCollectionViewCell
cell.imageCell.file = self.partArray[indexPath.item].image
cell.imageCell.loadInBackground()
cell.imageCell.layer.masksToBounds = true
// not sure if its good to run a query here as its constantly updated.
// query if file is on LDS and add image to indicate
let cellPartName = self.partArray[indexPath.item].name
let checkQuery = PFQuery(className: "downloadedAudio")
checkQuery.whereKeyExists(cellPartName)
checkQuery.fromLocalDatastore()
checkQuery.getFirstObjectInBackground(block: { (object, error) in
if error != nil || object == nil {
//print("The file does not exist locally on the device, remove the image.")
cell.imageDownloaded.isHidden = true
cell.imageDownloaded.image = UIImage(named: "")
cell.progessBar.isHidden = true
} else {
//print("the file already exists on the device, add the image.")
cell.contentView.bringSubview(toFront: cell.imageDownloaded)
cell.imageDownloaded.isHidden = false
cell.imageDownloaded.image = UIImage(named: "download-1")
}
})
return cell
}

This is a normal feature of "reuse" cells, for efficient memory management purposes. What you need to do is reset the cell values in below function:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
}
By reset, I mean set the cells to their default state, prior to you making any updates such as adding the left corner icon or the status bar.
You need to make sure the arrays that you are feeding the collectionview data from is maintained properly. For example, if you have an array A =[1,2,3] and you delete A[1], then array A needs to be [1,3].

So i tried placing the progress view programatically, i tried prepareForReuse in the custom cell class, neither resolved this issue directly, though i will keep using prepareForReuse as i think its a cleaner way to manage the cell than i had been.
What seems to have worked was relocating the cell within the progressBlock
if let downloadingCell = self.collectionView.cellForItem(at: selectedIndexPath) as? JourneyCollectionViewCell { downloadingCell.progessBar.isHidden = false
downloadingCell.contentView.bringSubview(toFront: downloadingCell.progessBar)
downloadingCell.progessBar.setProgress(Float(percent) / Float(100), animated: true)
downloadingCell.setNeedsDisplay()
downloadingCell.isUserInteractionEnabled = false
}

Related

Replacing default images in a collection view with user selected photos- swift

So I have a collection view that populates three default images on each cell. The user can select images through the new PHPicker, what I want to accomplish is
to replace default images with selected photos, so the first selected image should replace the camera cell and so on ... (see images through links at the bottom)
to display the default images in case user deletes the selected images
currently when I send a new photo to the imgArray it gets displayed in a new cell before the camera cell, as I'm using insert method like so: imgArray.insert(image, at: 0).
My code:
var imgArray = [UIImage(systemName: "camera"), UIImage(systemName: "photo"), UIImage(systemName: "photo")]
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "1", for: indexPath) as! CustomCell
cell.imageView.image = imgArray[indexPath.row]
cell.imageView.tintColor = .gray
cell.imageView.layer.cornerRadius = 4
return cell
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true, completion: nil)
for item in results {
item.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self.imgArray.insert(image, at: 0)
self.collectionView.reloadData()
}
}
}
}
}
I have tried to remove the first item of the array, and then insert new photo like this:
self.imgArray.removeFirst(1)
self.imgArray.insert(image, at: 0)
self.collectionView.reloadData()
but this would work just one time as the code itself says, after that all replacement takes place just in the first cell.
So how can I get to other cells after first replacement? Any other approach that gives the same result will help me alot. Thanks in advance guys!
before selection
after selecting three images
One way to do this is to keep two image arrays, defaultImages of type [UIImage] and inputImages of type [UIImage?]. The defaultImages will hold your camera and photo images, and inputImages will hold images selected by the user. Initialize the images as:
defaultImages = [UIImage(systemName: "camera"), UIImage(systemName: "photo"), UIImage(systemName: "photo")]
inputImages: [UIImage?] = [nil, nil, nil]
To select the correct photo for index index use:
image = inputImages[index] ?? defaultImages[index]
To add a user-input image at index index use:
image: UIImage = ... // Get the image.
inputImages[index] = image
And to delete a user-input image at index index use:
inputImages[index] = nil

Swift: Add more data to TableView on Scroll & Delete No Longer Needed Data from the TableView

I am using parse server.
I have # of social media posts with data that I add to the tableview. Everything loads correctly. However, I have a massive amount of posts (50).
I only want to limit the tableview to have a max of 10, which I successfully use via query.limit.
However, I would like to load the next 10 on the tableView once the user scrolls passed the last 10 of the series.
Additionally, I would like to delete the previous 10 of the series because this will take up too much data on my user's phone.
I would recommend against deleting the current items in the UITableView, as that's an abnormal design pattern and is likely to confuse your users. UITableViews and UICollectionViews are designed to reuse cells in order to be performant.
Here's how I would handle pagination. This example uses a UICollecitonView, but you should be able to apply the same logic with your tableview
var skip = 0
var loading = false
var fullyLoaded = false
var posts = [Post]()
func queryPosts(){
guard !self.fullyLoaded else{return}
guard !self.loading else{return}
self.loading = true
let query = Post.query()
query?.limit = 10
query?.skip = self.skip
query?.findObjectsInBackground(block:{
(posts, error) in
for post in posts{
self.posts.append(item)
}
self.skip = self.posts.count
if posts.count == 0{self.fullyLoaded = true}
self.loading = false
self.collectionView.reloadData()
})
}
func collectionView(_ collectionView:UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell{
let cell = collectionView.dequeueReusableCell(indexPath: indexPath) as PostCell
cell.loadPost(self.posts[indexPath.item])
if self.posts.count == indexPath.item - 1{
self.queryPosts()
}
return cell
}

Search Bar crashing app when inputting characters

I have a UITableView that is populating locations and a Search Bar set as the header of that UITableView.
Whenever certain characters are entered, or a certain amount of characters are entered, the app crashes, giving me no error code.
Sometimes the app crashes after inputting one character, maybe 2 characters, maybe 3, or maybe 4. There seems to be no apparent reason behind the crashing.
The search function properly searches and populates the filtered results, but for no apparent reason, crashes if a seemingly arbitrary amount of characters are inputted.
I have tried using the exception breakpoint tool already, and it is providing me with no new information. I think it has something to do with if there are no search results.
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Locations..."
navigationItem.hidesSearchBarWhenScrolling = false
searchController.hidesNavigationBarDuringPresentation = false
locationTableView.tableHeaderView = searchController.searchBar
searchController.searchBar.sizeToFit()
searchController.searchBar.showsCancelButton = false
searchController.searchBar.barTintColor = UIColor.white
filteredData = locationList
// Sets this view controller as presenting view controller for the search interface
definesPresentationContext = true
locationList = createArray()
// Reload the table
let range = NSMakeRange(0, self.locationTableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.locationTableView.reloadSections(sections as IndexSet, with: .fade)
}
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String) {
filteredData = locationList.filter({( locationName : Location) -> Bool in
return locationName.locationName.lowercased().contains(searchText.lowercased())
})
let range = NSMakeRange(0, self.locationTableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.locationTableView.reloadSections(sections as IndexSet, with: .fade)
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
func locationTableView(_ locationTableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return filteredData.count
}
return locationList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let locationCell = locationTableView.dequeueReusableCell(withIdentifier: "locationCell", for: indexPath) as! locationCell
let location: Location
if isFiltering() {
location = filteredData[indexPath.row]
} else {
location = locationList[indexPath.row]
}
locationCell.setLocation(location: location)
return locationCell
}
The expected result is that the UITableView should populate with filtered results. Instead, it populates them and crashes if too many characters are inputted (usually 1-4 characters).
EDIT 1: I have found through debugging the error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
appears on Line 2 on this block of code:
if isFiltering() {
location = filteredData[indexPath.row]
} else {
location = locationList[indexPath.row]
}
EDIT 2: This is the tutorial I used.
https://www.raywenderlich.com/472-uisearchcontroller-tutorial-getting-started
Seems like you are expecting the tableView to provide YOU with the number of sections... it is supposed to be driven by your own datasource.
Since you are not providing a numberOfSections in your data source I'm assuming it is 1. If all of your rows are in 1 section, all of the nifty reloading you are doing could be greatly simplified.
I suggest you read up on UITableView dataSource protocol at https://developer.apple.com/documentation/uikit/uitableviewdatasource
Reviewing the tutorial you are reading, it seems it is using a reloadData() which forces the tableView to ignore previous number of rows and reload its content with a new number of rows. And based on your findings so far, I would assume that is part of the root cause, with the tableview wrongly assuming a pre-determined number of rows and attempting to retrieve cells that are no longer within range.

How to give Image from another thread?

I have a code for getting user pic:
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.userPic.image = UIImage(data: data)
}
When I'm using it, tableView lagging at scrolling.
Please help me to put this code in another thread.
Here is a good sample provided by Apple, that you can adapt for your needs:
Prefetching collection view data
Basic idea is to create AsyncFetcher for your images and put image creation code to separate operation.
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 {
// There is no data available; clear the cell until we've fetched data.
cell.configure(with: nil)
// Ask the `asyncFetcher` to fetch data for the specified identifier.
asyncFetcher.fetchAsync(id) { fetchedData in
DispatchQueue.main.async {
/*
The `asyncFetcher` has fetched data for the identifier. Before
updating the cell, check if it has been recycled by the
collection view to represent other data.
*/
guard cell.representedId == id else { return }
// Configure the cell with the fetched image.
cell.configure(with: fetchedData)
}
}
}
return cell
}
But in your case you should use Table View prefetching
I can confirm that this approach works and (when done right) results smooth scrolling and good UX

How to make a prototype cell 'disappear' when it is 'empty' (not clickable)

I am making an application where the user can see certain items/users within his x km radius (much like Tinder where you can set the radius of girls/guys you want to see in your area). So in my cellForRowAtIndexPath function I am determining whether a cell can be shown.
If he is in the radius, the event is shown. If the location is too far away, it shouldn't be using a cell.
My current code just hides the cell, but it is still clickable. I want it to NOT use a cell in the first place, but I couldn't find how to do it. Any ideas?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> AllGeneralMeetsTableViewCell {
//get location from backend
let locLati = object?["coordLat"] as? Double
let locLongi = object?["coordLong"] as? Double
let currentLocation:CLLocation = CLLocation(latitude: localLati , longitude: localLongi)
let meetLocation:CLLocation = CLLocation(latitude: locLati! , longitude: locLongi!)
let meters:CLLocationDistance = currentLocation.distanceFromLocation(meetLocation)
// make distance in km
let distInKm = meters/1000
//get distance that user has set in his settings
let cell = tableView.dequeueReusableCellWithIdentifier("GeneralMeetsIdentifier") as! AllGeneralMeetsTableViewCell!
if (distInKm <= settingsKm) {
// Extract values from the PFObject to display in the table cel
if let title = object?["title"] as? String {
cell?.titleCell?.text = title
}
if let message = object?["message"] as? String {
cell?.messageCell?.text = message
}
if let image = object?["image"] as? PFFile {
image.getDataInBackgroundWithBlock({ (imageData: NSData?,error: NSError?) -> Void in
if (error == nil) {
let image1 = UIImage(data:imageData!)
cell.imageCell.image = image1
}
})
}
return cell
}
else {
return cell
}
}
}
The data is being returned by the following query
override func queryForTable() -> PFQuery {
let query = PFQuery(className: self.parseClassName!)
let FBID = myUser.objectForKey("facebookID")!
query.whereKey("facebookID", equalTo: FBID)
query.whereKey("private", equalTo: "false")
return query
}
In your cellForRowAtIndexPath-
if cell is to be displayed {
tableView.rowHeight = 120
//Replace 120 with desired rowHeight
} else {
tableView.rowHeight = 0
}
Hope this helps :-)
You want to make the cell disappear when there is no data. If I am right, then you can use this library called as 'DZNEmptyDataSet' to display some images telling the user that there is no data to load. Use Cocoapods to install it, or just drag the files and create a bridging header. The usage is pretty straightforward as well - just follow the documentation at the GitHub.
The proper solution is to remove the data from your data source so that it won't be displayed in the first place. But sometimes this is a lot of work.
If you just want to hide a cell you can give it a zero height using the tableView(_:heightForRowAtIndexPath:) delegate method:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == 1 {
return 0.0
}
return tableView.rowHeight
}
Parse provides a type PFGeopoint and it is even supported in the Query, with Parse framework you can also obtain location the same way you download stuff from database, this way you can download only those posts that are available for the user in his range...
PFGeoPoint.geoPointForCurrentLocationInBackground { (geoPoint: PFGeoPoint?, error: NSError?) -> Void in
if error == nil { //asynchronous, U have access to GPS location
// create Parse Query
let query: PFQuery = PFQuery(className: "Post")
// 5km range
query.whereKey("gps", nearGeoPoint: geoPoint, withinKilometers: 5.0)
//rest of query....
} else { // dont have access to GPS or whatever
print("Eror location: \(error)")
}
}