Download image in tableView (_ :, cellForRowAt :)

when i go to viewController with tableView, the tableviewcell will immediately send a request to the server using the method fetchUserAvatar(avatarName: handler: (String) -> Void)

. and this method returns the url that links to the image. load it and cache the image cacheImage

is an object NSCache<NSString, UIImage>

. this object cacheImage

was initialized in the previous view manager and was assigned from the transparent viewContoller withprepare(for segue: UIStoryboardSegue, sender: Any?)

... when this viewController comes up, I can't see the image in the cell. but I log out of the viewController and go back to that viewController using the tableView. the picture will show. I think (think) because the images haven't been fully loaded yet. therefore, I do not see the image. but if i exit viewController and load the viewController object and viewController gets images from cache. so images can be shown.

I want to know how to avoid this problem? thank.

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 changed it and moved the loading code to viewdidload and reload the tableview, the result is the same.

+3


source to share


1 answer


Does your image have a fixed size, or are you using an internal size? Based on your description, I would take on the latter. Both updating the cache and reloading the cell inside the completion handler fetchUserAvatar

should fix this issue.

But you have two problems:



  • You should really use dataTask

    to fetch the image, not Data(contentsOf:)

    because the former is asynchronous and the latter is synchronous. And you never want to make synchronous calls on the main queue. At best, this synchronous network call will adversely affect the smoothness of your scrolling. In the worst case, you run the risk of the clockwise process killing your application if for any reason the network request slows down and you block the main thread at the wrong time.

    Personally, I would fetchUserAvatar

    execute this second asynchronous request asynchronously and change the closure to return UIImage

    , not the URL as 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 shouldn't use cell

    a completion handler inside an asynchronously called closure. The cell may have scrolled out of sight and you could update the cell for another row in the table. This can probably be problematic for really slow network connections, but it's still a problem.

    Your asynchronous close should determine the cell index path and then reload only 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 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
    }
    
          

To be honest, there are other subtle issues here too (for example, if your username or avatar name contains reserved characters, your requests will fail, if you scroll quickly on a very slow connection, images for visible cells will depend on cells that are not longer , you risk latency, etc.). Instead of spending a lot of time thinking about how to fix these more subtle issues, you might consider using an established category UIImageView

that makes asynchronous image requests and supports caching. Typical options include AlamofireImage, KingFisher, SDWebImage, etc.

+2


source







All Articles