UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)

版本記錄

版本號 時(shí)間
V1.0 2018.11.28 星期三

前言

iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面球恤,用戶交互也是通過UIKit進(jìn)行的颈墅。感興趣的參考上面幾篇文章幔戏。
1. UIKit框架(一) —— UIKit動力學(xué)和移動效果(一)
2. UIKit框架(二) —— UIKit動力學(xué)和移動效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)

源碼

1. Swift

首先看一下代碼組織結(jié)構(gòu)

接著看一下sb中的內(nèi)容

下面就是源碼了

1. EmojiViewController.swift
import UIKit

class EmojiViewController: UICollectionViewController {
  let dataStore = DataStore()
  let loadingQueue = OperationQueue()
  var loadingOperations: [IndexPath: DataLoadOperation] = [:]
  var ratingOverlayView: RatingOverlayView?
  var previewInteraction: UIPreviewInteraction?

  override func viewDidLoad() {
    super.viewDidLoad()
    collectionView?.prefetchDataSource = self
    
    ratingOverlayView = RatingOverlayView(frame: view.bounds)
    guard let ratingOverlayView = ratingOverlayView else { return }
    
    view.addSubview(ratingOverlayView)
    view.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      ratingOverlayView.leftAnchor.constraint(equalTo: view.leftAnchor),
      ratingOverlayView.rightAnchor.constraint(equalTo: view.rightAnchor),
      ratingOverlayView.topAnchor.constraint(equalTo: view.topAnchor),
      ratingOverlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
      ])
    ratingOverlayView.isUserInteractionEnabled = false
    
    if let collectionView = collectionView {
      previewInteraction = UIPreviewInteraction(view: collectionView)
      previewInteraction?.delegate = self
    }
  }
}

// MARK: - UICollectionViewDataSource
extension EmojiViewController {
  override func collectionView(_ collectionView: UICollectionView,
                               numberOfItemsInSection section: Int) -> Int {
    return dataStore.numberOfEmoji
  }
  
  override func collectionView(_ collectionView: UICollectionView,
      cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "EmojiCell", for: indexPath)
    
    if let cell = cell as? EmojiViewCell {
      cell.updateAppearanceFor(.none, animated: false)
    }
    return cell
  }
}

// MARK: - UICollectionViewDelegate
extension EmojiViewController {
  override func collectionView(_ collectionView: UICollectionView,
      willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let cell = cell as? EmojiViewCell else { return }
    
    // How should the operation update the cell once the data has been loaded?
    let updateCellClosure: (EmojiRating?) -> () = { [weak self] emojiRating in
      guard let self = self else {
        return
      }
      cell.updateAppearanceFor(emojiRating, animated: true)
      self.loadingOperations.removeValue(forKey: indexPath)
    }
    
    // Try to find an existing data loader
    if let dataLoader = loadingOperations[indexPath] {
      // Has the data already been loaded?
      if let emojiRating = dataLoader.emojiRating {
        cell.updateAppearanceFor(emojiRating, animated: false)
        loadingOperations.removeValue(forKey: indexPath)
      } else {
        // No data loaded yet, so add the completion closure to update the cell
        // once the data arrives
        dataLoader.loadingCompleteHandler = updateCellClosure
      }
    } else {
      // Need to create a data loaded for this index path
      if let dataLoader = dataStore.loadEmojiRating(at: indexPath.item) {
        // Provide the completion closure, and kick off the loading operation
        dataLoader.loadingCompleteHandler = updateCellClosure
        loadingQueue.addOperation(dataLoader)
        loadingOperations[indexPath] = dataLoader
      }
    }
  }
  
  override func collectionView(_ collectionView: UICollectionView,
      didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    // If there's a data loader for this index path we don't need it any more.
    // Cancel and dispose
    if let dataLoader = loadingOperations[indexPath] {
      dataLoader.cancel()
      loadingOperations.removeValue(forKey: indexPath)
    }
  }
}

// MARK: - UICollectionViewDataSourcePrefetching
extension EmojiViewController: UICollectionViewDataSourcePrefetching {
  func collectionView(_ collectionView: UICollectionView,
      prefetchItemsAt indexPaths: [IndexPath]) {
    for indexPath in indexPaths {
      if let _ = loadingOperations[indexPath] {
        continue
      }
      if let dataLoader = dataStore.loadEmojiRating(at: indexPath.item) {
        loadingQueue.addOperation(dataLoader)
        loadingOperations[indexPath] = dataLoader
      }
    }
  }
  
