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
pull/1/head
Michael Kirk 8 years ago
parent 2a4c6506fb
commit ddf2fe21a1

@ -3,11 +3,16 @@
// //
import Foundation import Foundation
import PromiseKit
class GifPickerCell: UICollectionViewCell { class GifPickerCell: UICollectionViewCell {
let TAG = "[GifPickerCell]" let TAG = "[GifPickerCell]"
// MARK: Properties // MARK: Properties
enum GifPickerCellError: Error {
case assertionError(description: String)
case fetchFailure
}
var imageInfo: GiphyImageInfo? { var imageInfo: GiphyImageInfo? {
didSet { didSet {
@ -35,6 +40,9 @@ class GifPickerCell: UICollectionViewCell {
var animatedAsset: GiphyAsset? var animatedAsset: GiphyAsset?
var imageView: YYAnimatedImageView? 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 // MARK: Initializers
deinit { deinit {
@ -93,6 +101,16 @@ class GifPickerCell: UICollectionViewCell {
clearAssetRequests() clearAssetRequests()
return 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. // The Giphy API returns a slew of "renditions" for a given image.
// It's critical that we carefully "pick" the best rendition to use. // It's critical that we carefully "pick" the best rendition to use.
guard let animatedRendition = imageInfo.pickAnimatedRendition() else { guard let animatedRendition = imageInfo.pickAnimatedRendition() else {
@ -190,6 +208,31 @@ class GifPickerCell: UICollectionViewCell {
self.backgroundColor = nil 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() { private func clearViewState() {
imageView?.image = nil imageView?.image = nil
self.backgroundColor = UIColor(white:0.95, alpha:1.0) self.backgroundColor = UIColor(white:0.95, alpha:1.0)

@ -31,8 +31,8 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
public weak var delegate: GifPickerViewControllerDelegate? public weak var delegate: GifPickerViewControllerDelegate?
var thread: TSThread? let thread: TSThread
var messageSender: MessageSender? let messageSender: MessageSender
let searchBar: UISearchBar let searchBar: UISearchBar
let layout: GifPickerLayout let layout: GifPickerLayout
@ -40,7 +40,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
var noResultsView: UILabel? var noResultsView: UILabel?
var searchErrorView: UILabel? var searchErrorView: UILabel?
var activityIndicator: UIActivityIndicatorView? var activityIndicator: UIActivityIndicatorView?
var selectedCell: UICollectionViewCell?
var imageInfos = [GiphyImageInfo]() var imageInfos = [GiphyImageInfo]()
var reachability: Reachability? var reachability: Reachability?
@ -53,15 +53,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
@available(*, unavailable, message:"use other constructor instead.") @available(*, unavailable, message:"use other constructor instead.")
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
self.thread = nil fatalError("\(#function) is unimplemented.")
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")
} }
required init(thread: TSThread, messageSender: MessageSender) { required init(thread: TSThread, messageSender: MessageSender) {
@ -295,36 +287,36 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
// MARK: - UICollectionViewDelegate // MARK: - UICollectionViewDelegate
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? GifPickerCell else { guard let cell = collectionView.cellForItem(at: indexPath) as? GifPickerCell else {
owsFail("\(TAG) unexpected cell.") owsFail("\(TAG) unexpected cell.")
return return
} }
guard let asset = cell.animatedAsset else {
Logger.info("\(TAG) unload cell selected.") guard self.selectedCell == nil else {
return owsFail("\(TAG) Already selected cell")
}
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.")
return return
} }
self.selectedCell = cell
// 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)
self.delegate?.gifPickerWillSend() self.delegate?.gifPickerWillSend()
let outgoingMessage = ThreadUtil.sendMessage(with: attachment, in: thread, messageSender: messageSender) let outgoingMessage = ThreadUtil.sendMessage(with: attachment, in: self.thread, messageSender: self.messageSender)
self.delegate?.gifPickerDidSend(outgoingMessage: outgoingMessage) self.delegate?.gifPickerDidSend(outgoingMessage: outgoingMessage)
dismiss(animated: true, completion: nil) self.dismiss(animated: true, completion: nil)
}.retainUntilComplete()
} }
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {

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

Loading…
Cancel
Save