2017-09-28 20:09:11 +02:00
|
|
|
//
|
|
|
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
Download smaller GIF for previews.
Previously we were downloading a full sized GIF for each cell, which can
take dozens of seconds on a slower connection. Now we download a smaller
GIF for the picker view, and only download the full sized GIF for the
selected cell.
Some stats:
Before:
Scenario: search "Cat" and no scrolling, no picking
~10 MB
Scenario: search "Cat" and no scrolling, then pick
~10 MB
Scenario: search "Cat" and scroll 3 screens, no picking
~30 MB
Scenario: search "Cat" and scroll 3 screens, then pick
~30 MB
After:
Scenarios: search "Cat" and no scrolling, no picking
~1.0 MB (savings 90%)
Scenarios: search "Cat" and no scrolling, then pick
~3.5 MB (savings 65%)
Scenarios: search "Cat" and scroll 3 screens, no picking
~3.0 MB (savings 90%)
Scenarios: search "Cat" and scroll 3 screens, then pick
~5.5 MB (savings 81%)
// FREEBIE
2017-10-19 22:36:45 +02:00
|
|
|
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()
|
|
|
|
|
2017-10-01 02:43:00 +02:00
|
|
|
ensureCellState()
|
2017-09-29 05:30:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-30 23:59:19 +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.
|
2017-10-01 02:43:00 +02:00
|
|
|
var isCellVisible = false {
|
2017-09-29 05:30:33 +02:00
|
|
|
didSet {
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
|
2017-10-01 02:43:00 +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.
|
2017-09-30 23:59:19 +02:00
|
|
|
// This is critical on cellular connections.
|
2017-09-30 03:20:04 +02:00
|
|
|
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
|
|
|
|
Download smaller GIF for previews.
Previously we were downloading a full sized GIF for each cell, which can
take dozens of seconds on a slower connection. Now we download a smaller
GIF for the picker view, and only download the full sized GIF for the
selected cell.
Some stats:
Before:
Scenario: search "Cat" and no scrolling, no picking
~10 MB
Scenario: search "Cat" and no scrolling, then pick
~10 MB
Scenario: search "Cat" and scroll 3 screens, no picking
~30 MB
Scenario: search "Cat" and scroll 3 screens, then pick
~30 MB
After:
Scenarios: search "Cat" and no scrolling, no picking
~1.0 MB (savings 90%)
Scenarios: search "Cat" and no scrolling, then pick
~3.5 MB (savings 65%)
Scenarios: search "Cat" and scroll 3 screens, no picking
~3.0 MB (savings 90%)
Scenarios: search "Cat" and scroll 3 screens, then pick
~5.5 MB (savings 81%)
// FREEBIE
2017-10-19 22:36:45 +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
|
2017-10-01 02:43:00 +02:00
|
|
|
isCellVisible = false
|
2017-09-30 03:20:04 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-09-30 03:20:04 +02:00
|
|
|
private func clearStillAssetRequest() {
|
|
|
|
stillAssetRequest?.cancel()
|
|
|
|
stillAssetRequest = nil
|
|
|
|
}
|
|
|
|
|
2017-10-04 17:55:15 +02:00
|
|
|
private func clearAnimatedAssetRequest() {
|
2017-10-02 21:24:57 +02:00
|
|
|
animatedAssetRequest?.cancel()
|
|
|
|
animatedAssetRequest = nil
|
2017-09-30 03:20:04 +02:00
|
|
|
}
|
|
|
|
|
2017-10-01 02:43:00 +02:00
|
|
|
private func clearAssetRequests() {
|
2017-09-30 03:20:04 +02:00
|
|
|
clearStillAssetRequest()
|
2017-10-04 17:55:15 +02:00
|
|
|
clearAnimatedAssetRequest()
|
2017-09-29 05:30:33 +02:00
|
|
|
}
|
|
|
|
|
2017-10-01 04:03:55 +02:00
|
|
|
public func ensureCellState() {
|
2017-10-04 17:55:15 +02:00
|
|
|
ensureLoadState()
|
|
|
|
ensureViewState()
|
|
|
|
}
|
|
|
|
|
|
|
|
public func ensureLoadState() {
|
2017-10-01 02:43:00 +02:00
|
|
|
guard isCellVisible else {
|
2017-10-04 17:55:15 +02:00
|
|
|
// Don't load if cell is not visible.
|
2017-10-01 02:43:00 +02:00
|
|
|
clearAssetRequests()
|
2017-09-29 05:30:33 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
guard let imageInfo = imageInfo else {
|
2017-10-04 17:55:15 +02:00
|
|
|
// Don't load if cell is not configured.
|
2017-10-01 02:43:00 +02:00
|
|
|
clearAssetRequests()
|
2017-09-29 05:30:33 +02:00
|
|
|
return
|
|
|
|
}
|
2017-10-02 21:24:57 +02:00
|
|
|
guard self.animatedAsset == nil else {
|
2017-10-04 17:55:15 +02:00
|
|
|
// Don't load if cell is already loaded.
|
|
|
|
clearAssetRequests()
|
2017-09-29 05:30:33 +02:00
|
|
|
return
|
|
|
|
}
|
Download smaller GIF for previews.
Previously we were downloading a full sized GIF for each cell, which can
take dozens of seconds on a slower connection. Now we download a smaller
GIF for the picker view, and only download the full sized GIF for the
selected cell.
Some stats:
Before:
Scenario: search "Cat" and no scrolling, no picking
~10 MB
Scenario: search "Cat" and no scrolling, then pick
~10 MB
Scenario: search "Cat" and scroll 3 screens, no picking
~30 MB
Scenario: search "Cat" and scroll 3 screens, then pick
~30 MB
After:
Scenarios: search "Cat" and no scrolling, no picking
~1.0 MB (savings 90%)
Scenarios: search "Cat" and no scrolling, then pick
~3.5 MB (savings 65%)
Scenarios: search "Cat" and scroll 3 screens, no picking
~3.0 MB (savings 90%)
Scenarios: search "Cat" and scroll 3 screens, then pick
~5.5 MB (savings 81%)
// FREEBIE
2017-10-19 22:36:45 +02:00
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2017-09-30 23:59:19 +02: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-02 21:24:57 +02:00
|
|
|
guard let animatedRendition = imageInfo.pickAnimatedRendition() else {
|
2017-09-30 03:20:04 +02:00
|
|
|
Logger.warn("\(TAG) could not pick gif rendition: \(imageInfo.giphyId)")
|
2017-10-01 02:43:00 +02:00
|
|
|
clearAssetRequests()
|
2017-09-29 05:30:33 +02:00
|
|
|
return
|
|
|
|
}
|
2017-09-30 03:20:04 +02:00
|
|
|
guard let stillRendition = imageInfo.pickStillRendition() else {
|
|
|
|
Logger.warn("\(TAG) could not pick still rendition: \(imageInfo.giphyId)")
|
2017-10-01 02:43:00 +02:00
|
|
|
clearAssetRequests()
|
2017-09-30 03:20:04 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-01 20:54:39 +02:00
|
|
|
// Start still asset request if necessary.
|
2017-10-04 17:55:15 +02:00
|
|
|
if stillAsset != nil || animatedAsset != nil {
|
|
|
|
clearStillAssetRequest()
|
|
|
|
} else if stillAssetRequest == nil {
|
2017-10-01 20:54:39 +02:00
|
|
|
stillAssetRequest = GiphyDownloader.sharedInstance.requestAsset(rendition:stillRendition,
|
2017-09-30 03:20:04 +02:00
|
|
|
priority:.high,
|
2017-10-01 02:43:00 +02:00
|
|
|
success: { [weak self] assetRequest, asset in
|
2017-09-30 03:20:04 +02:00
|
|
|
guard let strongSelf = self else { return }
|
2017-10-01 02:43:00 +02:00
|
|
|
if assetRequest != nil && assetRequest != strongSelf.stillAssetRequest {
|
2017-10-01 20:43:51 +02:00
|
|
|
owsFail("Obsolete request callback.")
|
2017-10-01 02:43:00 +02:00
|
|
|
return
|
|
|
|
}
|
2017-09-30 03:20:04 +02:00
|
|
|
strongSelf.clearStillAssetRequest()
|
|
|
|
strongSelf.stillAsset = asset
|
2017-10-04 17:55:15 +02:00
|
|
|
strongSelf.ensureViewState()
|
2017-09-30 03:20:04 +02:00
|
|
|
},
|
2017-10-01 02:43:00 +02:00
|
|
|
failure: { [weak self] assetRequest in
|
2017-09-30 03:20:04 +02:00
|
|
|
guard let strongSelf = self else { return }
|
2017-10-01 02:43:00 +02:00
|
|
|
if assetRequest != strongSelf.stillAssetRequest {
|
2017-10-01 20:43:51 +02:00
|
|
|
owsFail("Obsolete request callback.")
|
2017-10-01 02:43:00 +02:00
|
|
|
return
|
|
|
|
}
|
2017-09-30 03:20:04 +02:00
|
|
|
strongSelf.clearStillAssetRequest()
|
|
|
|
})
|
|
|
|
}
|
2017-10-01 20:54:39 +02:00
|
|
|
|
2017-10-02 21:24:57 +02:00
|
|
|
// Start animated asset request if necessary.
|
2017-10-04 17:55:15 +02:00
|
|
|
if animatedAsset != nil {
|
|
|
|
clearAnimatedAssetRequest()
|
|
|
|
} else if animatedAssetRequest == nil {
|
2017-10-02 21:24:57 +02:00
|
|
|
animatedAssetRequest = GiphyDownloader.sharedInstance.requestAsset(rendition:animatedRendition,
|
2017-09-30 03:20:04 +02:00
|
|
|
priority:.low,
|
2017-10-01 02:43:00 +02:00
|
|
|
success: { [weak self] assetRequest, asset in
|
2017-09-30 03:20:04 +02:00
|
|
|
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.")
|
2017-10-01 02:43:00 +02:00
|
|
|
return
|
|
|
|
}
|
2017-10-02 21:24:57 +02:00
|
|
|
// If we have the animated asset, we don't need the still asset.
|
2017-10-01 02:43:00 +02:00
|
|
|
strongSelf.clearAssetRequests()
|
2017-10-02 21:24:57 +02:00
|
|
|
strongSelf.animatedAsset = asset
|
2017-10-04 17:55:15 +02:00
|
|
|
strongSelf.ensureViewState()
|
2017-09-30 03:20:04 +02:00
|
|
|
},
|
2017-10-01 02:43:00 +02:00
|
|
|
failure: { [weak self] assetRequest in
|
2017-09-30 03:20:04 +02:00
|
|
|
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.")
|
2017-10-01 02:43:00 +02:00
|
|
|
return
|
|
|
|
}
|
2017-10-04 17:55:15 +02:00
|
|
|
strongSelf.clearAnimatedAssetRequest()
|
2017-09-30 03:20:04 +02:00
|
|
|
})
|
|
|
|
}
|
2017-09-28 20:09:11 +02:00
|
|
|
}
|
2017-09-29 05:42:57 +02:00
|
|
|
|
2017-10-04 17:55:15 +02:00
|
|
|
private func ensureViewState() {
|
|
|
|
guard isCellVisible else {
|
|
|
|
// Clear image view so we don't animate offscreen GIFs.
|
|
|
|
clearViewState()
|
|
|
|
return
|
|
|
|
}
|
2017-09-30 03:20:04 +02:00
|
|
|
guard let asset = pickBestAsset() else {
|
2017-10-04 17:55:15 +02:00
|
|
|
clearViewState()
|
2017-09-29 05:42:57 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
guard let image = YYImage(contentsOfFile:asset.filePath) else {
|
|
|
|
owsFail("\(TAG) could not load asset.")
|
2017-10-04 17:55:15 +02:00
|
|
|
clearViewState()
|
2017-09-29 05:42:57 +02:00
|
|
|
return
|
|
|
|
}
|
2017-09-30 03:20:04 +02:00
|
|
|
if imageView == nil {
|
|
|
|
let imageView = YYAnimatedImageView()
|
|
|
|
self.imageView = imageView
|
|
|
|
self.contentView.addSubview(imageView)
|
2017-10-02 20:26:03 +02:00
|
|
|
imageView.autoPinToSuperviewEdges()
|
2017-09-30 03:20:04 +02:00
|
|
|
}
|
|
|
|
guard let imageView = imageView else {
|
|
|
|
owsFail("\(TAG) missing imageview.")
|
2017-10-04 17:55:15 +02:00
|
|
|
clearViewState()
|
2017-09-30 03:20:04 +02:00
|
|
|
return
|
|
|
|
}
|
2017-09-29 05:42:57 +02:00
|
|
|
imageView.image = image
|
2017-10-04 17:55:15 +02:00
|
|
|
self.backgroundColor = nil
|
|
|
|
}
|
|
|
|
|
2017-10-19 23:12:50 +02:00
|
|
|
public func requestRenditionForSending() -> Promise<GiphyAsset> {
|
Download smaller GIF for previews.
Previously we were downloading a full sized GIF for each cell, which can
take dozens of seconds on a slower connection. Now we download a smaller
GIF for the picker view, and only download the full sized GIF for the
selected cell.
Some stats:
Before:
Scenario: search "Cat" and no scrolling, no picking
~10 MB
Scenario: search "Cat" and no scrolling, then pick
~10 MB
Scenario: search "Cat" and scroll 3 screens, no picking
~30 MB
Scenario: search "Cat" and scroll 3 screens, then pick
~30 MB
After:
Scenarios: search "Cat" and no scrolling, no picking
~1.0 MB (savings 90%)
Scenarios: search "Cat" and no scrolling, then pick
~3.5 MB (savings 65%)
Scenarios: search "Cat" and scroll 3 screens, no picking
~3.0 MB (savings 90%)
Scenarios: search "Cat" and scroll 3 screens, then pick
~5.5 MB (savings 81%)
// FREEBIE
2017-10-19 22:36:45 +02:00
|
|
|
guard let renditionForSending = self.renditionForSending else {
|
|
|
|
owsFail("\(TAG) renditionForSending was unexpectedly nil")
|
2017-10-19 23:46:50 +02:00
|
|
|
return Promise(error: GiphyError.assertionError(description: "renditionForSending was unexpectedly nil"))
|
Download smaller GIF for previews.
Previously we were downloading a full sized GIF for each cell, which can
take dozens of seconds on a slower connection. Now we download a smaller
GIF for the picker view, and only download the full sized GIF for the
selected cell.
Some stats:
Before:
Scenario: search "Cat" and no scrolling, no picking
~10 MB
Scenario: search "Cat" and no scrolling, then pick
~10 MB
Scenario: search "Cat" and scroll 3 screens, no picking
~30 MB
Scenario: search "Cat" and scroll 3 screens, then pick
~30 MB
After:
Scenarios: search "Cat" and no scrolling, no picking
~1.0 MB (savings 90%)
Scenarios: search "Cat" and no scrolling, then pick
~3.5 MB (savings 65%)
Scenarios: search "Cat" and scroll 3 screens, no picking
~3.0 MB (savings 90%)
Scenarios: search "Cat" and scroll 3 screens, then pick
~5.5 MB (savings 81%)
// FREEBIE
2017-10-19 22:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2017-10-19 23:12:50 +02:00
|
|
|
_ = GiphyDownloader
|
|
|
|
.sharedInstance.requestAsset(rendition: renditionForSending,
|
|
|
|
priority: .high,
|
|
|
|
success: { _, asset in
|
|
|
|
fulfill(asset)
|
Download smaller GIF for previews.
Previously we were downloading a full sized GIF for each cell, which can
take dozens of seconds on a slower connection. Now we download a smaller
GIF for the picker view, and only download the full sized GIF for the
selected cell.
Some stats:
Before:
Scenario: search "Cat" and no scrolling, no picking
~10 MB
Scenario: search "Cat" and no scrolling, then pick
~10 MB
Scenario: search "Cat" and scroll 3 screens, no picking
~30 MB
Scenario: search "Cat" and scroll 3 screens, then pick
~30 MB
After:
Scenarios: search "Cat" and no scrolling, no picking
~1.0 MB (savings 90%)
Scenarios: search "Cat" and no scrolling, then pick
~3.5 MB (savings 65%)
Scenarios: search "Cat" and scroll 3 screens, no picking
~3.0 MB (savings 90%)
Scenarios: search "Cat" and scroll 3 screens, then pick
~5.5 MB (savings 81%)
// FREEBIE
2017-10-19 22:36:45 +02:00
|
|
|
},
|
2017-10-19 23:12:50 +02:00
|
|
|
failure: { _ in
|
|
|
|
// TODO GiphyDownloader API shoudl pass through a useful failing error
|
|
|
|
// so we can pass it through here
|
2017-10-19 23:46:50 +02:00
|
|
|
Logger.error("\(self.TAG) request failed")
|
|
|
|
reject(GiphyError.fetchFailure)
|
Download smaller GIF for previews.
Previously we were downloading a full sized GIF for each cell, which can
take dozens of seconds on a slower connection. Now we download a smaller
GIF for the picker view, and only download the full sized GIF for the
selected cell.
Some stats:
Before:
Scenario: search "Cat" and no scrolling, no picking
~10 MB
Scenario: search "Cat" and no scrolling, then pick
~10 MB
Scenario: search "Cat" and scroll 3 screens, no picking
~30 MB
Scenario: search "Cat" and scroll 3 screens, then pick
~30 MB
After:
Scenarios: search "Cat" and no scrolling, no picking
~1.0 MB (savings 90%)
Scenarios: search "Cat" and no scrolling, then pick
~3.5 MB (savings 65%)
Scenarios: search "Cat" and scroll 3 screens, no picking
~3.0 MB (savings 90%)
Scenarios: search "Cat" and scroll 3 screens, then pick
~5.5 MB (savings 81%)
// FREEBIE
2017-10-19 22:36:45 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
return promise
|
|
|
|
}
|
|
|
|
|
2017-10-04 17:55:15 +02:00
|
|
|
private func clearViewState() {
|
|
|
|
imageView?.image = nil
|
|
|
|
self.backgroundColor = UIColor(white:0.95, alpha:1.0)
|
2017-09-30 03:20:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|