diff options
Diffstat (limited to 'modules/expo-bluesky-gif-view/ios')
4 files changed, 272 insertions, 0 deletions
diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec new file mode 100644 index 000000000..ddd0877b2 --- /dev/null +++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'ExpoBlueskyGifView' + s.version = '1.0.0' + s.summary = 'A simple GIF player for Bluesky' + s.description = 'A simple GIF player for Bluesky' + s.author = '' + s.homepage = 'https://github.com/bluesky-social/social-app' + s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + s.dependency 'SDWebImage', '~> 5.17.0' + s.dependency 'SDWebImageWebPCoder', '~> 0.13.0' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift new file mode 100644 index 000000000..7c7132290 --- /dev/null +++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift @@ -0,0 +1,47 @@ +import ExpoModulesCore +import SDWebImage +import SDWebImageWebPCoder + +public class ExpoBlueskyGifViewModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyGifView") + + OnCreate { + SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared) + } + + AsyncFunction("prefetchAsync") { (sources: [URL]) in + SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil) + } + + View(GifView.self) { + Events( + "onPlayerStateChange" + ) + + Prop("source") { (view: GifView, prop: String) in + view.source = prop + } + + Prop("placeholderSource") { (view: GifView, prop: String) in + view.placeholderSource = prop + } + + Prop("autoplay") { (view: GifView, prop: Bool) in + view.autoplay = prop + } + + AsyncFunction("toggleAsync") { (view: GifView) in + view.toggle() + } + + AsyncFunction("playAsync") { (view: GifView) in + view.play() + } + + AsyncFunction("pauseAsync") { (view: GifView) in + view.pause() + } + } + } +} diff --git a/modules/expo-bluesky-gif-view/ios/GifView.swift b/modules/expo-bluesky-gif-view/ios/GifView.swift new file mode 100644 index 000000000..de722d7a6 --- /dev/null +++ b/modules/expo-bluesky-gif-view/ios/GifView.swift @@ -0,0 +1,185 @@ +import ExpoModulesCore +import SDWebImage +import SDWebImageWebPCoder + +typealias SDWebImageContext = [SDWebImageContextOption: Any] + +public class GifView: ExpoView, AVPlayerViewControllerDelegate { + // Events + private let onPlayerStateChange = EventDispatcher() + + // SDWebImage + private let imageView = SDAnimatedImageView(frame: .zero) + private let imageManager = SDWebImageManager( + cache: SDImageCache.shared, + loader: SDImageLoadersManager.shared + ) + private var isPlaying = true + private var isLoaded = false + + // Requests + private var webpOperation: SDWebImageCombinedOperation? + private var placeholderOperation: SDWebImageCombinedOperation? + + // Props + var source: String? = nil + var placeholderSource: String? = nil + var autoplay = true { + didSet { + if !autoplay { + self.pause() + } else { + self.play() + } + } + } + + // MARK: - Lifecycle + + public required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + self.clipsToBounds = true + + self.imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.imageView.layer.masksToBounds = false + self.imageView.backgroundColor = .clear + self.imageView.contentMode = .scaleToFill + + // We have to explicitly set this to false. If we don't, every time + // the view comes into the viewport, it will start animating again + self.imageView.autoPlayAnimatedImage = false + + self.addSubview(self.imageView) + } + + public override func willMove(toWindow newWindow: UIWindow?) { + if newWindow == nil { + // Don't cancel the placeholder operation, because we really want that to complete for + // when we scroll back up + self.webpOperation?.cancel() + self.placeholderOperation?.cancel() + } else if self.imageView.image == nil { + self.load() + } + } + + // MARK: - Loading + + private func load() { + guard let source = self.source, let placeholderSource = self.placeholderSource else { + return + } + + self.webpOperation?.cancel() + self.placeholderOperation?.cancel() + + // We only need to start an operation for the placeholder if it doesn't exist + // in the cache already. Cache key is by default the absolute URL of the image. + // See: + // https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently + if !SDImageCache.shared.diskImageDataExists(withKey: source), + let url = URL(string: placeholderSource) + { + self.placeholderOperation = imageManager.loadImage( + with: url, + options: [.retryFailed], + context: Util.createContext(), + progress: onProgress(_:_:_:), + completed: onLoaded(_:_:_:_:_:_:) + ) + } + + if let url = URL(string: source) { + self.webpOperation = imageManager.loadImage( + with: url, + options: [.retryFailed], + context: Util.createContext(), + progress: onProgress(_:_:_:), + completed: onLoaded(_:_:_:_:_:_:) + ) + } + } + + private func setImage(_ image: UIImage) { + if self.imageView.image == nil || image.sd_isAnimated { + self.imageView.image = image + } + + if image.sd_isAnimated { + self.firePlayerStateChange() + if isPlaying { + self.imageView.startAnimating() + } + } + } + + // MARK: - Loading blocks + + private func onProgress(_ receivedSize: Int, _ expectedSize: Int, _ imageUrl: URL?) {} + + private func onLoaded( + _ image: UIImage?, + _ data: Data?, + _ error: Error?, + _ cacheType: SDImageCacheType, + _ finished: Bool, + _ imageUrl: URL? + ) { + guard finished else { + return + } + + if let placeholderSource = self.placeholderSource, + imageUrl?.absoluteString == placeholderSource, + self.imageView.image == nil, + let image = image + { + self.setImage(image) + return + } + + if let source = self.source, + imageUrl?.absoluteString == source, + // UIImage perf suckssss if the image is animated + let data = data, + let animatedImage = SDAnimatedImage(data: data) + { + self.placeholderOperation?.cancel() + self.isPlaying = self.autoplay + self.isLoaded = true + self.setImage(animatedImage) + self.firePlayerStateChange() + } + } + + // MARK: - Playback Controls + + func play() { + self.imageView.startAnimating() + self.isPlaying = true + self.firePlayerStateChange() + } + + func pause() { + self.imageView.stopAnimating() + self.isPlaying = false + self.firePlayerStateChange() + } + + func toggle() { + if self.isPlaying { + self.pause() + } else { + self.play() + } + } + + // MARK: - Util + + private func firePlayerStateChange() { + onPlayerStateChange([ + "isPlaying": self.isPlaying, + "isLoaded": self.isLoaded + ]) + } +} diff --git a/modules/expo-bluesky-gif-view/ios/Util.swift b/modules/expo-bluesky-gif-view/ios/Util.swift new file mode 100644 index 000000000..55ed4152a --- /dev/null +++ b/modules/expo-bluesky-gif-view/ios/Util.swift @@ -0,0 +1,17 @@ +import SDWebImage + +class Util { + static func createContext() -> SDWebImageContext { + var context = SDWebImageContext() + + // SDAnimatedImage for some reason has issues whenever loaded from memory. Instead, we + // will just use the disk. SDWebImage will manage this cache for us, so we don't need + // to worry about clearing it. + context[.originalQueryCacheType] = SDImageCacheType.disk.rawValue + context[.originalStoreCacheType] = SDImageCacheType.disk.rawValue + context[.queryCacheType] = SDImageCacheType.disk.rawValue + context[.storeCacheType] = SDImageCacheType.disk.rawValue + + return context + } +} |