2016-06-13 60 views
7

我是swift和iOS編程的新手,但是我已經能夠在Xcode中爲我的應用程序構建一個基本穩定的啓動界面。 CollectionView從我的家庭網絡服務器上的csv文件創建的字典數組中抓取圖像和文本。 CVS的文件包含以下格式的數據(注意,網址已被更改,以保護許可圖像):swift iOS - UICollectionView圖像在快速滾動後混合

csv文件

csv file is @ url https://myserver.com/csv.txt and contains the following 

Title";"SeriesImageURL 
Title1";"https://licensedimage.com/url1 
Title2";"https://licensedimage.com/url2 
... 
Title1000";"https://licensedimage.com/url1000 

的問題是,通過的CollectionView快速滾動時,細胞將抓住不正確的圖像。值得注意的是,如果您進行慢速或中速滾動,則會在將正確的圖像渲染到正確的單元格之前顯示不同的圖像(單元格的標籤文本始終正確,只有圖像永遠不會顯示)。在圖像不匹配的情況下,出現標籤的正確單元格後,CollectionView中的所有其他單元格也將顯示不正確的圖像。

E.g.單元1-9將顯示Title1-9和正確的圖像1-9 當滾動緩慢時,單元格19-27將顯示標題19-27,將簡要顯示圖像10-18,然後顯示正確的圖像19-27。 快速滾動大量的單元格時(例如從單元格1-9到單元格90-99),單元格90-99將顯示標題90-99,將顯示圖像10-50ish,然後將錯誤地保留在圖像41- 50(或其附近)。當進一步滾動時,單元格100+將顯示正確的標題,但僅顯示圖像41-50範圍內的圖像。

我認爲這個錯誤是因爲沒有正確處理單元重用,圖像緩存處理不當,或者兩者兼而有之。這也可能是我沒有看到作爲初學者的iOS /快速程序員。我試圖用完成修飾符實現一個請求,但似乎無法讓我的代碼設置的方式正常工作。我將不勝感激任何幫助,以及解釋爲什麼修復程序的工作方式。謝謝!

相關代碼如下。

SeriesCollectionViewController.swift

class SeriesCollectionViewController: UICollectionViewController, UISearchBarDelegate { 

let reuseIdentifier:String = "SeriesCell" 

// Set Data Source Models & Variables 
struct seriesModel { 

    let title: AnyObject 
    let seriesimageurl: AnyObject 
} 
var seriesDict = [String:AnyObject]() 
var seriesArray = [seriesModel]() 

// Image Cache 
var imageCache = NSCache() 

override func viewDidLoad() { 
    super.viewDidLoad() 
    // Grab Data from Source 
    do { 

     let url = NSURL(string: "https://myserver.com/csv.txt") 
     let fullText = try NSString(contentsOfURL: url!, encoding: NSUTF8StringEncoding) 
     let readings = fullText.componentsSeparatedByString("\n") as [String] 
     var seriesDictCount = readings.count 
     seriesDictCount -= 1 
     for i in 1..<seriesDictCount { 
      let seriesData = readings[i].componentsSeparatedByString("\";\"") 
      seriesDict["Title"] = "\(seriesData[0])" 
      seriesDict["SeriesImageURL"] = "\(seriesData[1])" 
      seriesArray.append(seriesModel(
       title: seriesDict["Title"]!, 
       seriesimageurl: seriesDict["SeriesImageURL"]!, 
      )) 
     } 
    } catch let error as NSError { 
     print("Error: \(error)") 
    } 
} 

override func didReceiveMemoryWarning() { 
    super.didReceiveMemoryWarning() 
    imageCache.removeAllObjects() 
    // Dispose of any resources that can be recreated. 
} 

//... 
//...skipping over some stuff that isn't relevant 
//... 

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> SeriesCollectionViewCell { 
    let cell: SeriesCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! SeriesCollectionViewCell 

    if (self.searchBarActive) { 
     let series = seriesArrayForSearchResult[indexPath.row] 
     do { 
      // set image 
      if let imageURL = NSURL(string: "\(series.seriesimageurl)") { 
       if let image = imageCache.objectForKey(imageURL) as? UIImage { 
         cell.seriesImage.image = image 
       } else { 
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { 
         if let tvimageData = NSData(contentsOfURL: imageURL) { 
          let image = UIImage(data: tvimageData) 
          self.imageCache.setObject(image!, forKey: imageURL) 
           dispatch_async(dispatch_get_main_queue(), {() -> Void in 
            cell.seriesImage.image = nil 
            cell.seriesImage.image = image 
           }) 
         } 
        }) 
       } 
      } 
      cell.seriesLabel.text = "\(series.title)" 
     } 
    } else { 
     let series = seriesArray[indexPath.row] 
     do { 
      // set image 
      if let imageURL = NSURL(string: "\(series.seriesimageurl)") { 
       if let image = imageCache.objectForKey(imageURL) as? UIImage { 
         cell.seriesImage.image = image 
       } else { 
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { 
         if let tvimageData = NSData(contentsOfURL: imageURL) { 
          let image = UIImage(data: tvimageData) 
          self.imageCache.setObject(image!, forKey: imageURL) 
          dispatch_async(dispatch_get_main_queue(), {() -> Void in 
           cell.seriesImage.image = nil 
           cell.seriesImage.image = image 
          }) 
         } 
        }) 
       } 

      } 
      cell.seriesLabel.text = "\(series.title)" 
     } 
    } 
    cell.layer.shouldRasterize = true 
    cell.layer.rasterizationScale = UIScreen.mainScreen().scale 
    cell.prepareForReuse() 
    return cell 
} 

