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
This commit is contained in:
Michael Kirk 2017-10-19 13:36:45 -07:00
parent 2a4c6506fb
commit ddf2fe21a1
3 changed files with 87 additions and 38 deletions

View File

@ -3,11 +3,16 @@
//
import Foundation
import PromiseKit
class GifPickerCell: UICollectionViewCell {
let TAG = "[GifPickerCell]"
// MARK: Properties
enum GifPickerCellError: Error {
case assertionError(description: String)
case fetchFailure
}
var imageInfo: GiphyImageInfo? {
didSet {
@ -35,6 +40,9 @@ class GifPickerCell: UICollectionViewCell {
var animatedAsset: GiphyAsset?
var imageView: YYAnimatedImageView?
// As another bandwidth saving measure, we only fetch the full sized GIF when the user selects it.
private var renditionForSending: GiphyRendition?
// MARK: Initializers
deinit {
@ -93,6 +101,16 @@ class GifPickerCell: UICollectionViewCell {
clearAssetRequests()
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.
guard let animatedRendition = imageInfo.pickAnimatedRendition() else {
@ -190,6 +208,31 @@ class GifPickerCell: UICollectionViewCell {
self.backgroundColor = nil
}
public func fetchRenditionForSending() -> Promise<GiphyAsset> {
guard let renditionForSending = self.renditionForSending else {
owsFail("\(TAG) renditionForSending was unexpectedly nil")
return Promise(error: GifPickerCellError.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
reject(GifPickerCellError.fetchFailure)
})
return promise
}
private func clearViewState() {
imageView?.image = nil
self.backgroundColor = UIColor(white:0.95, alpha:1.0)

View File

@ -31,8 +31,8 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
public weak var delegate: GifPickerViewControllerDelegate?
var thread: TSThread?
var messageSender: MessageSender?
let thread: TSThread
let messageSender: MessageSender
let searchBar: UISearchBar
let layout: GifPickerLayout
@ -40,7 +40,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
var noResultsView: UILabel?
var searchErrorView: UILabel?
var activityIndicator: UIActivityIndicatorView?
var selectedCell: UICollectionViewCell?
var imageInfos = [GiphyImageInfo]()
var reachability: Reachability?
@ -53,15 +53,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
@available(*, unavailable, message:"use other constructor instead.")
required init?(coder aDecoder: NSCoder) {
self.thread = nil
self.messageSender = nil
self.searchBar = UISearchBar()
self.layout = GifPickerLayout()
self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.layout)
super.init(coder: aDecoder)
owsFail("\(self.TAG) invalid constructor")
fatalError("\(#function) is unimplemented.")
}
required init(thread: TSThread, messageSender: MessageSender) {
@ -295,36 +287,36 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
// MARK: - UICollectionViewDelegate
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? GifPickerCell else {
owsFail("\(TAG) unexpected cell.")
return
}
guard let asset = cell.animatedAsset else {
Logger.info("\(TAG) unload cell selected.")
return
}
let filePath = asset.filePath
guard let dataSource = DataSourcePath.dataSource(withFilePath: filePath) else {
owsFail("\(TAG) couldn't load asset.")
return
}
let attachment = SignalAttachment(dataSource: dataSource, dataUTI: asset.rendition.utiType)
guard let thread = thread else {
owsFail("\(TAG) Missing thread.")
return
}
guard let messageSender = messageSender else {
owsFail("\(TAG) Missing messageSender.")
guard self.selectedCell == nil else {
owsFail("\(TAG) Already selected cell")
return
}
self.selectedCell = cell
self.delegate?.gifPickerWillSend()
// TODO disable collection view scroll/selection
// TODO show loading
cell.fetchRenditionForSending().then { (asset: GiphyAsset) -> Void in
let filePath = asset.filePath
guard let dataSource = DataSourcePath.dataSource(withFilePath: filePath) else {
owsFail("\(self.TAG) couldn't load asset.")
return
}
let attachment = SignalAttachment(dataSource: dataSource, dataUTI: asset.rendition.utiType)
let outgoingMessage = ThreadUtil.sendMessage(with: attachment, in: thread, messageSender: messageSender)
self.delegate?.gifPickerWillSend()
self.delegate?.gifPickerDidSend(outgoingMessage: outgoingMessage)
let outgoingMessage = ThreadUtil.sendMessage(with: attachment, in: self.thread, messageSender: self.messageSender)
dismiss(animated: true, completion: nil)
self.delegate?.gifPickerDidSend(outgoingMessage: outgoingMessage)
self.dismiss(animated: true, completion: nil)
}.retainUntilComplete()
}
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {

View File

@ -89,8 +89,9 @@ enum GiphyFormat {
// TODO: We may need to tweak these constants.
let kMaxDimension = UInt(618)
let kMinDimension = UInt(101)
let kMaxFileSize = UInt(3 * 1024 * 1024)
let kMinDimension = UInt(60)
let kPreferedPreviewFileSize = UInt(256 * 1024)
let kPreferedSendingFileSize = UInt(3 * 1024 * 1024)
private enum PickingStrategy {
case smallerIsBetter, largerIsBetter
@ -105,20 +106,33 @@ enum GiphyFormat {
public func pickStillRendition() -> GiphyRendition? {
// Stills are just temporary placeholders, so use the smallest still possible.
return pickRendition(isStill:true, pickingStrategy:.smallerIsBetter, maxFileSize:kMaxFileSize)
return pickRendition(isStill:true, pickingStrategy:.smallerIsBetter, maxFileSize:kPreferedPreviewFileSize)
}
public func pickAnimatedRendition() -> GiphyRendition? {
// Try to pick a small file...
if let rendition = pickRendition(isStill:false, pickingStrategy:.largerIsBetter, maxFileSize:kMaxFileSize) {
if let rendition = pickRendition(isStill:false, pickingStrategy:.largerIsBetter, maxFileSize:kPreferedPreviewFileSize) {
return rendition
}
// ...but gradually relax the file restriction...
if let rendition = pickRendition(isStill:false, pickingStrategy:.smallerIsBetter, maxFileSize:kMaxFileSize * 2) {
if let rendition = pickRendition(isStill:false, pickingStrategy:.smallerIsBetter, maxFileSize:kPreferedPreviewFileSize * 2) {
return rendition
}
// ...and relax even more until we find an animated rendition.
return pickRendition(isStill:false, pickingStrategy:.smallerIsBetter, maxFileSize:kMaxFileSize * 3)
return pickRendition(isStill:false, pickingStrategy:.smallerIsBetter, maxFileSize:kPreferedPreviewFileSize * 3)
}
public func pickHighQualityAnimatedRendition() -> GiphyRendition? {
// Try to pick a small file...
if let rendition = pickRendition(isStill:false, pickingStrategy:.largerIsBetter, maxFileSize:kPreferedSendingFileSize) {
return rendition
}
// ...but gradually relax the file restriction...
if let rendition = pickRendition(isStill:false, pickingStrategy:.smallerIsBetter, maxFileSize:kPreferedSendingFileSize * 2) {
return rendition
}
// ...and relax even more until we find an animated rendition.
return pickRendition(isStill:false, pickingStrategy:.smallerIsBetter, maxFileSize:kPreferedSendingFileSize * 3)
}
// Picking a rendition must be done very carefully.