Replacing default images in a collection view with user selected photos- swift - 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

Related

add core data image to collection view cell

I have a collection view. I want to fill each cell with the images that are stored in the apps core data. However the cells only fill with the last image that was taken.
not each individual cell benign filled with each individual image.
my code is as follows:
when the user takes a photo it is compressed and stored in core data:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true)
guard let image = info[.editedImage] as? UIImage else {
print("No image found")
return
}
// print out the image size as a test
print(image.size)
//convert image to savable type for coredata
var jpegImageData = image.jpegData(compressionQuality: 1.0)
jegImage = jpegImageData!
//calling function in coredataManger to save image in data format
CoreDataManager.sharedInstance.addImage()
}
The adding to core data function that was activated above:
func addImage() {
let entityName = NSEntityDescription.entity(forEntityName: "ImageData", in: context)
let image2 = NSManagedObject(entity: entityName!, insertInto: context)
image2.setValue(jegImage, forKeyPath: "imageName")
do {
try context.save()
print("image is saved")
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
ive global variables to pass images from one VC to another
//global variables for images saved to core data
var jegImage = Data()
var theImagePulled = UIImage()
Trying to populate the collection view cell with the images stored in core data:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeRoomCell", for: indexPath) as! HomeRoomCell
let name = cartoonArr[indexPath.item]
//setting image shown in the colleciton view equal to the gloabl images stored in core data
let image = theImagePulled
cell.bindData(name: name, image: image)
return cell
}
So the problem is that the cell gets populated but they all populate with the same image. this image is the last image that was added to core data model, even though there are several images in the core data model. I have been playing around with trying to set up an array that stores all these core data images but i can't figure it out. any ideas?
Get rid of the global variables, which are indeed capturing the most recent image.
Use NSFetchedResultsController in your collection view.
If you’re new to Core Data, you might consider passing just a string around and displaying that, first. Then move on to images.

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

UICollectionView cell reuse and online images

I am trying to understand just how UICollectionView cell reuse works.
I am currently implementing a horizontally scrolling UICollectionView with large cells that take up almost the full size of the screen. There are about 100+ cells but you will only ever see ~3 at a time.
As I understand it UICollectionView cell reuse simply maintains a pool of initialized cell objects that way when one cell is out of view it can be cannibalized by a newly viewable cell. That is to say since I am using reuse the collection might only initialize ~3 actual cell objects in memory and I just will switch out their contents.
I am very worried about what this means in the case of custom cells that have image views that are based on images that need to be downloaded. Ideally I would have a scenario where every cells image is only ever downloaded once and it is only downloaded when absolutely necessary.
If there is truly a pool of my custom cell objects then this means that that is totally not happening. As each time a cell comes into view I am starting a completely new download.
How am I supposed to do this right?
The main reason I am asking this is that when scrolling (especially on the initial scroll) I do see some flickering between an image of an old cell and the image the cell is supposed to be displaying. I made a fix but I am fairly sure that it is causing the online images to be downloaded too many times. Am I doing this right?
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as? ImageCell else {
assert(false)
}
let image = data[indexPath.row]
cell.display(title: image.title, imageURL: image.imageURL)
return cell
}
And the cell
public class NewsCell: UICollectionViewCell {
private var title: UILabel = UILabel()
private var imageView: UIImageView = UIImageView()
override public init(frame: CGRect = CGRect.zero) {
super.init(frame: frame)
title.font = UIFont.systemFont(ofSize: 12, weight: .bold)
title.textColor = UIColor.white
title.textAlignment = NSTextAlignment.left
imageView.contentMode = UIViewContentMode.scaleAspectFill
imageView.clipsToBounds = true
contentView.addSubview(imageView)
imageView.addSubview(title)
// Layout constraints
}
public func display(title: String, imageURL: URL?) {
self.imageView.image = nil
self.title.text = title
if let url = imageURL {
downloadImage(from: url)
}
}
func downloadImage(from url: URL) {
getData(from: url) { data, response, error in
guard let data = data, error == nil else {
return
}
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
}
}
func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}
I am worried by the fact that I have to set the image views image to nil on display in order to prevent the flicker. Should I be doing something differently to avoid frivolous downloading of these images or does this look good?
You can use Prefetching Collection View Data to load your images earlier.
You use data prefetching when loading data is a slow or expensive process—for example when fetching data over the network. In these circumstances, perform data loading asynchronously.
It will require some changes of course. You will have to store your images separately and download them when DataSourcePrefetching method will be called. Also, then the image is downloaded, you'll need to check if there is any cell that waiting for that image. So your UICollectionView won't download anything anymore. It will show the image only, or waiting for it to be downloaded.
There is a problem with your current solution. If you'll scroll too fast, you make face a situation when because of reusing, the same UICollectionViewCell is loading a few images at once. And in this case, the user will see only the last one, and you never can tell which one it would be. To avoid this race condition, you can store the image identifier or its URL in the cell so after downloading is finished, it could check if the downloaded image is the right one.
It's okay to set image to nil while it's downloading, but you also can show to the user some UIActivityIndicatorView so he could see that some work is happening.

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

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
}

