版本记录
版本号 | 时间 |
---|---|
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的数据异步预加载,感兴趣的给个赞或者关注~~~
网友评论