3 votes

Télécharger l'image dans tableView(_:, cellForRowAt:)

Lorsque je fais une transition vers un viewController avec un tableView, la cellule de tableView envoie immédiatement une requête au serveur en utilisant la méthode fetchUserAvatar(avatarName: handler: (String) -> Void). Cette méthode renvoie une URL qui lie à l'image. Téléchargez-la et mettez en cache l'image cacheImage est un objet de NSCache. Cet objet cacheImage a été initialisé dans le viewController précédent et attribué du précédent à ce viewController à l'aide de prepare(for segue: UIStoryboardSegue, sender: Any?). Lorsque ce viewController s'affiche, je ne peux pas voir l'image dans la cellule. Mais si je ferme le viewController et fais une transition à nouveau vers ce viewController avec tableView, l'image s'affichera. Je pense (je suppose) que c'est parce que les images n'ont pas encore été toutes entièrement téléchargées. Donc, je ne peux pas voir les images. Mais si je ferme le viewController et charge un objet du viewController et que le viewController obtient les images depuis le cache. Par conséquent, les images pourraient être affichées.

Je veux savoir comment éviter ce problème ? Merci.

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()
}

Je l'ai modifié, et déplacé le code de téléchargement dans viewdidload et rechargé le tableView, le résultat est le même.

2voto

Rob Points 70987

Votre vue d'image a-t-elle une taille fixe, ou profitez-vous de la taille intrinsèque? Selon votre description, je supposerais ce dernier. Et mettre à jour le cache et recharger la cellule à l'intérieur de fetchUserAvatar le gestionnaire d'achèvement devrait résoudre ce problème.

Mais vous avez deux problèmes ici:

  1. Vous devriez vraiment utiliser dataTask pour récupérer l'image, pas Data(contentsOf:) car le premier est asynchrone et le dernier est synchrone. Et vous ne voulez jamais faire d'appels synchrones sur la file d'attente principale. Dans le meilleur des cas, la fluidité de votre défilement sera affectée de manière défavorable par cet appel réseau synchrone. Au pire, vous risquez de voir le processus du chien de garde tuer votre application si la demande réseau est ralentie pour une raison quelconque et que vous bloquez le thread principal au mauvais moment.

    Personnellement, j'aurais fetchUserAvatar faire cette deuxième demande asynchrone de manière asynchrone et changer la fermeture pour renvoyer l'UIImage plutôt que l'URL en tant que String.

    Peut-être quelque chose comme:

    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()
    }
  2. C'est un point plus subtil, mais vous ne devriez pas utiliser la cellule à l'intérieur de la fermeture de gestionnaire d'achèvement appelée de manière asynchrone. La cellule aurait pu défiler hors de la vue et vous pourriez mettre à jour la cellule pour une ligne différente du tableau. Cela est probablement problématique uniquement pour les connexions réseau vraiment lentes, mais c'est quand même un problème.

    Votre fermeture asynchrone devrait déterminer le chemin d'index de la cellule, puis recharger juste ce chemin d'index avec reloadRows(at:with:).

    Par exemple:

    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   // assurez-vous de réinitialiser cela en premier, au cas où la cellule serait réutilisée
            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)
    
                // notez, s'il est possible que des lignes aient été insérées d'ici à ce que cette demande asynchrone soit terminée,
                // vous devriez vraiment recalculer quel est le chemin d'index pour ce message particulier. Ici, je suis juste
                // en utilisant l'indexPath précédent, ce qui n'est valable que si vous _ne_ insérez jamais de lignes.
    
                tableView.reloadRows(at: [indexPath], with: .automatic)
            }
        }
    
        return cell
    }

Honnêtement, il y a d'autres problèmes subtils ici (par exemple, si votre nom d'utilisateur ou votre nom d'avatar contiennent des caractères réservés, vos demandes échoueront; si vous défilez rapidement sur une connexion vraiment lente, les images des cellules visibles seront mises en attente derrière des cellules qui ne sont plus visibles, vous risquez des délais d'attente, etc.). Au lieu de passer beaucoup de temps à réfléchir à la manière de résoudre ces problèmes plus subtils, vous pourriez envisager d'utiliser une UIImageView catégorie établie qui effectue des demandes d'images asynchrones et prend en charge la mise en cache. Les options typiques incluent AlamofireImage, KingFisher, SDWebImage, etc.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X