美文网首页
UIKit框架(十) —— UICollectionView的数

UIKit框架(十) —— UICollectionView的数

作者: 刀客传奇 | 来源:发表于2018-11-28 11:54 被阅读76次

版本记录

版本号 时间
V1.0 2018.11.28 星期三

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)

源码

1. Swift

首先看一下代码组织结构

接着看一下sb中的内容

下面就是源码了

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的数据异步预加载,感兴趣的给个赞或者关注~~~

相关文章

网友评论

      本文标题:UIKit框架(十) —— UICollectionView的数

      本文链接:https://www.haomeiwen.com/subject/brsaqqtx.html