session-ios/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift

281 lines
12 KiB
Swift
Raw Normal View History

2017-09-28 20:09:11 +02:00
//
2019-01-23 20:03:21 +01:00
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
2017-09-28 20:09:11 +02:00
//
import Foundation
import PromiseKit
2020-11-11 07:45:50 +01:00
import SignalUtilitiesKit
import SignalUtilitiesKit
import YYImage
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
// MARK: Properties
2017-09-29 05:30:33 +02:00
var imageInfo: GiphyImageInfo? {
didSet {
AssertIsOnMainThread()
2017-09-29 05:30:33 +02:00
ensureCellState()
2017-09-29 05:30:33 +02:00
}
}
2019-01-23 20:03:21 +01: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()
2017-09-29 05:30:33 +02:00
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.
2019-01-23 20:03:21 +01:00
var stillAssetRequest: ProxiedContentAssetRequest?
var stillAsset: ProxiedContentAsset?
var animatedAssetRequest: ProxiedContentAssetRequest?
var animatedAsset: ProxiedContentAsset?
2017-09-29 05:42:57 +02:00
var imageView: YYAnimatedImageView?
var activityIndicator: UIActivityIndicatorView?
var isCellSelected: Bool = false {
didSet {
AssertIsOnMainThread()
ensureCellState()
}
}
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
activityIndicator = nil
isCellSelected = false
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.
2017-10-20 20:27:36 +02:00
guard let highQualityAnimatedRendition = imageInfo.pickSendingRendition() else {
2018-08-23 16:37:34 +02:00
Logger.warn("could not pick gif rendition: \(imageInfo.giphyId)")
clearAssetRequests()
return
}
self.renditionForSending = highQualityAnimatedRendition
2019-01-23 20:03:21 +01:00
// 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-20 20:27:36 +02:00
guard let animatedRendition = imageInfo.pickPreviewRendition() else {
2018-08-23 16:37:34 +02:00
Logger.warn("could not pick gif rendition: \(imageInfo.giphyId)")
clearAssetRequests()
2017-09-29 05:30:33 +02:00
return
}
guard let stillRendition = imageInfo.pickStillRendition() else {
2018-08-23 16:37:34 +02:00
Logger.warn("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 {
2019-01-23 20:03:21 +01:00
stillAssetRequest = GiphyDownloader.giphyDownloader.requestAsset(assetDescription: stillRendition,
priority: .high,
success: { [weak self] assetRequest, asset in
guard let strongSelf = self else { return }
if assetRequest != nil && assetRequest != strongSelf.stillAssetRequest {
owsFailDebug("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 {
owsFailDebug("Obsolete request callback.")
return
}
strongSelf.clearStillAssetRequest()
})
}
// Start animated asset request if necessary.
if animatedAsset != nil {
clearAnimatedAssetRequest()
} else if animatedAssetRequest == nil {
animatedAssetRequest = GiphyDownloader.giphyDownloader.requestAsset(assetDescription: animatedRendition,
priority: .low,
success: { [weak self] assetRequest, asset in
guard let strongSelf = self else { return }
2019-01-23 20:03:21 +01:00
if assetRequest != nil && assetRequest != strongSelf.animatedAssetRequest {
2018-08-27 16:27:48 +02:00
owsFailDebug("Obsolete request callback.")
return
}
2019-01-23 20:03:21 +01:00
// If we have the animated asset, we don't need the still asset.
strongSelf.clearAssetRequests()
strongSelf.animatedAsset = asset
strongSelf.ensureViewState()
},
failure: { [weak self] assetRequest in
guard let strongSelf = self else { return }
2019-01-23 20:03:21 +01:00
if assetRequest != strongSelf.animatedAssetRequest {
2018-08-27 16:27:48 +02:00
owsFailDebug("Obsolete request callback.")
return
}
2019-01-23 20:03:21 +01:00
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
}
2018-08-31 02:59:26 +02:00
guard NSData.ows_isValidImage(atPath: asset.filePath, mimeType: OWSMimeTypeImageGif) else {
owsFailDebug("invalid asset.")
2018-08-31 02:59:26 +02:00
clearViewState()
return
}
2018-04-11 21:17:34 +02:00
guard let image = YYImage(contentsOfFile: asset.filePath) else {
2018-08-27 16:27:48 +02:00
owsFailDebug("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)
imageView.ows_autoPinToSuperviewEdges()
}
guard let imageView = imageView else {
2018-08-27 16:27:48 +02:00
owsFailDebug("missing imageview.")
clearViewState()
return
}
2017-09-29 05:42:57 +02:00
imageView.image = image
self.themeBackgroundColor = nil
if self.isCellSelected {
2019-03-30 14:22:31 +01:00
let activityIndicator = UIActivityIndicatorView(style: .gray)
self.activityIndicator = activityIndicator
addSubview(activityIndicator)
activityIndicator.autoCenterInSuperview()
activityIndicator.startAnimating()
// Render activityIndicator on a white tile to ensure it's visible on
2017-10-20 20:19:32 +02:00
// when overlayed on a variety of potential gifs.
activityIndicator.themeBackgroundColor = .white
activityIndicator.alpha = 0.3
activityIndicator.autoSetDimension(.width, toSize: 30)
activityIndicator.autoSetDimension(.height, toSize: 30)
activityIndicator.themeShadowColor = .black
activityIndicator.layer.cornerRadius = 3
activityIndicator.layer.shadowOffset = CGSize(width: 1, height: 1)
activityIndicator.layer.shadowOpacity = 0.7
activityIndicator.layer.shadowRadius = 1.0
} else {
self.activityIndicator?.stopAnimating()
self.activityIndicator = nil
}
}
2019-01-23 20:03:21 +01:00
public func requestRenditionForSending() -> Promise<ProxiedContentAsset> {
guard let renditionForSending = self.renditionForSending else {
2018-08-27 16:27:48 +02:00
owsFailDebug("renditionForSending was unexpectedly nil")
return Promise(error: GiphyError.assertionError(description: "renditionForSending was unexpectedly nil"))
}
2019-01-23 20:03:21 +01:00
let (promise, resolver) = Promise<ProxiedContentAsset>.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.
2019-01-23 20:03:21 +01:00
_ = GiphyDownloader.giphyDownloader.requestAsset(assetDescription: renditionForSending,
priority: .high,
success: { _, asset in
2018-10-13 21:21:46 +02:00
resolver.fulfill(asset)
},
2019-01-23 20:03:21 +01:00
failure: { _ in
2017-10-20 20:19:32 +02:00
// TODO GiphyDownloader API should pass through a useful failing error
// so we can pass it through here
2018-08-23 16:37:34 +02:00
Logger.error("request failed")
2018-10-13 21:21:46 +02:00
resolver.reject(GiphyError.fetchFailure)
})
return promise
}
private func clearViewState() {
imageView?.image = nil
self.themeBackgroundColor = .backgroundSecondary
}
2019-01-23 20:03:21 +01:00
private func pickBestAsset() -> ProxiedContentAsset? {
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
}