  func collectionView(_ collectionView: UICollectionView,
      cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
    for indexPath in indexPaths {
      if let dataLoader = loadingOperations[indexPath] {
        dataLoader.cancel()
        loadingOperations.removeValue(forKey: indexPath)
      }
    }
  }
}

// MARK: - UIPreviewInteractionDelegate
extension EmojiViewController: UIPreviewInteractionDelegate {
  func previewInteractionShouldBegin(_ previewInteraction: UIPreviewInteraction) -> Bool {
    if let indexPath = collectionView?.indexPathForItem(at: previewInteraction.location(in: collectionView!)),
      let cell = collectionView?.cellForItem(at: indexPath) {
      ratingOverlayView?.beginPreview(forView: cell)
      collectionView?.isScrollEnabled = false
      return true
    } else {
      return false
    }
  }
  
  func previewInteractionDidCancel(_ previewInteraction: UIPreviewInteraction) {
    ratingOverlayView?.endInteraction()
    collectionView?.isScrollEnabled = true
  }
  
  func previewInteraction(_ previewInteraction: UIPreviewInteraction,
      didUpdatePreviewTransition transitionProgress: CGFloat, ended: Bool) {
    ratingOverlayView?.updateAppearance(forPreviewProgress: transitionProgress)
  }
  
  func previewInteraction(_ previewInteraction: UIPreviewInteraction,
      didUpdateCommitTransition transitionProgress: CGFloat, ended: Bool) {
    let hitPoint = previewInteraction.location(in: ratingOverlayView!)
    if ended {
      let updatedRating = ratingOverlayView?.completeCommit(at: hitPoint)
      if let indexPath = collectionView?.indexPathForItem(at: previewInteraction.location(in: collectionView!)),
        let cell = collectionView?.cellForItem(at: indexPath) as? EmojiViewCell,
        let oldEmojiRating = cell.emojiRating {
        let newEmojiRating = EmojiRating(emoji: oldEmojiRating.emoji, rating: updatedRating!)
        dataStore.update(emojiRating: newEmojiRating)
        cell.updateAppearanceFor(newEmojiRating)
        collectionView?.isScrollEnabled = true
      }
    } else {
      ratingOverlayView?.updateAppearance(forCommitProgress: transitionProgress, touchLocation: hitPoint)
    }
  }
}
2. RatingOverlayView.swift
import UIKit

class RatingOverlayView: UIView {
  var blurView: UIVisualEffectView?
  var animator: UIViewPropertyAnimator?
  private var overlaySnapshot: UIView?
  private var ratingStackView: UIStackView?
  
  func updateAppearance(forPreviewProgress progress: CGFloat) {
    animator?.fractionComplete = progress
  }
  
