about summary refs log tree commit diff
path: root/modules/expo-bluesky-gif-view/ios
diff options
context:
space:
mode:
Diffstat (limited to 'modules/expo-bluesky-gif-view/ios')
-rw-r--r--modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec23
-rw-r--r--modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift47
-rw-r--r--modules/expo-bluesky-gif-view/ios/GifView.swift185
-rw-r--r--modules/expo-bluesky-gif-view/ios/Util.swift17
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
+  }
+}