From 1f40cc9928a59ca4fe9cd18bdf52427b4a5e34a4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 17 Jun 2020 23:33:48 -0400 Subject: [PATCH] Show controls/description for gifv attachments See #98 --- Tusker.xcodeproj/project.pbxproj | 4 + .../GalleryViewController.swift | 25 ++++-- .../Large Image/LargeImageContentView.swift | 84 +++++++++++++++++++ .../LargeImageViewController.swift | 78 ++++++++--------- .../Large Image/LargeImageViewController.xib | 15 +--- .../LoadingLargeImageViewController.swift | 12 +-- Tusker/TuskerNavigationDelegate.swift | 29 ------- .../Attachments/GifvAttachmentView.swift | 4 +- 8 files changed, 154 insertions(+), 97 deletions(-) create mode 100644 Tusker/Screens/Large Image/LargeImageContentView.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 4f287394..2c4d5997 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; }; D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; }; D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; }; + D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; }; D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */; }; D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */; }; D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; }; @@ -471,6 +472,7 @@ D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = ""; }; D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = ""; }; D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = ""; }; + D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = ""; }; D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteConversationActivity.swift; sourceTree = ""; }; D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnmuteConversationActivity.swift; sourceTree = ""; }; D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = ""; }; @@ -942,6 +944,7 @@ D646C954213B364600269FB5 /* Transitions */, D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */, D6C94D862139E62700CB5196 /* LargeImageViewController.swift */, + D681A299249AD62D0085E54E /* LargeImageContentView.swift */, 041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */, ); path = "Large Image"; @@ -1770,6 +1773,7 @@ D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */, + D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */, D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */, D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */, D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */, diff --git a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift index e678b538..64843017 100644 --- a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift +++ b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift @@ -28,8 +28,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc var animationSourceView: UIImageView? { sourceViews[currentIndex] } var animationImage: UIImage? { - if let page = pages[currentIndex] as? LoadingLargeImageViewController, - let image = page.largeImageVC?.image { + if let page = pages[currentIndex] as? LargeImageAnimatableViewController, + let image = page.animationImage { return image } else { return animationSourceView?.image @@ -65,18 +65,29 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc self.sourceViews = WeakArray(sourceViews) self.startIndex = startIndex - self.pages = attachments.map { - switch $0.kind { + self.pages = attachments.enumerated().map { (index, attachment) in + switch attachment.kind { case .image: - let vc = LoadingLargeImageViewController(attachment: $0) + let vc = LoadingLargeImageViewController(attachment: attachment) vc.shrinkGestureEnabled = false return vc case .video, .audio: let vc = AVPlayerViewController() - vc.player = AVPlayer(url: $0.url) + vc.player = AVPlayer(url: attachment.url) return vc case .gifv: - return GifvAttachmentViewController(attachment: $0) + // Passing the source view to the LargeImageGifvContentView is a crappy workaround for not + // having the video size directly inside the content view. This will break when there + // are more than 4 attachments and there is a gifv at index >= 3 (the More... button will show + // in place of the fourth attachment, so there aren't source views for the attachments at index >= 3). + // Really, what should happen is the LargeImageGifvContentView should get the size of the video from + // the AVFoundation instead of the source view. + // This isn't a priority as only Mastodon converts gifs to gifvs, and Mastodon (in its default configuration, + // I don't know about forks) doesn't allow more than four attachments, meaning there will always be a source view. + let gifvContentView = LargeImageGifvContentView(attachment: attachment, source: sourceViews[index]!) + let vc = LargeImageViewController(contentView: gifvContentView, description: attachment.description, sourceView: nil) + vc.shrinkGestureEnabled = false + return vc default: fatalError() } diff --git a/Tusker/Screens/Large Image/LargeImageContentView.swift b/Tusker/Screens/Large Image/LargeImageContentView.swift new file mode 100644 index 00000000..52726592 --- /dev/null +++ b/Tusker/Screens/Large Image/LargeImageContentView.swift @@ -0,0 +1,84 @@ +// +// LargeImageContentView.swift +// Tusker +// +// Created by Shadowfacts on 6/17/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import Gifu +import Pachyderm +import AVFoundation + +protocol LargeImageContentView { + var animationImage: UIImage? { get } + var animationGifData: Data? { get } + var activityItemsForSharing: [Any] { get } +} + +class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView { + lazy var animator: Animator? = { + return Animator(withDelegate: self) + }() + + var animationImage: UIImage? { image! } + let animationGifData: Data? + + var activityItemsForSharing: [Any] { + [image!] + } + + init(image: UIImage, gifData: Data?) { + self.animationGifData = gifData + + super.init(image: image) + + contentMode = .scaleAspectFit + + if let data = gifData { + self.animate(withGIFData: data) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func display(_ layer: CALayer) { + updateImageIfNeeded() + } +} + +class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView { + private(set) var animationImage: UIImage? + var animationGifData: Data? { nil } + var activityItemsForSharing: [Any] { + // todo: what should we share for gifvs? + // some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL? + [] + } + + private let asset: AVURLAsset + + // The content view needs to supply an intrinsicContentSize for the LargeImageViewController to handle layout/scrolling/zooming correctly + override var intrinsicContentSize: CGSize { + // This is a really sucky workaround for the fact that in the content view, we don't have access to the size of the underlying video. + // There's probably some way of getting this from the AVPlayer/AVAsset directly + animationImage?.size ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) + } + + init(attachment: Attachment, source: UIImageView) { + precondition(attachment.kind == .gifv) + + self.asset = AVURLAsset(url: attachment.url) + + super.init(asset: asset, gravity: .resizeAspect) + + self.animationImage = source.image + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Tusker/Screens/Large Image/LargeImageViewController.swift b/Tusker/Screens/Large Image/LargeImageViewController.swift index cf0f28be..0be4aea1 100644 --- a/Tusker/Screens/Large Image/LargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LargeImageViewController.swift @@ -7,22 +7,17 @@ // import UIKit -import Gifu class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController { + + typealias ContentView = UIView & LargeImageContentView weak var animationSourceView: UIImageView? - var animationImage: UIImage? { image ?? animationSourceView?.image } - var animationGifData: Data? { gifData } + var animationImage: UIImage? { contentView.animationImage } + var animationGifData: Data? { contentView.animationGifData } var dismissInteractionController: LargeImageInteractionController? @IBOutlet weak var scrollView: UIScrollView! - @IBOutlet weak var imageView: GIFImageView! - @IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint! - @IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint! - @IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint! - @IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint! - @IBOutlet weak var topControlsView: UIView! @IBOutlet weak var topControlsHeightConstraint: NSLayoutConstraint! @IBOutlet weak var shareButton: UIButton! @@ -35,8 +30,10 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma @IBOutlet weak var bottomControlsView: UIView! @IBOutlet weak var descriptionLabel: UILabel! - var image: UIImage? - var gifData: Data? + var contentView: ContentView + var contentViewLeadingConstraint: NSLayoutConstraint! + var contentViewTopConstraint: NSLayoutConstraint! + var imageDescription: String? var initialControlsVisible = true @@ -57,10 +54,11 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma return !controlsVisible } - init(image: UIImage, description: String?, sourceView: UIImageView?) { - self.image = image + init(contentView: ContentView, description: String?, sourceView: UIImageView?) { self.imageDescription = description self.animationSourceView = sourceView + + self.contentView = contentView super.init(nibName: "LargeImageViewController", bundle: nil) @@ -74,16 +72,20 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma override func viewDidLoad() { super.viewDidLoad() + contentView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentView) + contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor) + contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor) + NSLayoutConstraint.activate([ + contentViewLeadingConstraint, + contentViewTopConstraint, + ]) + setControlsVisible(initialControlsVisible, animated: false) - - imageView.image = image - if let gifData = gifData { - imageView.animate(withGIFData: gifData) - } - + shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty + scrollView.delegate = self - imageView.bounds = CGRect(origin: .zero, size: imageView.image!.size) - + if let imageDescription = imageDescription { descriptionLabel.text = imageDescription } else { @@ -99,23 +101,24 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma doubleTap.numberOfTapsRequired = 2 view.addGestureRecognizer(doubleTap) } - - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() + // todo: does this need to be in viewDidLayoutSubviews? // limit the image height to the safe area height, so the image doesn't overlap the top controls // while zoomed all the way out let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom - let heightScale = maxHeight / imageView.bounds.height - let widthScale = view.bounds.width / imageView.bounds.width + let heightScale = maxHeight / contentView.intrinsicContentSize.height + let widthScale = view.bounds.width / contentView.intrinsicContentSize.width let minScale = min(widthScale, heightScale) scrollView.minimumZoomScale = minScale scrollView.zoomScale = minScale scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2 - + centerImage() + // todo: does this need to be in viewDidLayoutSubviews? if view.safeAreaInsets.top == 44 { // running on iPhone X style notched device let notchWidth: CGFloat = 209 @@ -147,7 +150,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma } func viewForZooming(in scrollView: UIScrollView) -> UIView? { - return imageView + return contentView } func scrollViewDidZoom(_ scrollView: UIScrollView) { @@ -163,18 +166,18 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma } func centerImage() { - let yOffset = max(0, (view.bounds.size.height - imageView.frame.height) / 2) - imageViewTopConstraint.constant = yOffset + let yOffset = max(0, (view.bounds.size.height - contentView.frame.height) / 2) + contentViewTopConstraint.constant = yOffset - let xOffset = max(0, (view.bounds.size.width - imageView.frame.width) / 2) - imageViewLeadingConstraint.constant = xOffset + let xOffset = max(0, (view.bounds.size.width - contentView.frame.width) / 2) + contentViewLeadingConstraint.constant = xOffset } func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect { var zoomRect = CGRect.zero - zoomRect.size.width = imageView.frame.width / scale - zoomRect.size.height = imageView.frame.height / scale - let newCenter = scrollView.convert(center, to: imageView) + zoomRect.size.width = contentView.frame.width / scale + zoomRect.size.height = contentView.frame.height / scale + let newCenter = scrollView.convert(center, to: contentView) zoomRect.origin.x = newCenter.x - (zoomRect.width / 2) zoomRect.origin.y = newCenter.y - (zoomRect.height / 2) return zoomRect @@ -225,11 +228,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma } @IBAction func sharePressed(_ sender: Any) { - guard let image = image else { return } - let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil) - if let presentationController = activityVC.presentationController as? UIPopoverPresentationController { - presentationController.sourceView = shareButton - } + let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: nil) + activityVC.popoverPresentationController?.sourceView = shareButton present(activityVC, animated: true) } diff --git a/Tusker/Screens/Large Image/LargeImageViewController.xib b/Tusker/Screens/Large Image/LargeImageViewController.xib index ddc1d32d..5656e296 100644 --- a/Tusker/Screens/Large Image/LargeImageViewController.xib +++ b/Tusker/Screens/Large Image/LargeImageViewController.xib @@ -14,9 +14,6 @@ - - - @@ -31,19 +28,9 @@ - + - - - - - - - - - - diff --git a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift index 781dfd41..5e5dc4f9 100644 --- a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift @@ -36,8 +36,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie var shrinkGestureEnabled = true weak var animationSourceView: UIImageView? - var animationImage: UIImage? { largeImageVC?.image ?? animationSourceView?.image } - var animationGifData: Data? { largeImageVC?.gifData } + var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image } + var animationGifData: Data? { largeImageVC?.animationGifData } var dismissInteractionController: LargeImageInteractionController? override var prefersStatusBarHidden: Bool { @@ -108,12 +108,12 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie func createLargeImage(data: Data) { guard let image = UIImage(data: data) else { return } - largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceView: animationSourceView) + let gifData = url.pathExtension == "gif" ? data : nil + let imageView = LargeImageImageContentView(image: image, gifData: gifData) + + largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView) largeImageVC!.initialControlsVisible = initialControlsVisible largeImageVC!.shrinkGestureEnabled = false - if url.pathExtension == "gif" { - largeImageVC!.gifData = data - } embedChild(largeImageVC!) } diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index f97dee56..e161ef6e 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -36,14 +36,6 @@ protocol TuskerNavigationDelegate: class { func reply(to statusID: String, mentioningAcct: String?) - func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController - - func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController - - func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView) - - func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView) - func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) @@ -150,27 +142,6 @@ extension TuskerNavigationDelegate where Self: UIViewController { vc.presentationController?.delegate = compose present(vc, animated: true) } - - func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController { - let vc = LargeImageViewController(image: image, description: description, sourceView: sourceView) - vc.transitioningDelegate = self - return vc - } - - func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController { - let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceView: sourceView) - vc.transitioningDelegate = self - vc.gifData = gifData - return vc - } - - 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: UIImageView) { - present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true) - } func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController { let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description) diff --git a/Tusker/Views/Attachments/GifvAttachmentView.swift b/Tusker/Views/Attachments/GifvAttachmentView.swift index ab8ce20a..4de213b6 100644 --- a/Tusker/Views/Attachments/GifvAttachmentView.swift +++ b/Tusker/Views/Attachments/GifvAttachmentView.swift @@ -19,8 +19,8 @@ class GifvAttachmentView: UIView { layer as! AVPlayerLayer } - private let item: AVPlayerItem - private let player: AVPlayer + let item: AVPlayerItem + let player: AVPlayer init(asset: AVAsset, gravity: AVLayerVideoGravity) { item = AVPlayerItem(asset: asset)