Sporadic behaviour in UITableViewCells when loading images - uitableview

I have a Table View that presents comments. Each comment includes a label and a UIImage that presents the authors profile image. There is no pattern to the way images are appearing when the table is loaded. Sometimes they load fine. Other times, the placeholder image appears and the image never loads at all. If I scroll, the behavior varies. I'm using AlamofireImage.
After some research I came across this post where the recommendation is to call
self.tableView.beginUpdates()
self.tableView.endUpdates()
in AlamofireImage's completion handler. This had no effect at all.
Here is my original code:
func tableView(_ tableView: UITableView, cellForRowAt.....{
...
let myURL = Comment.profileImageURL
if Comment.profileImageURL != nil {
profileImage.af_setImage(withURL: myURL!, placeholderImage: #imageLiteral(resourceName: "Profile Image"))
} else {
profileImage.image = UIImage(named: "Profile Image")
}
And here is the update based on this question's recommendation (last answer on page):
let myURL = comment.profileImageURL
if comment.profileImageURL != nil {
cell.profileImage.af_setImage(
withURL: myURL!,
placeholderImage: #imageLiteral(resourceName: "Profile Image"),
filter: nil,
imageTransition: UIImageView.ImageTransition.crossDissolve(0.5),
runImageTransitionIfCached: false) {
// Completion closure
response in
// Check if the image isn't already cached
if response.response != nil {
// Force the cell update
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
} else {
cell.profileImage.image = UIImage(named: "Profile Image")
}
====EDIT====
Adding additional information in the hopes it will help:
I have tried setting the profileImage.image = nil as suggested below. I have also cancel the download in prepareForReuse() as suggested below to no avail.
Before calling the image, I am doing some configuration on the UIImageView
profileImage.layer.cornerRadius = profileImage.bounds.size.width / 2
profileImage.clipsToBounds = true
profileImage.layer.borderWidth = 2
profileImage.layer.borderColor = UIColor(red:222/255.0, green:225/255.0, blue:227/255.0, alpha: 1.0).cgColor
The following is the imageView setting in XCode:
Finally the issue itself. About 50% of the time I move back and forth between views or scroll, placeholder images appear in place of the actual image. In fact. The place holders often appear on first load.
===EDIT#2===
I'm trying to follow Ashley Mills second suggestion. I check the Image URL in the completion handler of AlamofireImage and the results are odd. I just loaded the table view. All images loaded fine as shown below:
I print out the the image URL when the Completion Handler fires and it looks fine:
I'm in completion
Optional(http://www.smarttapp.com/Portals/0/Users/018/18/18/sballmer.jpg)
I'm in completion
Optional(http://www.smarttapp.com/Portals/0/Users/018/18/18/sballmer.jpg)
I'm in completion
Optional(http://www.smarttapp.com/Portals/0/Users/011/11/11/Elon%20Musk.jpeg)
I'm in completion
Optional(http://www.smarttapp.com/Portals/_default/Users/001/01/1/Martin%20Profile.jpg)
Using the Navigation Bar, I back out, then reload and the images are all place holder images as shown:
The print out is as follows:
I'm in completion nil
I'm in completion nil
I'm in completion nil
Finally this is the code where the image is loaded and the completion handler:
let myURL = Comment.profileImageURL
if Comment.profileImageURL != nil {
// profileImage.af_setImage(withURL: myURL!, placeholderImage: #imageLiteral(resourceName: "Profile Image"))
profileImage.af_setImage(
withURL: myURL!,
placeholderImage: #imageLiteral(resourceName: "Profile Image"),
filter: nil,
completion:{ image in
print("I'm in completion")
print(image.response?.url)
self.setNeedsLayout()
self.layoutIfNeeded()
})
} else {
profileImage.image = UIImage(named: "Profile Image")
}
I've tried everything at this point. Please help.

UITableViewController reuses cells. In order to prevent old images reappearing in cells that are not fully loaded yet, set the cell's image to nil before loading.
cell.profileImage.image = nil
let myURL = comment.profileImageURL
if comment.profileImageURL != nil {
[...]
} else {
cell.profileImage.image = UIImage(named: "Profile Image")
}

Each cell you display in image in can be reused if is scrolled off the screen. Using af_setImage will fetch the image, and display it when the response is received, but by that time the cell might be reused, and be trying to display an image at a different URL.
You can handle this in a couple of ways, but first thing to do is to add a Comment property to your cell, and handle the downloading there rather than in the view controller (you might find making all your IBOutlet properties fileprivate helps with this - it makes sure you're can't update the cell's UI from the view controller)
1) Cancel the download request when the cell is reused…
override func prepareForResuse() {
profileImage.af_cancelImageRequest
}
This should prevent the response for a previous URL being returned, but will mean the image isn't downloaded.
2) Check the URL that that the response is for is the one you're expecting.
var requestedURL: URL?
var comment: Comment? {
didSet {
if let myURL = Comment.profileImageURL {
requestedURL = myURL
// Fetch image from URL and when response is received,
// check self.myURL == response.url
} else {
profileImage.image = UIImage(named: "Profile Image")
}
}
}
This way the image will still be downloaded even if it's not displayed.

Related

Swift having image in background not changing with subview

I have a view that displays 1 picture at a time and that picture is supposed to be the background image. I also have other elements on top of that image that are supposed to change on touch . The view that I have is called VideoView and I have elements on top . All of this works my problem is that when I use insertSubview all the elements on top change (like they are supposed to) but the image stays the same (VideoView) and it is supposed to change . If I replace the InsertSubView with addSubview then the image changes correctly on click but my top elements do not show . I know that what I'm supposed to use is insertSubview and on touch I can see the image URL changing but the screen stays with the first default image .
I think I know what might be going on . On every tap I am adding a new InsertSubview and the new image might be behind the old image . I verified by uploading a video . I first had an image and when I tapped to go to the next screen the image stayed however the video sound came on .
class BookViewC: UIViewController{
#IBOutlet weak var VideoView: UIView!
var imageView: UIImageView?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// This is how I change images, touchCount number is linked with unique image depending on the number passed in .
for touch in touches {
let location = touch.location(in: self.VideoView)
if location.x < self.VideoView.layer.frame.size.width / 2 {
if touchCount == 0 || touchCount < 0 {
touchCount = 0
} else {
touchCount = touchCount - 1
}
reloadTable(Offset: touchCount)
}
else {
touchCount = touchCount + 1
reloadTable(Offset: touchCount)
}
}
}
func ShowImage(s_image: String) {
let imgURL: NSURL = NSURL(string: s_image)!
let request:NSURLRequest = NSURLRequest(url: imgURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
bookviewImage.imageView = UIImageView(image: UIImage(named: s_image))
bookviewImage.imageView?.frame = VideoView.bounds
bookviewImage.imageView?.contentMode = UIViewContentMode.scaleAspectFill
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
DispatchQueue.main.async(execute: { () -> Void in
if data != nil {
self.bookviewImage.imageView?.image = UIImage(data: data!)
self.VideoView.insertSubview(self.bookviewImage.imageView!, at: 0)
// self.VideoView.addSubview(self.bookviewImage.imageView!)
} else {
self.bookviewImage.imageView!.image = nil
}
})
});
task.resume()
}
func reloadTable(Offset: Int) {
// send http request
session.dataTask(with:request, completionHandler: {(data, response, error) in
if error != nil {
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
if let Streams = parsedData["Results"] as? [AnyObject]?
{
if self.streamsModel.Locations.count >= 0 {
// Get Results
}
DispatchQueue.main.async {
for Stream in Streams! {
if let s_image = Stream["stream_image"] as? String {
self.ShowImage(s_image: s_image)
}
}
}
This background Image on InsertSubview does not change on click however when I add AddSubview the background image changes so I know there is something wrong in my code since the InsertSubview background image is not updating
[![ .][2]][2]
I'm not sure how you are changing pictures and might have to see the code to provide a better explanation. In the meantime, you should understand the difference between insertSubview and addSubview.
addSubview:
addSubview adds a view at the end of the list. In the hierarchical structure, this would be the bottom most item. On screen, however, it would be the top most item covering everything underneath it.
The hierarchy would look like
View
Video View
Profile Image
Time
Full Name
Post
Button
imageView
insertSubview:
insertSubview inserts a view at a given position in the list. Your code is inserting at position 0. In the hierarchical structure, this would be the top most item. On screen, however, it would be the bottom most item covered by everything on top of it.
The hierarchy would look like
View
Video View
imageView
Profile Image
Time
Full Name
Post
Button
Hope that helps.

Unable to reload collection view on successful image download

I'm editing a record which has some text information and images. As I get images URL so I'm using SDWebImage to download images which are then displayed in a collectionView and it's vertical scrollable. So when the view is loaded I'm doing this in viewDidLoad:
for (index, _) in mediaFiles.enumerated()
{
let img = UIImageView()
if let url = NSURL(string: "\(baseURLGeneral)\(mediaFiles[index].imageFile ?? "")")
{
print(url)
img.sd_setShowActivityIndicatorView(true)
img.sd_setIndicatorStyle(.gray)
img.sd_setImage(with: url as URL, placeholderImage: nil, options: .refreshCached, completed: { (loadedImage, error, cache, url) in
self.imagesArray.append(loadedImage!)
DispatchQueue.main.async
{
self.photoCollection.reloadData()
}
})
}
}
This code as per my understanding is downloading the image from web and when the image is loaded it add the image in an imageArray which I've declared as var imagesArray: [UIImage] = [] and then reload the collection view.
As this is the edit screen so user can also add more images in imagesArray which will show with the downloaded images in the same array and can also remove images.
As per collectionView delegate and dataSource is concerned so I'm returning return imagesArray.count in numberOfItemsInSection.
In cellForItemAt after making a cell variable I've cell.imageAddedByUser.image = imagesArray[indexPath.row].
THE ISSUE which I'm having is that after downloading images in viewDidLoad() collectionView is not getting refreshed. But If I pop and then push view controller it shows the images.
Try to call it in viewDidAppear()
DispatchQueue.main.async { self.photoCollection.reloadData() }
Try to dispatch it in main Queue
DispatchQueue.main.async
{
self.photoCollection.reloadData()
}

UIImage appears 2-3 seconds late on viewController swift

I have a UIImageView that I use for users profile pic. But, the image appears late on screen like 2 to 3 seconds. Here is how I download it:
#IBOutlet weak var ProfilePic: UIImageView!
override func awakeFromNib() {
FriendSystem.system.USER_REF.child(user.uid).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.hasChild("imageUrl"){
self.firebaseStorage.child("users").child(FriendSystem.system.CURRENT_USER_ID).getData(maxSize: 10*1024*1024, completion: { (data, error) in
let userPhoto = UIImage(data: data!)
self.ProfilePic.image = userPhoto
})
}
})
}
How can I solve this?
The code you're running runs on a different Thread, and according to Apple, all UI updates must run on the Main Thread. Here's a link that explains why all UI updates must be on the Main Thread
So in your code you just have to use DispatchQueue like this
DispatchQueue.main.async {
// please don't use implicit unwrapping of variables `data!` cause it may cause crashes, use if let statements instead
if let data = data {
let userPhoto = UIImage(data: data)
self.ProfilePic.image = userPhoto
}
}
On a different note:
Please read on Swift coding standards in this link
Well i actually solved it and I will post it here if newbies like me encounter the same problem. I used Firebase and SDWebImage. Here is the link This is from Firebase's official website.
// Reference to an image file in Firebase Storage
let reference = storageRef.child("images/stars.jpg")
// UIImageView in your ViewController
let imageView: UIImageView = self.imageView
// Placeholder image
let placeholderImage = UIImage(named: "placeholder.jpg")
// Load the image using SDWebImage
imageView.sd_setImage(with: reference, placeholderImage: placeholderImage)

Swift dispatch_async not updating UIView until iOS notification

I have a UIView that is updated from a server once a button is pushed.
The UIView update happens within a dispatch_async(). Through debugging I can see that the server response has been received and the new subview is created and applied to the UIView.
However the view remains unchanged UNTIL another iOS notification occurs (e.g. the device receives an email). As soon as the notification banner is displayed the UIView is refreshed and displays the image from the server.
What am I missing? How do I get the UIView to update as soon as the new subview is added?
Note: I have tried the following within and outside the dispatch_async() and tried calling them on the same queue after the initial change takes place without luck.
self.view.setNeedsDisplay()
self.view.bringSubviewToFront(view)
self.view.layoutIfNeeded() (thanks for the suggestion #buxik)
EDIT added code (small version of larger class, only relevant section added):
class networkedImage: UIViewController {
#IBOutlet var viewArea: UIView = UIView()
let originalWidth: CGFloat = 200
let originalHeight: CGFloat = 100
let imageURL: NSURL = NSURL(string: "www.example.com/image.jpg")!
func updateViewArea() {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
dispatch_async(dispatch_get_main_queue()) {
self.viewArea.subviews.forEach({ $0.removeFromSuperview() })
let newImageView = UIImageView(frame: CGRectMake(0, 0, self.originalWidth, self.originalHeight))
let data = NSData(contentsOfURL: self.imageURL)
if data != nil {
let image = UIImage(data: data!)
if image != nil {
newImageView.image = image
self.viewArea.addSubview(newImageView)
self.viewArea.bringSubviewToFront(newImageView)
self.viewArea.setNeedsDisplay()
}
NSNotificationCenter.defaultCenter().addObserverForName("NotificationIdentifier", object: nil, queue: nil, usingBlock: {
[unowned self] note in
print("I thought this might work, it didn't.")
})
}
}
}
}
}
To resolve the issue I added an Observer to viewDidLoad()
NSNotificationCenter.defaultCenter().addObserverForName("imageReadyNotification", object: nil, queue: NSOperationQueue.mainQueue(), usingBlock: {
[unowned self] note in
self.updateViewArea()
})
Then at the bottom of updateViewArea() I post the notification
if self.newImage == true {
NSNotificationCenter.defaultCenter().postNotificationName("imageReadyNotification", object: nil, userInfo: nil)
}
The if condition is to ensure the notification isn't fired continually. I have no idea why calling this function twice works, but it does.

Asynchronously Loading Image constantly causes image to flicker?

I have a header view for every UITableViewCell. In this header view, I load a picture of an individual via an asynchronous function in the Facebook API. However, because the function is asynchronous, I believe the function is called multiple times over and over again, causing the image to flicker constantly. I would imagine a fix to this issue would be to load the images in viewDidLoad in an array first, then display the array contents in the header view of the UITableViewCell. However, I am having trouble implementing this because of the asynchronous nature of the function: I can't seem to grab every photo, and then continue on with my program. Here is my attempt:
//Function to get a user's profile picture
func getProfilePicture(completion: (result: Bool, image: UIImage?) -> Void){
// Get user profile pic
let url = NSURL(string: "https://graph.facebook.com/1234567890/picture?type=large")
let urlRequest = NSURLRequest(URL: url!)
//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: \(error)")
}
// Display the image
let image = UIImage(data: data)
if(image != nil){
completion(result: true, image: image)
}
}
}
override func viewDidLoad() {
self.getProfilePicture { (result, image) -> Void in
if(result == true){
println("Loading Photo")
self.creatorImages.append(image!)
}
else{
println("False")
}
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
headerView.headerImage.image = self.creatorImages[section]
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
As seen by the program above, I the global array that I created called self.creatorImages which holds the array of images I grab from the Facebook API is always empty and I need to "wait" for all the pictures to populate the array before actually using it. I'm not sure how to accomplish this because I did try a completion handler in my getProfilePicture function but that didn't seem to help and that is one way I have learned to deal with asynchronous functions. Any other ideas? Thanks!
I had the same problem but mine was in Objective-C
Well, the structure is not that different, what i did was adding condition with:
headerView.headerImage.image
Here's an improved solution that i think suits your implementation..
since you placed self.getProfilePicture inside viewDidLoad it will only be called once section==0 will only contain an image,
the code below will request for addition image if self.creatorImages's index is out of range/bounds
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
if (section < self.creatorImages.count) // validate self.creatorImages index to prevent 'Array index out of range' error
{
if (headerView.headerImage.image == nil) // prevents the blinks
{
headerView.headerImage.image = self.creatorImages[section];
}
}
else // requests for additional image at section
{
// this will be called more than expected because of tableView.reloadData()
println("Loading Photo")
self.getProfilePicture { (result, image) -> Void in
if(result == true) {
//simply appending will do the work but i suggest something like:
if (self.creatorImages.count <= section)
{
self.creatorImages.append(image!)
tableView.reloadData()
println("self.creatorImages.count \(self.creatorImages.count)")
}
//that will prevent appending excessively to data source
}
else{
println("Error loading image")
}
}
}
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
You sure have different implementation from what i have in mind, but codes in edit history is not in vain, right?.. hahahaha.. ;)
Hope i've helped you.. Cheers!

Resources