iOS UIImageView not refreshing automatically

I'm learning iOS programming by following this wonderful tutorial, except I'm targeting iOS 9 and make some small modifications.
In the tableView() function below, I can get thumbnail image downloaded and my handlers invoked, as evident from the console print out of the two logging lines. However, when the app is running (in simulator), I have to click on each table cell to get the image to show up. I tried to see if there is a refresh() or something like that in the UIImageView or the table cell, but found nothing.
How to make the image show up immediately as the data is received?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier)!
let album = self.albums[indexPath.row]
// ... setting up other cell's data, see the tutorial link
// Start by setting the cell's image to a static file
// Without this, we will end up without an image view!
cell.imageView?.image = UIImage(named: "Blank52")
let request: NSURLRequest = NSURLRequest(URL: thumbnailURL)
let urlSession = NSURLSession.sharedSession()
urlSession.dataTaskWithRequest(request, completionHandler: {(data, response, error) -> Void in
print("received thumbnail \(thumbnailURLString)") // reached
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
}
})
}
}).resume()
return cell
}
This part doesn't make sense:
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
This seems to be creating an infinite loop. Every call to cellForRowAtIndexPath will cause another call to cellForRowAtIndexPath.
I think you just meant:
cell.imageView?.image = image
You already know the cell. You just created it before calling this block. You don't need to look it up again.
I figured out a work around. If I also update the text slight, the image shows up right the way without clicking on the table view cell.
cellToUpdate.imageView?.image = image
cellToUpdate.textLabel?.text = "\(album.title) " // a space is added to the end to make a difference and force the cell to be updated
Sounds like a bug in UITableViewCell (or the simulator? haven't tried on a real device) to me.
Why u just try without dispatch_async(dispatch_get_main_queue()) part.
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image
}
})
to
cell.imageView?.image = image
You probably need to cast the cell when you get the cell you try to update.
dispatch_async(dispatch_get_main_queue(), {
print("...dispatched thumbnail image for \(thumbnailURLString)") // reached
if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) as! UITableViewCell {
cellToUpdate.imageView?.image = image
}
})
Swift 5 Just put the following piece of code in the cellForRowAt delegate method after setting the image.
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
}
// Full Method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoCell", for: indexPath) as? PhotoCell else { return UITableViewCell() }
let model = self.arrPhotos?[indexPath.row]
// Below method download image from url using asycn
SwiftHelper.downloadImageFrom(strUrl: model?.url ?? "") {[weak self] (img) in
guard let _ = self, let image = img else { return }
print("ImgDownloaded")
cell.imgView.image = image
// cell.layoutIfNeeded()
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
}
}
return cell
}
To do animation on image you can remove performWithoutAnimation block.