Load GIFs progressively using stills.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-09-29 21:20:04 -04:00
parent 2dfd7aa0e9
commit 4f77a2a504
4 changed files with 195 additions and 58 deletions

View File

@ -25,8 +25,10 @@ class GifPickerCell: UICollectionViewCell {
}
}
var assetRequest: GiphyAssetRequest?
var asset: GiphyAsset?
var stillAssetRequest: GiphyAssetRequest?
var stillAsset: GiphyAsset?
var fullAssetRequest: GiphyAssetRequest?
var fullAsset: GiphyAsset?
var imageView: YYAnimatedImageView?
// MARK: Initializers
@ -46,16 +48,29 @@ class GifPickerCell: UICollectionViewCell {
imageInfo = nil
shouldLoad = false
asset = nil
assetRequest?.cancel()
assetRequest = nil
stillAsset = nil
stillAssetRequest?.cancel()
stillAssetRequest = nil
fullAsset = nil
fullAssetRequest?.cancel()
fullAssetRequest = nil
imageView?.removeFromSuperview()
imageView = nil
}
private func clearStillAssetRequest() {
stillAssetRequest?.cancel()
stillAssetRequest = nil
}
private func clearFullAssetRequest() {
fullAssetRequest?.cancel()
fullAssetRequest = nil
}
private func clearAssetRequest() {
assetRequest?.cancel()
assetRequest = nil
clearStillAssetRequest()
clearFullAssetRequest()
}
private func ensureLoad() {
@ -67,31 +82,60 @@ class GifPickerCell: UICollectionViewCell {
clearAssetRequest()
return
}
guard self.assetRequest == nil else {
guard self.fullAsset == nil else {
return
}
guard let rendition = imageInfo.pickGifRendition() else {
Logger.warn("\(TAG) could not pick rendition")
guard let fullRendition = imageInfo.pickGifRendition() else {
Logger.warn("\(TAG) could not pick gif rendition: \(imageInfo.giphyId)")
// imageInfo.log()
clearAssetRequest()
return
}
// Logger.verbose("\(TAG) picked rendition: \(rendition.name)")
guard let stillRendition = imageInfo.pickStillRendition() else {
Logger.warn("\(TAG) could not pick still rendition: \(imageInfo.giphyId)")
// imageInfo.log()
clearAssetRequest()
return
}
// Logger.verbose("picked full: \(fullRendition.name)")
// Logger.verbose("picked still: \(stillRendition.name)")
assetRequest = GifDownloader.sharedInstance.downloadAssetAsync(rendition:rendition,
success: { [weak self] asset in
guard let strongSelf = self else { return }
strongSelf.clearAssetRequest()
strongSelf.asset = asset
strongSelf.tryToDisplayAsset()
},
failure: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.clearAssetRequest()
})
if stillAsset == nil && fullAsset == nil && stillAssetRequest == nil {
stillAssetRequest = GifDownloader.sharedInstance.downloadAssetAsync(rendition:stillRendition,
priority:.high,
success: { [weak self] asset in
// Logger.verbose("downloaded still")
guard let strongSelf = self else { return }
strongSelf.clearStillAssetRequest()
strongSelf.stillAsset = asset
strongSelf.tryToDisplayAsset()
},
failure: { [weak self] in
// Logger.verbose("failed to download still")
guard let strongSelf = self else { return }
strongSelf.clearStillAssetRequest()
})
}
if fullAsset == nil && fullAssetRequest == nil {
fullAssetRequest = GifDownloader.sharedInstance.downloadAssetAsync(rendition:fullRendition,
priority:.low,
success: { [weak self] asset in
// Logger.verbose("downloaded full")
guard let strongSelf = self else { return }
strongSelf.clearAssetRequest()
strongSelf.fullAsset = asset
strongSelf.tryToDisplayAsset()
},
failure: { [weak self] in
// Logger.verbose("failed to download full")
guard let strongSelf = self else { return }
strongSelf.clearAssetRequest()
})
}
}
private func tryToDisplayAsset() {
guard let asset = asset else {
guard let asset = pickBestAsset() else {
owsFail("\(TAG) missing asset.")
return
}
@ -99,11 +143,27 @@ class GifPickerCell: UICollectionViewCell {
owsFail("\(TAG) could not load asset.")
return
}
let imageView = YYAnimatedImageView()
self.imageView = imageView
if imageView == nil {
let imageView = YYAnimatedImageView()
self.imageView = imageView
self.contentView.addSubview(imageView)
imageView.autoPinWidthToSuperview()
imageView.autoPinHeightToSuperview()
}
guard let imageView = imageView else {
owsFail("\(TAG) missing imageview.")
return
}
imageView.image = image
self.contentView.addSubview(imageView)
imageView.autoPinWidthToSuperview()
imageView.autoPinHeightToSuperview()
}
private func pickBestAsset() -> GiphyAsset? {
if let fullAsset = fullAsset {
return fullAsset
}
if let stillAsset = stillAsset {
return stillAsset
}
return nil
}
}

View File

@ -164,7 +164,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
owsFail("\(TAG) unexpected cell.")
return
}
guard let asset = cell.asset else {
guard let asset = cell.fullAsset else {
Logger.info("\(TAG) unload cell selected.")
return
}

View File

@ -5,20 +5,27 @@
import Foundation
import ObjectiveC
enum GiphyRequestPriority {
case low, high
}
@objc class GiphyAssetRequest: NSObject {
static let TAG = "[GiphyAssetRequest]"
let rendition: GiphyRendition
let priority: GiphyRequestPriority
let success: ((GiphyAsset) -> Void)
let failure: (() -> Void)
var wasCancelled = false
var assetFilePath: String?
init(rendition: GiphyRendition,
priority: GiphyRequestPriority,
success:@escaping ((GiphyAsset) -> Void),
failure:@escaping (() -> Void)
) {
self.rendition = rendition
self.priority = priority
self.success = success
self.failure = failure
}
@ -121,6 +128,7 @@ extension URLSessionTask {
// The success and failure handlers are always called on main queue.
// The success and failure handlers may be called synchronously on cache hit.
public func downloadAssetAsync(rendition: GiphyRendition,
priority: GiphyRequestPriority,
success:@escaping ((GiphyAsset) -> Void),
failure:@escaping (() -> Void)) -> GiphyAssetRequest? {
AssertIsOnMainThread()
@ -132,6 +140,7 @@ extension URLSessionTask {
var hasRequestCompleted = false
let assetRequest = GiphyAssetRequest(rendition:rendition,
priority:priority,
success : { asset in
DispatchQueue.main.async {
// Ensure we call success or failure exactly once.
@ -171,14 +180,9 @@ extension URLSessionTask {
guard !self.isDownloading else {
return
}
guard self.assetRequestQueue.count > 0 else {
guard let assetRequest = self.popNextAssetRequest() else {
return
}
guard let assetRequest = self.assetRequestQueue.first else {
owsFail("\(GiphyAsset.TAG) could not pop asset requests")
return
}
self.assetRequestQueue.removeFirst()
guard !assetRequest.wasCancelled else {
DispatchQueue.main.async {
self.downloadIfNecessary()
@ -188,7 +192,7 @@ extension URLSessionTask {
self.isDownloading = true
if let asset = self.assetMap[assetRequest.rendition.url] {
// Deferred cache hit, avoids re-downloading assets already in the
// Deferred cache hit, avoids re-downloading assets already in the
// asset cache.
assetRequest.success(asset)
return
@ -206,6 +210,22 @@ extension URLSessionTask {
}
}
private func popNextAssetRequest() -> GiphyAssetRequest? {
AssertIsOnMainThread()
// var result : GiphyAssetRequest?
for priority in [GiphyRequestPriority.high, GiphyRequestPriority.low] {
for (assetRequestIndex, assetRequest) in assetRequestQueue.enumerated() {
if assetRequest.priority == priority {
assetRequestQueue.remove(at:assetRequestIndex)
return assetRequest
}
}
}
return nil
}
// MARK: URLSessionDataDelegate
@nonobjc

View File

@ -7,7 +7,7 @@ import ObjectiveC
// There's no UTI type for webp!
enum GiphyFormat {
case gif, mp4
case gif, mp4, jpg
}
@objc class GiphyRendition: NSObject {
@ -38,6 +38,8 @@ enum GiphyFormat {
return "gif"
case .mp4:
return "mp4"
case .jpg:
return "jpg"
}
}
@ -47,8 +49,14 @@ enum GiphyFormat {
return kUTTypeGIF as String
case .mp4:
return kUTTypeMPEG4 as String
case .jpg:
return kUTTypeJPEG as String
}
}
public func log() {
Logger.verbose("\t \(format), \(name), \(width), \(height), \(fileSize)")
}
}
@objc class GiphyImageInfo: NSObject {
@ -69,33 +77,67 @@ enum GiphyFormat {
let kMinDimension = UInt(101)
let kMaxFileSize = UInt(3 * 1024 * 1024)
public func log() {
Logger.verbose("giphyId: \(giphyId), \(renditions.count)")
for rendition in renditions {
rendition.log()
}
}
public func pickStillRendition() -> GiphyRendition? {
return pickRendition(isStill:true)
}
public func pickGifRendition() -> GiphyRendition? {
return pickRendition(isStill:false)
}
private func pickRendition(isStill: Bool) -> GiphyRendition? {
var bestRendition: GiphyRendition?
for rendition in renditions {
guard rendition.format == .gif else {
continue
}
guard !rendition.name.hasSuffix("_still")
else {
if isStill {
guard [.gif, .jpg].contains(rendition.format) else {
continue
}
guard !rendition.name.hasSuffix("_downsampled")
else {
continue
}
guard rendition.width >= kMinDimension &&
rendition.width <= kMaxDimension &&
rendition.height >= kMinDimension &&
rendition.height <= kMaxDimension &&
rendition.fileSize <= kMaxFileSize
else {
}
guard rendition.name.hasSuffix("_still") else {
continue
}
guard rendition.width >= kMinDimension &&
rendition.height >= kMinDimension &&
rendition.fileSize <= kMaxFileSize
else {
continue
}
} else {
guard rendition.format == .gif else {
continue
}
guard !rendition.name.hasSuffix("_still") else {
continue
}
guard !rendition.name.hasSuffix("_downsampled") else {
continue
}
guard rendition.width >= kMinDimension &&
rendition.width <= kMaxDimension &&
rendition.height >= kMinDimension &&
rendition.height <= kMaxDimension &&
rendition.fileSize <= kMaxFileSize
else {
continue
}
}
if let currentBestRendition = bestRendition {
if rendition.width > currentBestRendition.width {
bestRendition = rendition
if isStill {
if rendition.width < currentBestRendition.width {
bestRendition = rendition
}
} else {
if rendition.width > currentBestRendition.width {
bestRendition = rendition
}
}
} else {
bestRendition = rendition
@ -191,6 +233,8 @@ enum GiphyFormat {
// MARK: Parse API Responses
private func parseGiphyImages(responseJson:Any?) -> [GiphyImageInfo]? {
// Logger.verbose("\(responseJson)")
guard let responseJson = responseJson else {
Logger.error("\(GifManager.TAG) Missing response.")
return nil
@ -292,14 +336,27 @@ enum GiphyFormat {
Logger.warn("\(GifManager.TAG) Rendition url missing file extension.")
return nil
}
guard fileExtension.lowercased() == "gif" else {
// Logger.verbose("\(GifManager.TAG) Rendition has invalid type: \(fileExtension).")
var format = GiphyFormat.gif
if fileExtension.lowercased() == "gif" {
format = .gif
} else if fileExtension.lowercased() == "jpg" {
format = .jpg
} else if fileExtension.lowercased() == "mp4" {
format = .mp4
} else if fileExtension.lowercased() == "webp" {
return nil
} else {
Logger.warn("\(GifManager.TAG) Invalid file extension: \(fileExtension).")
return nil
}
// guard fileExtension.lowercased() == "gif" else {
//// Logger.verbose("\(GifManager.TAG) Rendition has invalid type: \(fileExtension).")
// return nil
// }
// Logger.debug("\(GifManager.TAG) Rendition successfully parsed.")
return GiphyRendition(
format : .gif,
format : format,
name : renditionName,
width : width,
height : height,