SeriesCollectionViewCell

class SeriesCollectionViewCell: UICollectionViewCell { 

@IBOutlet weak var seriesImage: UIImageView! 
@IBOutlet weak var seriesLabel: UILabel! 

} 
+0

單元格被重用並且您正在異步調度圖像獲取,因此當您詭計單元並異步獲取新圖像時,舊獲取仍在運行。你需要處理這個。可能最簡單的方法是使用SDWebImage,它在UIImageView上有一個擴展名,可以爲你處理這個問題 – Paulw11

+0

@ Paulw11:是的,我認爲它必須處理沒有被正確取消的請求。我希望能夠使用UIKit編寫代碼,因爲我使用這個應用程序作爲學習體驗,並且我覺得依靠外部框架會失敗,因爲這是一個捷徑。雖然我確實看過Alamofire/AlamofireImage和SDWebImage,但由於某些原因,在安裝了pod之後,我無法導入模塊(這是一個單獨的問題,我還沒弄清楚)。如果我需要的話,我會用它,只是想先嚐試用UIKit來處理它。 – shadowofjazz

+0

另一種方法是將圖像URL存儲爲您單元格的屬性,然後在完成關閉中,根據您剛剛獲取的URL檢查屬性值。如果它不匹配,則不要設置圖像,因爲單元格已被重新使用。 – Paulw11

回答

9

你需要了解如何離隊正常工作。有很好的文章。

總結:

爲了保持滾動平滑性,出隊用於基本上 某一限度後重用細胞。假設您在 時間內有10個可見單元格,那麼即使您可能需要1000個單元格,它也可能會創建16-18(以上3-4個,下面3-4個,僅粗略估計)單元格。

現在,當你在做這個 -

let cell: SeriesCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! SeriesCollectionViewCell 

要重複使用從已經取得的細胞現有細胞。這就是爲什麼你看到舊圖像一段時間,然後你看到新的圖像,一旦它被加載。

您需要在您退出單元后立即清除舊映像。

cell.seriesImage.image = UIImage() //nil 

你基本上將設置一個空白佔位符這樣的形象,而不是惹imageCache是​​在你的案件爲零。

可以解決這個problem-

值得注意的是,如果你做一個慢速或中滾動,一個不同的圖像會顯示正確的圖像呈現到正確的小區之前(對細胞的標記文本總是正確的,只有圖像永遠關閉)。

現在,當爲當前單元格分配圖像時,如果要分配的圖像屬於該單元格,您需要再次檢查。你需要檢查這個,因爲你正在分配給它的圖像視圖正在被重用,並且它可能與indexPath中的一個單元格不同,這個單元格與生成圖像加載請求時的單元格不同。

當您將單元出列並將image屬性設置爲nil時,請讓seriesImage爲您記住當前的indexPath。

cell.seriesImage.indexPath = indexPath 

之後,如果imageView仍然屬於先前分配的indexPath,則只將圖像分配給它。這對細胞再利用100%有效。

if cell.seriesImage.indexPath == indexPath 
cell.seriesImage.image = image 

您可能需要考慮在UIImageView實例上設置indexPath。這是我準備和用於類似場景的東西 - UIView-Additions

它在Objective-C & Swift中都可用。

+0

因此,經過一些測試後,一旦出列單元格就清除舊映像似乎會破壞圖像緩存。它確實解決了滾動時在單元格中顯示的多個圖像的問題,但是它破壞了已經滾動到的圖像的緩存。例如,如果向後滾動,圖像現在變爲黑色,即使通過刷新發送reloaddata()信號也不會加載。 至於檢查屬於單元格的圖像,您可以擴展一點嗎?我看了一下你的UIView Additions swift文件,但我不知道如何實現它,因爲我的代碼結構。 – shadowofjazz

+0

當您將單元出列時,您將indexPath指定給imageView實例,稍後在分配圖像之前檢查該值。我正在更新答案。 –

+0

只要緩存圖像的重置變黑,我非常懷疑這是通過imageCache和圖像引用相同對象的問題。假設你有一個圖像,緩存它,現在cell.seriesImage指向這個UIImage實例內的緩存,我們需要手動將其設置爲零。這也爲imageCache緩存實例設置了零,因爲它們都指向同一個實例。這可能看起來有點怪異,嘗試設置UIImage()而不是零,這會在CGRectZero大小的圖像上設置一個空白實例。這樣你就不會混淆現有的緩存實例。 –

2

在您的自定義單元類中執行func prepareForReuse()。並在prepareForReuse()函數中重置您的圖像。所以它會在重新使用單元之前重置圖像視圖。

+0

你可以擴大或澄清?當我在自定義單元類中的'func prepareForReuse()'方法中重置圖像時,滾動回滾到那些圖像時,所有滾動的圖像都是黑色(無)。 – shadowofjazz

+0

重寫func prepareForReuse(){ super.prepareForReuse() yourImageView.image = UIImage() } –

+0

謝謝!我會在這個週末嘗試一下。 – shadowofjazz