session-ios/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift

242 lines
11 KiB
Swift
Raw Normal View History

2017-09-28 20:09:11 +02:00
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
2017-09-28 20:09:11 +02:00
2017-09-29 00:15:18 +02:00
class GifPickerCell: UICollectionViewCell {
2017-09-28 20:09:11 +02:00
let TAG = "[GifPickerCell]"
// MARK: Properties
2017-09-29 05:30:33 +02:00
var imageInfo: GiphyImageInfo? {
didSet {
AssertIsOnMainThread()
ensureCellState()
2017-09-29 05:30:33 +02:00
}
}
// Loading and playing GIFs is quite expensive (network, memory, cpu).
// Here's a bit of logic to not preload offscreen cells that are prefetched.
var isCellVisible = false {
2017-09-29 05:30:33 +02:00
didSet {
AssertIsOnMainThread()
ensureCellState()
2017-09-29 05:30:33 +02:00
}
}
2017-10-02 21:24:57 +02:00
// We do "progressive" loading by loading stills (jpg or gif) and "animated" gifs.
// This is critical on cellular connections.
var stillAssetRequest: GiphyAssetRequest?
var stillAsset: GiphyAsset?
2017-10-02 21:24:57 +02:00
var animatedAssetRequest: GiphyAssetRequest?
var animatedAsset: GiphyAsset?
2017-09-29 05:42:57 +02:00
var imageView: YYAnimatedImageView?
2017-09-29 05:30:33 +02:00
// As another bandwidth saving measure, we only fetch the full sized GIF when the user selects it.
private var renditionForSending: GiphyRendition?
2017-09-29 05:30:33 +02:00
// MARK: Initializers
2017-09-29 00:15:18 +02:00
2017-10-01 20:43:51 +02:00
deinit {
stillAssetRequest?.cancel()
2017-10-02 21:24:57 +02:00
animatedAssetRequest?.cancel()
2017-10-01 20:43:51 +02:00
}
2017-09-28 20:09:11 +02:00
override func prepareForReuse() {
super.prepareForReuse()
2017-09-29 05:30:33 +02:00
imageInfo = nil
isCellVisible = false
stillAsset = nil
stillAssetRequest?.cancel()
stillAssetRequest = nil
2017-10-02 21:24:57 +02:00
animatedAsset = nil
animatedAssetRequest?.cancel()
animatedAssetRequest = nil
2017-09-29 05:42:57 +02:00
imageView?.removeFromSuperview()
imageView = nil
2017-09-29 05:30:33 +02:00
}
private func clearStillAssetRequest() {
stillAssetRequest?.cancel()
stillAssetRequest = nil
}
private func clearAnimatedAssetRequest() {
2017-10-02 21:24:57 +02:00
animatedAssetRequest?.cancel()
animatedAssetRequest = nil
}
private func clearAssetRequests() {
clearStillAssetRequest()
clearAnimatedAssetRequest()
2017-09-29 05:30:33 +02:00
}
public func ensureCellState() {
ensureLoadState()
ensureViewState()
}
public func ensureLoadState() {
guard isCellVisible else {
// Don't load if cell is not visible.
clearAssetRequests()
2017-09-29 05:30:33 +02:00
return
}
guard let imageInfo = imageInfo else {
// Don't load if cell is not configured.
clearAssetRequests()
2017-09-29 05:30:33 +02:00
return
}
2017-10-02 21:24:57 +02:00
guard self.animatedAsset == nil else {
// Don't load if cell is already loaded.
clearAssetRequests()
2017-09-29 05:30:33 +02:00
return
}
// Record high quality animated rendition, but to save bandwidth, don't start downloading
// until it's selected.
guard let highQualityAnimatedRendition = imageInfo.pickHighQualityAnimatedRendition() else {
Logger.warn("\(TAG) could not pick gif rendition: \(imageInfo.giphyId)")
clearAssetRequests()
return
}
self.renditionForSending = highQualityAnimatedRendition
// The Giphy API returns a slew of "renditions" for a given image.
// It's critical that we carefully "pick" the best rendition to use.
2017-10-02 21:24:57 +02:00
guard let animatedRendition = imageInfo.pickAnimatedRendition() else {
Logger.warn("\(TAG) could not pick gif rendition: \(imageInfo.giphyId)")
clearAssetRequests()
2017-09-29 05:30:33 +02:00
return
}
guard let stillRendition = imageInfo.pickStillRendition() else {
Logger.warn("\(TAG) could not pick still rendition: \(imageInfo.giphyId)")
clearAssetRequests()
return
}
2017-10-01 20:54:39 +02:00
// Start still asset request if necessary.
if stillAsset != nil || animatedAsset != nil {
clearStillAssetRequest()
} else if stillAssetRequest == nil {
2017-10-01 20:54:39 +02:00
stillAssetRequest = GiphyDownloader.sharedInstance.requestAsset(rendition:stillRendition,
priority:.high,
success: { [weak self] assetRequest, asset in
guard let strongSelf = self else { return }
if assetRequest != nil && assetRequest != strongSelf.stillAssetRequest {
2017-10-01 20:43:51 +02:00
owsFail("Obsolete request callback.")
return
}
strongSelf.clearStillAssetRequest()
strongSelf.stillAsset = asset
strongSelf.ensureViewState()
},
failure: { [weak self] assetRequest in
guard let strongSelf = self else { return }
if assetRequest != strongSelf.stillAssetRequest {
2017-10-01 20:43:51 +02:00
owsFail("Obsolete request callback.")
return
}
strongSelf.clearStillAssetRequest()
})
}
2017-10-01 20:54:39 +02:00
2017-10-02 21:24:57 +02:00
// Start animated asset request if necessary.
if animatedAsset != nil {
clearAnimatedAssetRequest()
} else if animatedAssetRequest == nil {
2017-10-02 21:24:57 +02:00
animatedAssetRequest = GiphyDownloader.sharedInstance.requestAsset(rendition:animatedRendition,
priority:.low,
success: { [weak self] assetRequest, asset in
guard let strongSelf = self else { return }
2017-10-02 21:24:57 +02:00
if assetRequest != nil && assetRequest != strongSelf.animatedAssetRequest {
2017-10-01 20:43:51 +02:00
owsFail("Obsolete request callback.")
return
}
2017-10-02 21:24:57 +02:00
// If we have the animated asset, we don't need the still asset.
strongSelf.clearAssetRequests()
2017-10-02 21:24:57 +02:00
strongSelf.animatedAsset = asset
strongSelf.ensureViewState()
},
failure: { [weak self] assetRequest in
guard let strongSelf = self else { return }
2017-10-02 21:24:57 +02:00
if assetRequest != strongSelf.animatedAssetRequest {
2017-10-01 20:43:51 +02:00
owsFail("Obsolete request callback.")
return
}
strongSelf.clearAnimatedAssetRequest()
})
}
2017-09-28 20:09:11 +02:00
}
2017-09-29 05:42:57 +02:00
private func ensureViewState() {
guard isCellVisible else {
// Clear image view so we don't animate offscreen GIFs.
clearViewState()
return
}
guard let asset = pickBestAsset() else {
clearViewState()
2017-09-29 05:42:57 +02:00
return
}
guard let image = YYImage(contentsOfFile:asset.filePath) else {
owsFail("\(TAG) could not load asset.")
clearViewState()
2017-09-29 05:42:57 +02:00
return
}
if imageView == nil {
let imageView = YYAnimatedImageView()
self.imageView = imageView
self.contentView.addSubview(imageView)
2017-10-02 20:26:03 +02:00
imageView.autoPinToSuperviewEdges()
}
guard let imageView = imageView else {
owsFail("\(TAG) missing imageview.")
clearViewState()
return
}
2017-09-29 05:42:57 +02:00
imageView.image = image
self.backgroundColor = nil
}
public func requestRenditionForSending() -> Promise<GiphyAsset> {
guard let renditionForSending = self.renditionForSending else {
owsFail("\(TAG) renditionForSending was unexpectedly nil")
return Promise(error: GiphyError.assertionError(description: "renditionForSending was unexpectedly nil"))
}
let (promise, fulfill, reject) = Promise<GiphyAsset>.pending()
// We don't retain a handle on the asset request, since there will only ever
// be one selected asset, and we never want to cancel it.
_ = GiphyDownloader
.sharedInstance.requestAsset(rendition: renditionForSending,
priority: .high,
success: { _, asset in
fulfill(asset)
},
failure: { _ in
// TODO GiphyDownloader API shoudl pass through a useful failing error
// so we can pass it through here
Logger.error("\(self.TAG) request failed")
reject(GiphyError.fetchFailure)
})
return promise
}
private func clearViewState() {
imageView?.image = nil
self.backgroundColor = UIColor(white:0.95, alpha:1.0)
}
private func pickBestAsset() -> GiphyAsset? {
2017-10-02 21:24:57 +02:00
return animatedAsset ?? stillAsset
2017-09-29 05:42:57 +02:00
}
2017-09-28 20:09:11 +02:00
}