about summary refs log tree commit diff
path: root/modules/expo-bluesky-gif-view/ios/GifView.swift
diff options
context:
space:
mode:
Diffstat (limited to 'modules/expo-bluesky-gif-view/ios/GifView.swift')
-rw-r--r--modules/expo-bluesky-gif-view/ios/GifView.swift185
1 files changed, 185 insertions, 0 deletions
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
+    ])
+  }
+}