2017-07-18 51 views
1

我試圖從我的firebase數據庫下載圖像並將它們加載到collectionviewcells中。圖像下載,但我有麻煩讓他們都下載和異步加載。異步下載和緩存來自url的圖像

當前當我運行我的代碼最後圖像下載加載。但是,如果我更新數據庫,集合視圖更新和新的最後一個用戶配置文件圖像也會加載,但其餘部分不存在。

我寧願不使用第三方庫,所以任何資源或建議將不勝感激。

下面是處理下載的代碼:

func loadImageUsingCacheWithUrlString(_ urlString: String) { 

    self.image = nil 

//  checks cache 
    if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage { 
     self.image = cachedImage 
     return 
    } 

    //download 
    let url = URL(string: urlString) 
    URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in 

     //error handling 
     if let error = error { 
      print(error) 
      return 
     } 

     DispatchQueue.main.async(execute: { 

      if let downloadedImage = UIImage(data: data!) { 
       imageCache.setObject(downloadedImage, forKey: urlString as NSString) 

       self.image = downloadedImage 
      } 

     }) 

    }).resume() 
} 

我認爲解決之道在於重裝的CollectionView我只是不知道到底哪裏做它的地方。

有什麼建議嗎?編輯: 這裏是函數被調用的地方;我cellForItem at indexpath

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell 

    let user = users[indexPath.row] 

    cell.nameLabel.text = user.name 

    if let profileImageUrl = user.profileImageUrl { 

      cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl) 
    } 

    return cell 
} 

其他唯一的,我相信可能會影響圖像裝載的是這個功能我用它來下載用戶數據,這就是所謂的viewDidLoad,但是其他所有的數據下載正確。

func fetchUser(){ 
    Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in 

     if let dictionary = snapshot.value as? [String: AnyObject] { 
      let user = User() 
      user.setValuesForKeys(dictionary) 

      self.users.append(user) 
      print(self.users.count) 

      DispatchQueue.main.async(execute: { 
      self.collectionView?.reloadData() 
       }) 
     } 


    }, withCancel: nil) 

} 

當前的行爲:

至於當前行爲的最後一個單元是顯示下載的輪廓影像的唯一細胞;如果有5個單元格,第5個是唯一一個顯示配置文件圖像的單元格。另外當我更新數據庫,即註冊一個新用戶時,collectionview會更新並正確顯示新註冊用戶的個人資料圖像,以及正確下載圖像的最後一個單元格。然而,其餘的,沒有配置文件圖像。

+0

你可以顯示你的CollectionViewController嗎? – javimuu

+0

@javimuu我包括函數被調用的地方。 – Stefan

+0

使用sdwebimage庫輕鬆加載圖像。 –

回答

6

我知道你發現你的問題,它與上面的代碼無關,但我仍然有一個觀察。具體來說,即使單元(以及圖像視圖)隨後被重新用於其他索引路徑,異步請求也會繼續進行。這將導致兩個問題:

  1. 如果您快速滾動到第100行,你將不得不等待第一個99行的圖像檢索你看到的圖像的可見單元格之前。在圖像開始彈出之前,這可能會導致真正的長時間延遲。

  2. 如果第100行的單元格被多次重複使用(例如第0行,第9行,第18行等),您將可能會看到圖像從一個圖像閃爍到下一個圖像,直到您進入第100行的圖像檢索爲止。現在

,您可能不會立即注意到無論這些都是問題,因爲他們只會表現自己,當圖像檢索也很難跟上用戶的滾動(速度較慢的網絡和快速滾動的組合)。另外,您應該始終使用網絡鏈接調節器測試您的應用程序,該網絡鏈接調節器可以模擬不良連接,這使得更容易顯示這些錯誤。

無論如何,解決方案是跟蹤(a)與最後一個請求相關的當前URLSessionTask;和(b)請求當前URL。然後您可以(a)在開始新請求時,確保取消任何先前的請求; (b)更新圖片視圖時,請確保與圖片相關的網址與當前網址相匹配。

但是,編寫擴展時,你不能只添加新的存儲屬性。因此,您必須使用關聯的對象API,因此您可以將這兩個新存儲的值與UIImageView對象相關聯。我個人將這個關聯的值包裝在一個計算屬性中,以便檢索圖像的代碼不會被這種東西掩蓋。無論如何,這得到:

extension UIImageView { 

    private static var taskKey = 0 
    private static var urlKey = 0 

    private var currentTask: URLSessionTask? { 
     get { return objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask } 
     set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 
    } 

    private var currentURL: URL? { 
     get { return objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL } 
     set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 
    } 

    func loadImageAsync(with urlString: String?) { 
     // cancel prior task, if any 

     weak var oldTask = currentTask 
     currentTask = nil 
     oldTask?.cancel() 

     // reset imageview's image 

     self.image = nil 

     // allow supplying of `nil` to remove old image and then return immediately 

     guard let urlString = urlString else { return } 

     // check cache 

     if let cachedImage = ImageCache.shared.image(forKey: urlString) { 
      self.image = cachedImage 
      return 
     } 

     // download 

     let url = URL(string: urlString)! 
     currentURL = url 
     let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in 
      self?.currentTask = nil 

      //error handling 

      if let error = error { 
       // don't bother reporting cancelation errors 

       if (error as NSError).domain == NSURLErrorDomain && (error as NSError).code == NSURLErrorCancelled { 
        return 
       } 

       print(error) 
       return 
      } 

      guard let data = data, let downloadedImage = UIImage(data: data) else { 
       print("unable to extract image") 
       return 
      } 

      ImageCache.shared.save(image: downloadedImage, forKey: urlString) 

      if url == self?.currentURL { 
       DispatchQueue.main.async { 
        self?.image = downloadedImage 
       } 
      } 
     } 

     // save and start new task 

     currentTask = task 
     task.resume() 
    } 

} 

另外請注意,你引用了一些imageCache變量(一個全球性的?)。我建議的圖像緩存單,其中,除了提供基本的緩存機制,還注意到內存警告並清除本身在內存壓力的情況下:

class ImageCache { 
    private let cache = NSCache<NSString, UIImage>() 
    private var observer: NSObjectProtocol! 

    static let shared = ImageCache() 

    private init() { 
     // make sure to purge cache on memory pressure 

     observer = NotificationCenter.default.addObserver(forName: .UIApplicationDidReceiveMemoryWarning, object: nil, queue: nil) { [weak self] notification in 
      self?.cache.removeAllObjects() 
     } 
    } 

    deinit { 
     NotificationCenter.default.removeObserver(observer) 
    } 

    func image(forKey key: String) -> UIImage? { 
     return cache.object(forKey: key as NSString) 
    } 

    func save(image: UIImage, forKey key: String) { 
     cache.setObject(image, forKey: key as NSString) 
    } 
} 

正如你所看到的,異步檢索和緩存開始變得更復雜一點,這就是爲什麼我們通常建議考慮建立異步圖像檢索機制,如AlamofireImage或Kingfisher或SDWebImage。這些人花了很多時間處理上述問題和其他問題,並且相當健壯。但是如果你打算「自己動手」,那麼我會在最低限度內提出類似上述的建議。