From 6e4f89df4a5a799ef6b6568710c731576db27b5e Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 10 Sep 2019 12:25:50 -0400 Subject: [PATCH] Add support for video attachments #7 --- .../Gallery/GalleryViewController.swift | 53 +++++++++++++++---- .../GalleryExpandAnimationController.swift | 15 ++---- .../GalleryShrinkAnimationController.swift | 13 ++--- .../LargeImageViewController.swift | 2 +- .../LargeImageExpandAnimationController.swift | 3 +- .../LargeImageShrinkAnimationController.swift | 5 +- Tusker/TuskerNavigationDelegate.swift | 28 +++++----- Tusker/Views/Attachments/AttachmentView.swift | 52 ++++++++++++++++-- .../AttachmentsContainerView.swift | 2 +- 9 files changed, 119 insertions(+), 54 deletions(-) diff --git a/Tusker/Screens/Gallery/GalleryViewController.swift b/Tusker/Screens/Gallery/GalleryViewController.swift index 0d55f5e4..034316f5 100644 --- a/Tusker/Screens/Gallery/GalleryViewController.swift +++ b/Tusker/Screens/Gallery/GalleryViewController.swift @@ -7,6 +7,8 @@ import UIKit import Pachyderm +import AVFoundation +import AVKit class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { @@ -16,10 +18,10 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc let sourcesInfo: [LargeImageViewController.SourceInfo?] let startIndex: Int - let pages: [AttachmentViewController] + let pages: [UIViewController] var currentIndex: Int { - guard let vc = viewControllers?.first as? AttachmentViewController, + guard let vc = viewControllers?.first, let index = pages.firstIndex(of: vc) else { fatalError() } @@ -39,7 +41,18 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc self.sourcesInfo = sourcesInfo self.startIndex = startIndex - self.pages = attachments.map(AttachmentViewController.init) + self.pages = attachments.map { + switch $0.kind { + case .image: + return AttachmentViewController(attachment: $0) + case .video: + let vc = AVPlayerViewController() + vc.player = AVPlayer(url: $0.url) + return vc + default: + fatalError() + } + } super.init(transitionStyle: .scroll, navigationOrientation: .horizontal) @@ -61,12 +74,24 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc dismissInteractionController = LargeImageInteractionController(viewController: self) } - + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let vc = pages[currentIndex] as? AVPlayerViewController { + // when the gallery is first shown, after the transition finishes, the controls for the player controller appear semi-transparent + // hiding the controls and then immediately reshowing them makes sure they're visible when the gallery is presented + vc.showsPlaybackControls = false + vc.showsPlaybackControls = true + + // begin playing the video as soon as we appear + vc.player?.play() + } + } + // MARK: - Page View Controller Data Source func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - guard let attachment = viewController as? AttachmentViewController, - let index = pages.firstIndex(of: attachment), + guard let index = pages.firstIndex(of: viewController), index > 0 else { return nil } @@ -74,8 +99,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - guard let attachment = viewController as? AttachmentViewController, - let index = pages.firstIndex(of: attachment), + guard let index = pages.firstIndex(of: viewController), index < pages.count - 1 else { return nil } @@ -84,9 +108,16 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc // MARK: - Page View Controller Delegate func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { - let pending = pendingViewControllers.first as! AttachmentViewController - let current = viewControllers!.first as! AttachmentViewController - pending.controlsVisible = current.controlsVisible + if let pending = pendingViewControllers.first as? AttachmentViewController, + let current = viewControllers!.first as? AttachmentViewController { + pending.controlsVisible = current.controlsVisible + } + + if let pending = pendingViewControllers.first as? AVPlayerViewController { + // show controls and begin playing when the player page becomes visible + pending.showsPlaybackControls = true + pending.player?.play() + } } } diff --git a/Tusker/Screens/Gallery/Transitions/GalleryExpandAnimationController.swift b/Tusker/Screens/Gallery/Transitions/GalleryExpandAnimationController.swift index 096cbdd3..0d5d9e72 100644 --- a/Tusker/Screens/Gallery/Transitions/GalleryExpandAnimationController.swift +++ b/Tusker/Screens/Gallery/Transitions/GalleryExpandAnimationController.swift @@ -21,7 +21,7 @@ class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransi } let finalVCFrame = transitionContext.finalFrame(for: toVC) - guard let (sourceFrame, sourceCornerRadius) = toVC.sourcesInfo[toVC.startIndex] else { + guard let (image, sourceFrame, sourceCornerRadius) = toVC.sourcesInfo[toVC.startIndex] else { toVC.view.frame = finalVCFrame transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return @@ -29,12 +29,8 @@ class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransi let attachment = toVC.attachments[toVC.startIndex] - guard let data = ImageCache.attachments.get(attachment.url), let image = UIImage(data: data) else { - toVC.view.frame = finalVCFrame - transitionContext.completeTransition(!transitionContext.transitionWasCancelled) - return - } - + let containerView = transitionContext.containerView + let ratio = image.size.width / image.size.height var width = finalVCFrame.width var height = width / ratio @@ -46,11 +42,10 @@ class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransi } let finalFrame = CGRect(x: finalVCFrame.midX - width / 2, y: finalVCFrame.midY - height / 2, width: width, height: height) - let containerView = transitionContext.containerView - let imageView = GIFImageView(frame: sourceFrame) imageView.image = image - if attachment.url.pathExtension == "gif" { + if attachment.url.pathExtension == "gif", + let data = ImageCache.attachments.get(attachment.url) { imageView.animate(withGIFData: data) } imageView.contentMode = .scaleAspectFill diff --git a/Tusker/Screens/Gallery/Transitions/GalleryShrinkAnimationController.swift b/Tusker/Screens/Gallery/Transitions/GalleryShrinkAnimationController.swift index 48f96de2..0bd2022f 100644 --- a/Tusker/Screens/Gallery/Transitions/GalleryShrinkAnimationController.swift +++ b/Tusker/Screens/Gallery/Transitions/GalleryShrinkAnimationController.swift @@ -26,20 +26,14 @@ class GalleryShrinkAnimationController: NSObject, UIViewControllerAnimatedTransi return } - guard let (sourceFrame, sourceCornerRadius) = fromVC.sourcesInfo[fromVC.currentIndex] else { + guard let (image, sourceFrame, sourceCornerRadius) = fromVC.sourcesInfo[fromVC.currentIndex] else { transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } let originalVCFrame = fromVC.view.frame let attachment = fromVC.attachments[fromVC.currentIndex] - - guard let data = ImageCache.attachments.get(attachment.url), - let image = UIImage(data: data) else { - transitionContext.completeTransition(!transitionContext.transitionWasCancelled) - return - } - + let ratio = image.size.width / image.size.height var width = originalVCFrame.width var height = width / ratio @@ -53,7 +47,8 @@ class GalleryShrinkAnimationController: NSObject, UIViewControllerAnimatedTransi let imageView = GIFImageView(frame: originalFrame) imageView.image = image - if attachment.url.pathExtension == "gif" { + if attachment.url.pathExtension == "gif", + let data = ImageCache.attachments.get(attachment.url) { imageView.animate(withGIFData: data) } imageView.contentMode = .scaleAspectFill diff --git a/Tusker/Screens/Large Image/LargeImageViewController.swift b/Tusker/Screens/Large Image/LargeImageViewController.swift index d36649d2..69e42c6f 100644 --- a/Tusker/Screens/Large Image/LargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LargeImageViewController.swift @@ -13,7 +13,7 @@ import Gifu class LargeImageViewController: UIViewController, UIScrollViewDelegate { - typealias SourceInfo = (frame: CGRect, cornerRadius: CGFloat) + typealias SourceInfo = (image: UIImage, frame: CGRect, cornerRadius: CGFloat) var sourceInfo: SourceInfo? var dismissInteractionController: LargeImageInteractionController? diff --git a/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift b/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift index af9086cb..086a010f 100644 --- a/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift +++ b/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift @@ -22,14 +22,13 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra } let finalVCFrame = transitionContext.finalFrame(for: toVC) - guard let (originFrame, originCornerRadius) = toVC.sourceInfo else { + guard let (image, originFrame, originCornerRadius) = toVC.sourceInfo else { toVC.view.frame = finalVCFrame transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } let containerView = transitionContext.containerView - let image = toVC.imageView.image! let ratio = image.size.width / image.size.height let width = finalVCFrame.width let height = width / ratio diff --git a/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift b/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift index 8a063a7a..ef10f080 100644 --- a/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift +++ b/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift @@ -27,7 +27,7 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra return } - guard let (finalFrame, finalCornerRadius) = fromVC.sourceInfo else { + guard let (image, finalFrame, finalCornerRadius) = fromVC.sourceInfo else { transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } @@ -35,14 +35,13 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra let originalVCFrame = fromVC.view.frame let containerView = transitionContext.containerView - let image = fromVC.image! let ratio = image.size.width / image.size.height let width = originalVCFrame.width let height = width / ratio let originalFrame = CGRect(x: originalVCFrame.midX - width / 2, y: originalVCFrame.midY - height / 2, width: width, height: height) let imageView = GIFImageView(frame: originalFrame) - imageView.image = fromVC.image! + imageView.image = image if let gifData = fromVC.gifData { imageView.animate(withGIFData: gifData) } diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index ad1c2dd4..40d0b2c3 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -28,17 +28,17 @@ protocol TuskerNavigationDelegate { func reply(to statusID: String) - func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController + func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController - func largeImage(gifData: Data, description: String?, sourceView: UIView) -> LargeImageViewController + func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController - func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIView) + func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView) - func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIView) + func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView) - func gallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) -> GalleryViewController + func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController - func showGallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) + func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) func showMoreOptions(forStatus statusID: String) @@ -109,7 +109,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { present(vc, animated: true) } - private func sourceViewInfo(_ sourceView: UIView?) -> LargeImageViewController.SourceInfo? { + private func sourceViewInfo(_ sourceView: UIImageView?) -> LargeImageViewController.SourceInfo? { guard let sourceView = sourceView else { return nil } var sourceFrame = sourceView.convert(sourceView.bounds, to: view) @@ -121,38 +121,38 @@ extension TuskerNavigationDelegate where Self: UIViewController { let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY sourceFrame = CGRect(x: x, y: y, width: width, height: height) } - return (frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius) + return (image: sourceView.image!, frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius) } - func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController { + func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController { let vc = LargeImageViewController(image: image, description: description, sourceInfo: sourceViewInfo(sourceView)) vc.transitioningDelegate = self return vc } - func largeImage(gifData: Data, description: String?, sourceView: UIView) -> LargeImageViewController { + func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController { let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceInfo: sourceViewInfo(sourceView)) vc.transitioningDelegate = self vc.gifData = gifData return vc } - func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIView) { + func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView) { present(largeImage(image, description: description, sourceView: sourceView), animated: true) } - func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIView) { + func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView) { present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true) } - func gallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) -> GalleryViewController { + func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController { let sourcesInfo = sourceViews.map(sourceViewInfo) let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex) vc.transitioningDelegate = self return vc } - func showGallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) { + func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) { present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true) } diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index e777676d..6771d73e 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -9,6 +9,7 @@ import UIKit import Pachyderm import Gifu +import AVFoundation protocol AttachmentViewDelegate { func showAttachmentsGallery(startingAt index: Int) @@ -17,6 +18,8 @@ protocol AttachmentViewDelegate { class AttachmentView: UIImageView, GIFAnimatable { var delegate: AttachmentViewDelegate? + + var playImageView: UIImageView! var attachment: Attachment! var index: Int! @@ -31,7 +34,7 @@ class AttachmentView: UIImageView, GIFAnimatable { self.attachment = attachment self.index = index - loadImage() + loadAttachment() } required init?(coder aDecoder: NSCoder) { @@ -44,6 +47,27 @@ class AttachmentView: UIImageView, GIFAnimatable { layer.masksToBounds = true isUserInteractionEnabled = true addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed))) + + playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill")) + playImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(playImageView) + NSLayoutConstraint.activate([ + playImageView.widthAnchor.constraint(equalToConstant: 50), + playImageView.heightAnchor.constraint(equalToConstant: 50), + playImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + playImageView.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } + + func loadAttachment() { + switch attachment.kind { + case .image: + loadImage() + case .video: + loadVideo() + default: + fatalError() + } } func loadImage() { @@ -58,6 +82,22 @@ class AttachmentView: UIImageView, GIFAnimatable { } } } + + playImageView.isHidden = true + } + + func loadVideo() { + DispatchQueue.global(qos: .userInitiated).async { + let asset = AVURLAsset(url: self.attachment.url) + let generator = AVAssetImageGenerator(asset: asset) + generator.appliesPreferredTrackTransform = true + guard let image = try? generator.copyCGImage(at: CMTime(seconds: 0, preferredTimescale: 1), actualTime: nil) else { return } + DispatchQueue.main.async { + self.image = UIImage(cgImage: image) + } + } + + playImageView.isHidden = false } override func display(_ layer: CALayer) { @@ -65,8 +105,14 @@ class AttachmentView: UIImageView, GIFAnimatable { } @objc func imagePressed() { -// delegate?.showLargeAttachment(for: self) - delegate?.showAttachmentsGallery(startingAt: index) +// switch attachment.kind { +// case .image: + delegate?.showAttachmentsGallery(startingAt: index) +// case .video: +// delegate?.showVideo(attachment: attachment) +// default: +// fatalError() +// } } } diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index 34ef9a15..224f92de 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -48,7 +48,7 @@ class AttachmentsContainerView: UIView { func updateUI(status: Status) { self.statusID = status.id - attachments = status.attachments.filter { $0.kind == .image } + attachments = status.attachments.filter { $0.kind == .image || $0.kind == .video } attachmentViews.removeAllObjects() subviews.forEach { $0.removeFromSuperview() }