I am working on VirtualTourist project on Udacity and we are required to display images as the images are being refreshed through a Flickr Client. Currently my implementation is such that the refresh will only show once the images are fully downloaded. The project requires such that the images are shown as an when they are downloaded, and while the downloading is happening, an activity indicator is displayed instead.
My code as follows:
#IBAction func newCollectionButtonDidTap(_ sender: Any) {
print("FETCHING NEW COLLECTION...")
DispatchQueue.main.async {
self.photos.removeAll()
self.collectionView.reloadData()
print("Reload Data newColVC")
}
for items in fetchedResultsController.fetchedObjects! {
context.delete(items as! NSManagedObject)
}
loadPhotos()
}
func loadPhotos() {
newColActivityIndicator.startAnimating()
newColActivityIndicator.isHidden = false
newCollectionButton.isUserInteractionEnabled = false
newCollectionButton.alpha = 0.4
FlickrClient.sharedInstance.getPhotos(pin.coordinate.latitude as AnyObject, lon: pin.coordinate.longitude as AnyObject, { (results, error) in
if let error = error {
print(error)
} else {
if results != nil {
for result in results! {
let picture = Photo(pin: self.pin, imageData: result as NSData, context: self.context)
self.photos.append(picture)
}
do {
try self.delegate.stack?.saveContext()
} catch let error as NSError {
print("Error saving context in loadPhotos(): \(error.localizedDescription)")
}
DispatchQueue.main.async {
self.collectionView.reloadData()
self.newColActivityIndicator.stopAnimating()
self.newColActivityIndicator.isHidden = true
self.newCollectionButton.isUserInteractionEnabled = true
self.newCollectionButton.alpha = 1.0
print("Reload Data loadPhotos")
}
}
}
})
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ImageViewCell
cell.activityIndicator.startAnimating()
cell.activityIndicator.isHidden = false
let photo = photos[indexPath.row]
if let photoImage = photo.imageData {
print("PhotoImage present")
DispatchQueue.main.async {
cell.activityIndicator.stopAnimating()
cell.activityIndicator.isHidden = true
cell.imageView?.image = UIImage(data: photoImage as Data)
}
} else {
print("PhotosImage don have")
}
return cell
}
Some advice here is much appreciated, thanks!
So firstly when you tap newCollectionButton you are removing all the data stored in core data as well as removing all those which are being displayed in collection view. So at this moment, collection view doesnot have anything to display. as a result it should be empty showing nothing.
Now, i think your loadPhotos methods returns the collection of photos from flicker which you add it to your local array and coredata. datasource of collection view must be using data from photos array.
You remove the activity indicator and then reload the collection view. At this point, you already have the necessary data for the images to be shown available. So cellForItemAt retrives data from local array, and create image and displays it. Maybe this process is happenning so fast, that your cell.activityIndicator.startAnimating() & cell.activityIndicator.stopAnimating() is executing much faster.
Since your question is unclear, & your loadPhotos fetches collection of photos data, if you want that activityIndicator to be shown in each cell, until all the image gets downloaded, you can
Remove just imagedata present in Photo object stored in local/coredata
make cell to show activity indicator if there is no image data present in each cell
reloadData() so that activity indicator starts
call loadPhotos
After downloading all photos, clear photo array, store downloaded Photos in array and call reloadData() so that all the images gets displayed.
Related
The application has a UITable View which loads dynamic images into cells. Images are downloaded while scrolling and they are converted into UIImages. Those UIImages will be set to the ImageView with help of KingFisher. Simple Cache mechanism has been already implemented but images are flash on scroll until the exact image loads. Code is mentioned below.
Table View Code
extension EmployeeListViewController:UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: EmployeeTableViewCell.self), for: indexPath) as! EmployeeTableViewCell
cell.onBindCell(management: (employeePresenter?.getManagementItem(position: indexPath.row))!)
cell.actionCallBack = self
let empIdNumber = NSString(string: (directoryPresenter!.getManagementItem(position: indexPath.row).agmid))
if let cachedImage = self.cache.object(forKey: empIdNumber) {
cell.profileImage.kf.base.image = cachedImage
} else {
directoryPresenter?.getProfileImage(id: empIdNumber as String) { image in
cell.profileImage.kf.base.image = image
self.cache.setObject(image!, forKey: empIdNumber)
}
}
return cell
}
}
Data Binding inside the Custom cell
I made the image data nil before reuse
override func prepareForReuse() {
profileImage.kf.base.image = nil
}
And set respective data inside onBind() data method.
func onBindCell(management: Management) {
name.text = management.getEmployeeDisplayName()
// Place Holder
profileImage.kf.base.image = UIImage(named: management.details.gender == "M" ? "placeholder_profile_male" : "placeholder_profile_female")
}
But Still, images blink when scrolling down the view for the first time. After the exact image is loaded it won't blick. How to sort the initial blinking issue on first scroll?
What about still using KingFisher but telling it how to interpret/process the downloaded data?
Let's create our own ImageProcessor:
struct CustomBase64Processor: ImageProcessor {
var identifier: String = "com.larme.customBase64Processor"
var encoding: String.Encoding = .utf8
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
print("CustomBase64Processor Processing: \(item)")
switch item {
case .image(let image):
return image
case .data(let data):
// Strip the Base 64 data
if let utf8String = String(data: data, encoding: encoding) {
//Remove Data URI if needed
let base64String = utf8String.replacingOccurrences(of: "data:image\\/.*?;base64,",
with: "",
options: .regularExpression)
// Convert into a Data the Base64 Encoded String
if let cleanedData = Data(base64Encoded: base64String.trimmingCharacters(in: .whitespacesAndNewlines)) {
return KFCrossPlatformImage(data: cleanedData)
} else {
// Just an attempt here, but in fact it could be a gif, and that's not supported.
// Let's rely on KFCrossPlatformImage initilization
return KFCrossPlatformImage(data: Data(base64String.utf8))
}
} else {
//Default image data aren't UTF8 Convertible, and are "ready to process"
return KFCrossPlatformImage(data: data)
}
}
}
}
I was wondering if you had Data URI (since it made me think of related question), ie the content is data:image/png;base64,theBase64Data, so I added a removal of it if needed.
In use, it's like that:
imageView.kf.setImage(with: url,
placeholder: nil,
options: [.processor(CustomBase64Processor())]) { result in
print("KingFisher callback: \(result)")
switch result {
case .success(let imageresult):
print("Success: \(imageresult)")
case .failure(let error):
print("Error: \(error)")
}
}
Be careful, since I made a processor, we should get a data to interpret. So if you want to use other processing (like resizing, etc.) process the base 64 before to have a real working image first.
It's not fully tested, but I guess it could be a good start.
I think you should use a library to load images from url
I have a collection view and want to load images and other data asynchronously from firebase and display them within the cell. However, my current approach displays wrong images to the text data (they simply don't fit) and also, the image in the one specific cell changes few times until it settles down (sometime wrong, sometimes correct).
My code
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainViewCollectionCell", for: indexPath) as! MainViewCollectionViewCell
// issue when refreshing collection view after a new challenge has been created
if (hitsSource?.hit(atIndex: indexPath.row) == nil) {
return photoCell
}
let challengeObject = Challenge(json: (hitsSource?.hit(atIndex: indexPath.row))!)
let group = DispatchGroup()
group.enter()
// async call
self.checkIfChallengeIsBlocked(completionHandler: { (IsUserBlocked) in
if (IsUserBlocked) {
return
}
else {
group.leave()
}
}, challengeObject: challengeObject)
group.notify(queue: .main) {
photoCell.setChallengeLabel(title: challengeObject.title)
// async call
photoCell.fetchChallengeImageById(challengeObject: challengeObject)
photoCell.checkIfToAddOrRemovePlayIcon(challengeObject: challengeObject)
// async call
self.dataAccessService.fetchUserById(completionHandler: { (userObject) in
photoCell.setFullName(userObject: userObject)
photoCell.setStarNumber(challengeObject: challengeObject)
}, uid: challengeObject.organizerId)
// async all
self.dataAccessService.fetchAllParticipantsByChallengeId(completionHandler: { (participationArray) in
photoCell.setParticipationNumber(challengeObject: challengeObject, participationArray: participationArray)
}, challengeId: challengeObject.id)
// resize image to collection view cell
self.activityView.removeFromSuperview()
}
return photoCell
}
...
Just to show you my MainViewCollectionViewCell
class MainViewCollectionViewCell: UICollectionViewCell {
...
public func fetchChallengeImageById(challengeObject:Challenge) {
self.dataAccessService.fetchChallengeImageById(completion: { (challengeImage) in
self.challengeImageView.image = challengeImage
self.layoutSubviews()
}, challengeId: challengeObject.id)
}
and DataAccessService.swift
class DataAccessService {
...
// fetch main challenge image by using challenge id
public func fetchChallengeImageById(completion:#escaping(UIImage)->(), challengeId:String) {
//throws {
BASE_STORAGE_URL.child(challengeId).child(IMAGE_NAME).getData(maxSize: 1 * 2048 * 2048,
completion:({ data, error in
if error != nil {
print(error?.localizedDescription as Any)
let notFoundImage = UIImage()
completion(notFoundImage)
} else {
let image = UIImage(data: data!)!
completion(image)
}
}))
}
...
public func fetchUserById(completionHandler:#escaping(_ user: User)->(), uid:String?) { //
throws{
var userObject = User()
let _userId = UserUtil.validateUserId(userId: uid)
USER_COLLECTION?.whereField("uid", isEqualTo: _userId).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
self.error = error
print(error?.localizedDescription as Any)
} else {
for document in querySnapshot!.documents {
userObject = User(snapShot: document)
completionHandler(userObject)
}
}
})
}
Could anyone tell me what I need to change for being able to fit the text data to the correct image in the cell?
With asynchronous calls to fetch user data, the fact that cells are re-used introduces two issues:
When a cell is re-used, make sure that you do not show the values for the prior cell while your asynchronous request is in progress. Either have collectionView(_:cellForItemAt:) reset the values or, better, have the cell’s prepareForReuse make sure the controls are reset.
In the asynchronous request completion handler, check to see if the cell is still visible before updating it. You do this by calling collectionView.cellForItem(at:). If the resulting cell is nil, then the cell is not visible and there's nothing to update.
Thus:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainViewCollectionCell", for: indexPath) as! MainViewCollectionViewCell
// make sure to initialize these so if the cell has been reused, you don't see the old values
photoCell.label.text = nil
photoCell.imageView.image = nil
// now in your asynchronous process completion handler, check to make sure the cell is still visible
someAsynchronousProcess(for: indexPath.row) {
guard let cell = collectionView.cellForItem(at: indexPath) else { return }
// update `cell`, not `photoCell` in here
}
return photoCell
}
Obviously, if one asynchronous completion handler initiates another asynchronous request, then you have to repeat this pattern.
I have a tableview which acts as a newsfeed. The cells are filled from an array of newsfeed items. I get the JSON from the Server, create newsfeed items from that input and attach them to my newsfeed array. a newsfeed item contains a title, a description and an imageurl string.
At:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "ImageFeedItemTableViewCell1", for: indexPath) as! ImageFeedItemTableViewCell
var item = self.feed!.items[indexPath.row]
if (item.messageType == 1){
cell = tableView.dequeueReusableCell(withIdentifier: "ImageFeedItemTableViewCell1", for: indexPath) as! ImageFeedItemTableViewCell
cell.title.text = item.title
cell.description.text = item.contentText
if (item.imageURL as URL == URL(string: "noPicture")!)
{
cell.picture.image = UIImage(named:"empty")
}
else{
if (item.cachedImage == UIImage(named:"default-placeholder")){
let request = URLRequest(url: item.imageURL as URL)
cell.picture.image = item.cachedImage
cell.dataTask = self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
OperationQueue.main.addOperation({ () -> Void in
if error == nil && data != nil {
let image = UIImage(data: data!)
if (image != nil){
self.feed!.items[indexPath.row].cachedImage = image!
}
cell.picture.image = image
}
})
})
cell.dataTask?.resume()
}else
{
cell.picture.image = item.cachedImage
}
}
}
the cells from the rows get filled with my newsfeeditem data.
But since i keep all my newsfeeditems inside an array, the memory usage is high and gehts higher for each additional newsfeeditem. I want it to work with endless scrolling like twitter, so i wonder how experienced developers tackle this memory issue.
Your problem is in this lines or wherever you try to hold UIImage inside your array, this is really not advised and will cause crash due to memory since image is very large data and not advised to persist it in your RAM with UIImage inside array:
self.feed!.items[indexPath.row].cachedImage = image!
What you need to do is basically after fetch your image from URL, you save it to your app's documents folder and save the name or it's path that can distinct your image in cachedImage (just change the type to string or sth) and refetch it from your app's document folder when you need to show it in cellForRow
Flow: Fetch image -> save to disk and persist path in array -> refetch from disk with the path in cellForRow
Currently I have the following code which saves an object however I am wanting to update/reload the tableview. The button isn't attached to a cell/row it's top right within my navigation controller (plus icon)
Note: Everything is happening within the same scene therefore any events attached to segue where I could reload table data is out of the question.
#IBAction func addWeek (sender: UIButton){
let newnumber:Int = routineWeeks.count + 1
// save data using cor data managed object context
if let managedObjectContext = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext {
week = NSEntityDescription.insertNewObjectForEntityForName("Weeks", inManagedObjectContext: managedObjectContext) as! Weeks
week.weekNumber = newnumber
do {
try managedObjectContext.save()
} catch {
print(error)
return
}
}
//currently not reloading table with new data
tableView.reloadData()
//print information in console
print("end of save function, dismiss controller")
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
// Configure the cell...
cell.textLabel?.text = "Week \(routineWeeks[indexPath.row].weekNumber)"
return cell
}
UPDATE
Thanks Stackoverflow community for pointing me in the right direction.
routineWeeks.append(week)
print("this is a new week:\(week)")
tableView.reloadData()
I do not know if it will help you or not but I had the same problems and I added Refresher (to add "Pull To Refresh" function)
In my class :
override func viewDidLoad() {
super.viewDidLoad()
// Pull to refresh - DEBUT
tableFiches.addPullToRefreshWithAction {
NSOperationQueue().addOperationWithBlock {
//sleep(1)
NSOperationQueue.mainQueue().addOperationWithBlock {
self.loadMyData() // My func here to load data
self.tableFiches.stopPullToRefresh()
}
}
}
// Pull to refresh - FIN
}
func loadMyData(){
// request here
let recupJSON = JSON(value) // my data
if let resData = recupJSON["Data"].arrayObject {
//print("OK2")
self.ArrayFiches = resData as! [[String:AnyObject]] // Attribute json ==> ArrayFiches
}
if self.ArrayFiches.count > 0 {
self.tableFiches.reloadData()
}
}
And when I want reload my data, I use :
tableFiches.startPullToRefresh()
And it works :)
You are not updating routineWeeks array. Update it with your new data before reloading the tableView.
You seem to never add "week" to routineWeeks.
EDIT :
You should reload the datas in routineWeeks (from CoreData) right before your tableView.reloadData.
routineWeeks.append(week)
print("this is a new week:\(week)")
tableView.reloadData()
I am using parse to store and retrieve some data, which I then load into a UITableview, each cell contains some text and image, however when I open my tableview, any cells in the view do not show images until I scroll them out of view and back into view (I guess this is calling cellForRowAtIndexPath). Is there a way to check when all images are downloaded and then reload the tableview?
func loadData(){
self.data.removeAllObjects()
var query = PFQuery(className:"Tanks")
query.orderByAscending("createdAt")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
// The find succeeded.
for object in objects {
self.data.addObject(object)
}
} else {
// Log details of the failure
NSLog("Error: %# %#", error, error.userInfo!)
}
self.tableView.reloadData()
}
}
override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell {
self.cell = tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath!) as TankTableViewCell // let cell:TankTableViewCell
let item:PFObject = self.data.objectAtIndex(indexPath!.row) as PFObject
self.cell.productName.alpha = 1.0
self.cell.companyName.alpha = 1.0
self.cell.reviewTv.alpha = 1.0
self.rating = item.objectForKey("rating") as NSNumber
cell.productName.text = item.objectForKey("prodName") as? String
cell.companyName.text = item.objectForKey("compName") as? String
self.cell.reviewTv.text = item.objectForKey("review") as? String
let userImageFile = item.objectForKey("image") as PFFile
userImageFile.getDataInBackgroundWithBlock({
(imageData: NSData!, error: NSError!) -> Void in
if (error == nil) {
let image = UIImage(data:imageData)
self.cell.productImage.image = image
}
}, progressBlock: {
(percentDone: CInt) -> Void in
if percentDone == 100{
}
})
self.setStars(self.rating)
// Configure the cell...
UIView.animateWithDuration(0.5, animations: {
self.cell.productName.alpha = 1.0
self.cell.companyName.alpha = 1.0
self.cell.reviewTv.alpha = 1.0
self.cell.reviewTv.scrollRangeToVisible(0)
})
return cell
}
The problem is that you use self.cell and that you change that reference each time a cell is returned. So, when the images are loaded they are all set into the last cell to be returned, which probably isn't on screen (or at least not fully).
Really you should be capturing the cell in the completion block of the image download (and checking that the cell is still linked to the same index path).
Also, you should cache the downloaded images so you don't always download the data / recreate the image.
You could set up a delegate method in your UITableViewController that gets called by another controller class that fetches the images. I doubt that's what you want to do though.
What you should do is initialize the cells with a default image, and have the cell controller itself go and fetch the image in the background, and update its UIImageView when the fetch completes. You definitely don't want to wait around for all images to load before reloading the table because a.) that takes a long time, and b.) what if one fails or times out?
Once the cell has loaded its image, if it is swapped out by the recycler and swapped back in, you can simply get the cached image by calling getData instead of getDataInBackground as long as isDataAvailable is true.
After your line:
self.cell.productImage.image = image
Try Adding:
cell.layoutSubviews() or self.cell.layoutSubviews()
It should render the subview, or your image in this case, on the first table view.