  func updateAppearance(forCommitProgress progress: CGFloat, touchLocation: CGPoint) {
    guard let ratingStackView = ratingStackView else { return }
    // During the commit phase the user can select a rating based on touch location
    for subview in ratingStackView.arrangedSubviews {
      let translatedPoint = convert(touchLocation, to: subview)
      if subview.point(inside: translatedPoint, with: .none) {
        subview.backgroundColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1).withAlphaComponent(0.6)
      } else {
        subview.backgroundColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1).withAlphaComponent(0.2)
      }
    }
  }
  
  func completeCommit(at touchLocation: CGPoint) -> String {
    // At commit, find the selected rating and pass it back
    var selectedRating = ""
    
    guard let ratingStackView = ratingStackView else {
      return selectedRating
    }
    
    for subview in ratingStackView.arrangedSubviews where subview is UILabel {
      let subview = subview as! UILabel
      let translatedPoint = convert(touchLocation, to: subview)
      if subview.point(inside: translatedPoint, with: .none) {
        selectedRating = subview.text!
      }
    }
    
    // Tidy everything away
    endInteraction()
    
    return selectedRating
  }
  
  func beginPreview(forView view: UIView) {
    // Reset any previous animations / blurs
    animator?.stopAnimation(false)
    blurView?.removeFromSuperview()
    // Create the visual effect
    prepareBlurView()
    // Create and configure the snapshot of the view we are picking out
    overlaySnapshot?.removeFromSuperview()
    overlaySnapshot = view.snapshotView(afterScreenUpdates: false)
    if let overlaySnapshot = overlaySnapshot {
      blurView?.contentView.addSubview(overlaySnapshot)
      // Calculate the position (adjusted for scroll views)
      let adjustedCenter = view.superview?.convert(view.center, to: self)
      overlaySnapshot.center = adjustedCenter!
      // Create ratings labels
      prepareRatings(for: overlaySnapshot)
    }
    // Create the animator that'll track the preview progress
    animator = UIViewPropertyAnimator(duration: 0.3, curve: .linear) {
      // Specifying a blur type animates the blur radius
      self.blurView?.effect = UIBlurEffect(style: .regular)
      // Pull out the snapshot
      self.overlaySnapshot?.layer.shadowRadius = 8
      self.overlaySnapshot?.layer.shadowColor = #colorLiteral(red: 0.2549019754, green: 0.2745098174, blue: 0.3019607961, alpha: 1).cgColor
      self.overlaySnapshot?.layer.shadowOpacity = 0.3
      // Fade the ratings in
      self.ratingStackView?.alpha = 1
    }
    animator?.addCompletion { position in
      // Remove the blur view when animation gets back to the beginning
      switch position {
      case .start:
        self.blurView?.removeFromSuperview()
      default:
        break
      }
    }
  }
  
  func endInteraction() {
    // Animate back to the beginning (no blur)
    animator?.isReversed = true
    animator?.startAnimation()
  }
  
  private func prepareBlurView() {
    // Create a visual effect view and make it completely fill self. Start with no
    // effect - will animate the blur in.
    blurView = UIVisualEffectView(effect: .none)
    if let blurView = blurView {
      addSubview(blurView)
      blurView.translatesAutoresizingMaskIntoConstraints = false
      NSLayoutConstraint.activate([
        blurView.leftAnchor.constraint(equalTo: leftAnchor),
        blurView.rightAnchor.constraint(equalTo: rightAnchor),
        blurView.topAnchor.constraint(equalTo: topAnchor),
        blurView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
  }
  
  private func prepareRatings(for view: UIView) {
    // Build the two ratings labels
    let ??label = UILabel()
    ??label.text = "??"
    ??label.font = UIFont.systemFont(ofSize: 50)
    ??label.textAlignment = .center
    ??label.backgroundColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1).withAlphaComponent(0.2)
    let ??label = UILabel()
    ??label.text = "??"
    ??label.font = UIFont.systemFont(ofSize: 50)
    ??label.textAlignment = .center
    ??label.backgroundColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1).withAlphaComponent(0.2)
    
    // Pop them in a stack view
    ratingStackView = UIStackView(arrangedSubviews: [??label, ??label])
    if let ratingStackView = ratingStackView {
      ratingStackView.axis = .vertical
      ratingStackView.alignment = .fill
      ratingStackView.distribution = .fillEqually
      // Ratings should completely cover the supplied view
      view.addSubview(ratingStackView)
      ratingStackView.translatesAutoresizingMaskIntoConstraints = false
      NSLayoutConstraint.activate([
        view.leftAnchor.constraint(equalTo: ratingStackView.leftAnchor),
        view.rightAnchor.constraint(equalTo: ratingStackView.rightAnchor),
        view.topAnchor.constraint(equalTo: ratingStackView.topAnchor),
        view.bottomAnchor.constraint(equalTo: ratingStackView.bottomAnchor)
        ])
      ratingStackView.alpha = 0
    }
  }
}
3. EmojiViewCell.swift
import UIKit

class EmojiViewCell: UICollectionViewCell {
  @IBOutlet weak var emojiLabel: UILabel!
  @IBOutlet weak var ratingLabel: UILabel!
  @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
  
  var emojiRating: EmojiRating?
  
  override func prepareForReuse() {
    DispatchQueue.main.async {
      self.displayEmojiRating(.none)
    }
  }
  
  func updateAppearanceFor(_ emojiRating: EmojiRating?, animated: Bool = true) {
    DispatchQueue.main.async {
      if animated {
        UIView.animate(withDuration: 0.5) {
          self.displayEmojiRating(emojiRating)
        }
      } else {
        self.displayEmojiRating(emojiRating)
      }
    }
  }
  
