ios


download image in tableView(_:, cellForRowAt:)


when i segue to a viewController with tableView, the tableviewcell immediately send a request to the server using the method fetchUserAvatar(avatarName: handler: (String) -> Void). and this method returns a url that link to the image. download it and cache the image cacheImage is an object of NSCache<NSString, UIImage>. this object cacheImage was initalised in previous view controller and being assigned from pervious to this viewContoller using prepare(for segue: UIStoryboardSegue, sender: Any?). when this viewController shows up, I can't see the image in the cell. but i pop the viewController out and segue to this viewController with tableView again. the image will show. I think(I guess) because
the images weren't all fully downloaded yet. so, i can't see the images. but if i pop out the viewController and load an object of viewController and the viewController gets the images from cache. therefore, the images could be shown.
I want to know how to avoid this problem? thanks.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessageCell
let row = indexPath.row
cell.content.text = messages[row].content
cell.date.text = messages[row].createdDateStrInLocal
cell.messageOwner.text = messages[row].user
if let avatar = cacheImage.object(forKey: messages[row].user as NSString){
cell.profileImageView.image = avatar
} else {
fetchUserAvatar(avatarName: messages[row].user, handler: { [unowned self] urlStr in
if let url = URL(string: urlStr), let data = try? Data(contentsOf: url), let avatar = UIImage(data: data){
self.cacheImage.setObject(avatar, forKey: self.messages[row].user as NSString)
cell.profileImageView.image = avatar
}
})
}
return cell
}
fileprivate func fetchUserAvatar(avatarName: String, handler: #escaping (String) -> Void){
guard !avatarName.isEmpty, let user = self.user, !user.isEmpty else { return }
let url = URL(string: self.url + "/userAvatarURL")
var request = URLRequest(url: url!)
let body = "username=" + user + "&avatarName=" + avatarName
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.httpBody = body.data(using: .utf8)
defaultSession.dataTask(with: request as URLRequest){ data, response, error in
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse {
if 200...299 ~= httpResponse.statusCode {
print("statusCode: \(httpResponse.statusCode)")
if let urlStr = String(data: data!, encoding: String.Encoding.utf8), urlStr != "NULL" {
handler(urlStr)
}
} else {
print("statusCode: \(httpResponse.statusCode)")
if let unwrappedData = String(data: data!, encoding: String.Encoding.utf8) {
print("POST: \(unwrappedData)")
self.warning(title: "Fail", message: unwrappedData, buttonTitle: "OK", style: .default)
} else {
self.warning(title: "Fail", message: "unknown error.", buttonTitle: "OK", style: .default)
}
}
} else if let error = error {
print("Error: \(error)")
}
}
}.resume()
}
I modified it, and move the download code in viewdidload and reload the tableview, the result is the same.
Does your image view have a fixed size, or are you taking advantage of the intrinsic size? Based upon your description, I'd assume the latter. And updating the cache and reloading the cell inside fetchUserAvatar completion handler should resolve that problem.
But you have two issues here:
You should really use dataTask to retrieve the image, not Data(contentsOf:) because the former is asynchronous and the latter is synchronous. And you never want to do synchronous calls on the main queue. At best, the smoothness of your scrolling will be adversely affected by this synchronous network call. At worst, you risk having the watch dog process kill your app if the network request is slowed down for any reason and you block the main thread at the wrong time.
Personally, I'd have fetchUserAvatar do this second asynchronous request asynchronously and change the closure to return the UIImage rather than the URL as a String.
Perhaps something like:
fileprivate func fetchUserAvatar(avatarName: String, handler: #escaping (UIImage?) -> Void){
guard !avatarName.isEmpty, let user = self.user, !user.isEmpty else {
handler(nil)
return
}
let url = URL(string: self.url + "/userAvatarURL")!
var request = URLRequest(url: url)
let body = "username=" + user + "&avatarName=" + avatarName
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.httpBody = body.data(using: .utf8)
defaultSession.dataTask(with: request) { data, response, error in
guard let data = data, error == nil, let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
print("Error: \(error?.localizedDescription ?? "Unknown error")")
DispatchQueue.main.async { handler(nil) }
return
}
guard let string = String(data: data, encoding: .utf8), let imageURL = URL(string: string) else {
DispatchQueue.main.async { handler(nil) }
return
}
defaultSession.dataTask(with: imageURL) { (data, response, error) in
guard let data = data, error == nil else {
DispatchQueue.main.async { handler(nil) }
return
}
let image = UIImage(data: data)
DispatchQueue.main.async { handler(image) }
}.resume()
}.resume()
}
This is a more subtle point, but you should not use the cell inside the asynchronously called completion handler closure. The cell could have scrolled out of view and you could be updating the cell for a different row of the table. This is likely only to be problematic for really slow network connections, but it's still an issue.
Your asynchronous closure should be determining the index path of the cell and then reloading just that index path with reloadRows(at:with:).
For example:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessageCell
let row = indexPath.row
cell.content.text = messages[row].content
cell.date.text = messages[row].createdDateStrInLocal
cell.messageOwner.text = messages[row].user
if let avatar = cacheImage.object(forKey: messages[row].user as NSString){
cell.profileImageView.image = avatar
} else {
cell.profileImageView.image = nil // make sure to reset this first, in case cell is reused
fetchUserAvatar(avatarName: messages[row].user) { [unowned self] avatar in
guard let avatar = avatar else { return }
self.cacheImage.setObject(avatar, forKey: self.messages[row].user as NSString)
// note, if it's possible rows could have been inserted by the time this async request is done,
// you really should recalculate what the indexPath for this particular message. Below, I'm just
// using the previous indexPath, which is only valid if you _never_ insert rows.
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
return cell
}
Frankly, there are other subtle issues here (e.g. if your username or avatar name contain reserved characters, your requests will fail; if you scroll quickly on a really slow connection, images for visible cells will get backlogged behind cells that are no longer visible, you risk timeouts, etc.). Rather than spending a lot of time contemplating how to fix these more subtle issues, you might consider using an established UIImageView category that performs asynchronous image requests and supports caching. Typical options include AlamofireImage, KingFisher, SDWebImage, etc.

Related Links

FirebaseCrash error with dump_syms when submitting to iOS App Store
How to autoreconnect a bluetooth keychain connected on my iOS app then restart my iPhone?
Append result arrays in Realm
how to place photo on imageView using AVfoundation (swift3)
Trouble with one of my constrains
An issue which I got with WKWebView seems to be a major bug on Safari
Multiple array elements selected from one viewcontroller to another viewController
JSON not working with Alamofire 4 [closed]
MagicalRecord and CoreData saving in different context error
xmpp authentication failure
WebKit Transform Wrong Transition
creating local push notification using react-native - IOS
UITableViewCell height not changing with custom height
Swift getter + array initialization
Corner Radius of View in UITableViewCell Subclass
Hidden Adjacent CollectionViewCells are still active?

Categories

HOME
ffmpeg
music
elf
runnable
android-sqlite
tcl
typeahead.js
application-cache
mvvm-light
sony-camera-api
identifier
pouchdb
zend-expressive
aar
eigen3
servlet-filters
snort
pydev
mlr
aspdotnetstorefront
missingmethodexception
wso2cep
symbolic-math
unsigned-integer
jtable
nested-loops
angular-ngmodel
editorconfig
whatsapi
mobile-center
charts.js
kube-dns
vue-router
spring-websocket
jdk1.6
folly
jetty-9
sagepay
azure-management-api
jpype
mamp-pro
quicksand
wp-api
win2d
sdcc
pulseaudio
html-form
fido
masm32
ammonite
assembly-references
android-touch-event
lapply
configuration-profile
xceed
dms
enhanced-ecommerce
uploadify
groupbox
kinto
samsung-gear
android-gpuimageview
double-quotes
dhc
pegkit
linegraph
antiforgerytoken
generalization
targetinvocationexception
sigkill
swift-array
iscroll4
pkcs#10
modalpopupextender
libavcodec
execcommand
vlfeat
resolver
prudentia
flash-cs4
micro-orm
polarssl
programmers-notepad
godaddy-api
slimbox
memento
timespan
file-structure
cakephp-2.1
vote
datakey
google-website-optimizer
maven-bundle-plugin
hpple
server.mappath
intel-8080
proxy-classes
demoscene
retain
emacs-ecb
plinqo
orthographic
apache-config
spquery
ryz
docking

Resources

Mobile Apps Dev
Database Users
javascript
java
csharp
php
android
MS Developer
developer works
python
ios
c
html
jquery
RDBMS discuss
Cloud Virtualization
Database Dev&Adm
javascript
java
csharp
php
python
android
jquery
ruby
ios
html
Mobile App
Mobile App
Mobile App