  private func displayEmojiRating(_ emojiRating: EmojiRating?) {
    self.emojiRating = emojiRating
    if let emojiRating = emojiRating {
      emojiLabel?.text = emojiRating.emoji
      ratingLabel?.text = emojiRating.rating
      emojiLabel?.alpha = 1
      ratingLabel?.alpha = 1
      loadingIndicator?.alpha = 0
      loadingIndicator?.stopAnimating()
      backgroundColor = #colorLiteral(red: 0.9338415265, green: 0.9338632822, blue: 0.9338515401, alpha: 1)
      layer.cornerRadius = 10
    } else {
      emojiLabel?.alpha = 0
      ratingLabel?.alpha = 0
      loadingIndicator?.alpha = 1
      loadingIndicator?.startAnimating()
      backgroundColor = #colorLiteral(red: 0.7450980544, green: 0.1568627506, blue: 0.07450980693, alpha: 1)
      layer.cornerRadius = 10
    }
  }
}
4. DataStore.swift
import Foundation

let emoji = "??,??,??,??,??,??,??,??,??,??,??,??,??,??,?,??,??,??,??,??,??,??,??,??,??".components(separatedBy: ",")


class DataStore {
  private var emojiRatings = emoji.map { EmojiRating(emoji: $0, rating: "") }
  
  public var numberOfEmoji: Int {
    return emojiRatings.count
  }
  
  public func loadEmojiRating(at index: Int) -> DataLoadOperation? {
    if (0..<emojiRatings.count).contains(index) {
      return DataLoadOperation(emojiRatings[index])
    }
    return .none
  }
  
  public func update(emojiRating: EmojiRating) {
    if let index = emojiRatings.index(where: { $0.emoji == emojiRating.emoji }) {
      emojiRatings.replaceSubrange(index...index, with: [emojiRating])
    }
  }
}


class DataLoadOperation: Operation {
  var emojiRating: EmojiRating?
  var loadingCompleteHandler: ((EmojiRating) ->Void)?
  
  private let _emojiRating: EmojiRating
  
  init(_ emojiRating: EmojiRating) {
    _emojiRating = emojiRating
  }
  
  override func main() {
    if isCancelled { return }
    
    let randomDelayTime = Int.random(in: 500..<2000)
    usleep(useconds_t(randomDelayTime * 1000))
    
    if isCancelled { return }
    emojiRating = _emojiRating
    
    if let loadingCompleteHandler = loadingCompleteHandler {
      DispatchQueue.main.async {
        loadingCompleteHandler(self._emojiRating)
      }
    }
  }
}
5. EmojiRating.swift
import Foundation

struct EmojiRating {
  let emoji: String
  let rating: String
}

后記

本篇主要講述了UICollectionView的數(shù)據(jù)異步預(yù)加載罪帖,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末复隆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子讯检,更是在濱河造成了極大的恐慌琐鲁,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件人灼,死亡現(xiàn)場離奇詭異围段,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)投放,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門奈泪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事涝桅“葑耍” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵冯遂,是天一觀的道長砾隅。 經(jīng)常有香客問我,道長债蜜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任究反,我火速辦了婚禮寻定,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘精耐。我一直安慰自己狼速,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布卦停。 她就那樣靜靜地躺著向胡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惊完。 梳的紋絲不亂的頭發(fā)上僵芹,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音小槐,去河邊找鬼拇派。 笑死,一個(gè)胖子當(dāng)著我的面吹牛凿跳,可吹牛的內(nèi)容都是我干的件豌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼控嗜,長吁一口氣:“原來是場噩夢啊……” “哼茧彤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起疆栏,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤曾掂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后承边,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遭殉,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年博助,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了险污。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蛔糯,靈堂內(nèi)的尸體忽然破棺而出拯腮,到底是詐尸還是另有隱情,我是刑警寧澤蚁飒,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布动壤,位于F島的核電站,受9級特大地震影響淮逻,放射性物質(zhì)發(fā)生泄漏琼懊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一爬早、第九天 我趴在偏房一處隱蔽的房頂上張望哼丈。 院中可真熱鬧,春花似錦筛严、人聲如沸醉旦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽车胡。三九已至,卻和暖如春照瘾,著一層夾襖步出監(jiān)牢的瞬間匈棘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工析命, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留羹饰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓碳却,卻偏偏與公主長得像队秩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子昼浦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容