From fcab6818b05737c8bd9e3d679850eb0e08bc8638 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 25 Mar 2020 23:10:48 -0400 Subject: [PATCH] Hide large image source view during expand/shrink animation --- Tusker.xcodeproj/project.pbxproj | 4 ++ .../GalleryViewController.swift | 15 +++---- .../LargeImageViewController.swift | 10 ++--- .../LoadingLargeImageViewController.swift | 6 +-- .../LargeImageExpandAnimationController.swift | 34 ++++++++++++++-- .../LargeImageShrinkAnimationController.swift | 13 ++++-- Tusker/TuskerNavigationDelegate.swift | 26 +++--------- Tusker/WeakArray.swift | 40 +++++++++++++++++++ 8 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 Tusker/WeakArray.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index e2f7db91..5afec677 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -224,6 +224,7 @@ D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; }; D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; }; D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; }; + D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; }; D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; }; @@ -512,6 +513,7 @@ D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = ""; }; D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = ""; }; D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = ""; }; + D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = ""; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = ""; }; D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = ""; }; D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = ""; }; @@ -1213,6 +1215,7 @@ D6945C2E23AC47C3005C403C /* SavedDataManager.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, + D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */, D6F1F84E2193B9BE00F5FE67 /* Caching */, D6757A7A2157E00100721E32 /* XCallbackURL */, D62D241E217AA46B005076CC /* Shortcuts */, @@ -1707,6 +1710,7 @@ D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */, D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */, D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, + D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */, D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */, D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */, D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */, diff --git a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift index 320a3b9a..befaec37 100644 --- a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift +++ b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift @@ -13,7 +13,7 @@ import AVKit class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController { let attachments: [Attachment] - let sourcesInfo: [LargeImageViewController.SourceInfo?] + let sourceViews: WeakArray let startIndex: Int let pages: [UIViewController] @@ -26,12 +26,13 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc return index } - var animationSourceInfo: LargeImageViewController.SourceInfo? { sourcesInfo[currentIndex] } + var animationSourceView: UIImageView? { sourceViews[currentIndex] } var animationImage: UIImage? { - if let sourceImage = sourcesInfo[currentIndex]?.image { - return sourceImage + if let page = pages[currentIndex] as? LoadingLargeImageViewController, + let image = page.largeImageVC?.image { + return image } else { - return (pages[currentIndex] as? LoadingLargeImageViewController)?.largeImageVC?.image + return animationSourceView?.image } } var animationGifData: Data? { @@ -59,9 +60,9 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc } } - init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) { + init(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) { self.attachments = attachments - self.sourcesInfo = sourcesInfo + self.sourceViews = WeakArray(sourceViews) self.startIndex = startIndex self.pages = attachments.map { diff --git a/Tusker/Screens/Large Image/LargeImageViewController.swift b/Tusker/Screens/Large Image/LargeImageViewController.swift index 868ea0b5..d16567e5 100644 --- a/Tusker/Screens/Large Image/LargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LargeImageViewController.swift @@ -11,10 +11,8 @@ import Gifu class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController { - typealias SourceInfo = (image: UIImage?, frame: CGRect, cornerRadius: CGFloat) - - var animationSourceInfo: SourceInfo? - var animationImage: UIImage? { animationSourceInfo?.image ?? image } + weak var animationSourceView: UIImageView? + var animationImage: UIImage? { image ?? animationSourceView?.image } var animationGifData: Data? { gifData } var dismissInteractionController: LargeImageInteractionController? @@ -59,10 +57,10 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma return !controlsVisible } - init(image: UIImage, description: String?, sourceInfo: SourceInfo?) { + init(image: UIImage, description: String?, sourceView: UIImageView?) { self.image = image self.imageDescription = description - self.animationSourceInfo = sourceInfo + self.animationSourceView = sourceView super.init(nibName: "LargeImageViewController", bundle: nil) diff --git a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift index d10c7ffd..781dfd41 100644 --- a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift @@ -35,8 +35,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie var shrinkGestureEnabled = true - var animationSourceInfo: LargeImageViewController.SourceInfo? - var animationImage: UIImage? { animationSourceInfo?.image ?? largeImageVC?.image } + weak var animationSourceView: UIImageView? + var animationImage: UIImage? { largeImageVC?.image ?? animationSourceView?.image } var animationGifData: Data? { largeImageVC?.gifData } var dismissInteractionController: LargeImageInteractionController? @@ -108,7 +108,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie func createLargeImage(data: Data) { guard let image = UIImage(data: data) else { return } - largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceInfo: nil) + largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceView: animationSourceView) largeImageVC!.initialControlsVisible = initialControlsVisible largeImageVC!.shrinkGestureEnabled = false if url.pathExtension == "gif" { diff --git a/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift b/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift index af28c647..5bd0f3d0 100644 --- a/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift +++ b/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift @@ -10,12 +10,30 @@ import UIKit import Gifu protocol LargeImageAnimatableViewController: UIViewController { - var animationSourceInfo: LargeImageViewController.SourceInfo? { get } + var animationSourceView: UIImageView? { get } var animationImage: UIImage? { get } var animationGifData: Data? { get } var dismissInteractionController: LargeImageInteractionController? { get } } +extension LargeImageAnimatableViewController { + func sourceViewFrame(in coordinateSpace: UIView) -> CGRect? { + guard let sourceView = animationSourceView else { return nil } + + var sourceFrame = sourceView.convert(sourceView.bounds, to: coordinateSpace) + if let scrollView = coordinateSpace as? UIScrollView { + let scale = scrollView.zoomScale + let width = sourceFrame.width * scale + let height = sourceFrame.height * scale + let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX + let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY + sourceFrame = CGRect(x: x, y: y, width: width, height: height) + } + + return sourceFrame + } +} + class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { @@ -32,12 +50,16 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra containerView.addSubview(toVC.view) let finalVCFrame = transitionContext.finalFrame(for: toVC) - guard let sourceInfo = toVC.animationSourceInfo, + guard let sourceView = toVC.animationSourceView, + let sourceFrame = toVC.sourceViewFrame(in: fromVC.view), let image = toVC.animationImage else { toVC.view.frame = finalVCFrame transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } + + // use alpha, becaus isHidden makes stack views re-layout + sourceView.alpha = 0 var finalFrameSize = finalVCFrame.inset(by: fromVC.view.safeAreaInsets).size let newWidth = finalFrameSize.width / image.size.width @@ -49,13 +71,14 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra } let finalFrame = CGRect(origin: CGPoint(x: finalVCFrame.midX - finalFrameSize.width / 2, y: finalVCFrame.midY - finalFrameSize.height / 2), size: finalFrameSize) - let imageView = GIFImageView(frame: sourceInfo.frame) + let imageView = GIFImageView(frame: sourceFrame) imageView.image = image if let gifData = toVC.animationGifData { imageView.animate(withGIFData: gifData) } imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = sourceInfo.cornerRadius + imageView.layer.cornerRadius = sourceView.layer.cornerRadius + imageView.layer.maskedCorners = sourceView.layer.maskedCorners imageView.layer.masksToBounds = true let blackView = UIView(frame: finalVCFrame) @@ -84,6 +107,9 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra fromVC.view.isHidden = false blackView.removeFromSuperview() imageView.removeFromSuperview() + + sourceView.alpha = 1 + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) } diff --git a/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift b/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift index 37e97b6b..d6da2c3e 100644 --- a/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift +++ b/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift @@ -27,12 +27,15 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra return } - guard let sourceInfo = fromVC.animationSourceInfo, + guard let sourceView = fromVC.animationSourceView, + let sourceFrame = fromVC.sourceViewFrame(in: toVC.view), let image = fromVC.animationImage else { transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } + // use alpha, becaus isHidden makes stack views re-layout + sourceView.alpha = 0 let containerView = transitionContext.containerView @@ -66,8 +69,9 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra let duration = transitionDuration(using: transitionContext) UIView.animate(withDuration: duration, animations: { - imageView.frame = sourceInfo.frame - imageView.layer.cornerRadius = sourceInfo.cornerRadius + imageView.frame = sourceFrame + imageView.layer.cornerRadius = sourceView.layer.cornerRadius + imageView.layer.maskedCorners = sourceView.layer.maskedCorners blackView.alpha = 0 }, completion: { _ in blackView.removeFromSuperview() @@ -75,6 +79,9 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra if transitionContext.transitionWasCancelled { toVC.view.removeFromSuperview() } + + sourceView.alpha = 1 + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) } diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 321eb602..977e7795 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -150,30 +150,15 @@ extension TuskerNavigationDelegate where Self: UIViewController { vc.presentationController?.delegate = compose present(vc, animated: true) } - - private func sourceViewInfo(_ sourceView: UIImageView?) -> LargeImageViewController.SourceInfo? { - guard let sourceView = sourceView else { return nil } - - var sourceFrame = sourceView.convert(sourceView.bounds, to: view) - if let scrollView = view as? UIScrollView { - let scale = scrollView.zoomScale - let width = sourceFrame.width * scale - let height = sourceFrame.height * scale - let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX - let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY - sourceFrame = CGRect(x: x, y: y, width: width, height: height) - } - return (image: sourceView.image, frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius) - } - + func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController { - let vc = LargeImageViewController(image: image, description: description, sourceInfo: sourceViewInfo(sourceView)) + 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, sourceInfo: sourceViewInfo(sourceView)) + let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceView: sourceView) vc.transitioningDelegate = self vc.gifData = gifData return vc @@ -189,7 +174,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController { let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description) - vc.animationSourceInfo = sourceViewInfo(sourceView) + vc.animationSourceView = sourceView vc.transitioningDelegate = self return vc } @@ -199,8 +184,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { } func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController { - let sourcesInfo = sourceViews.map(sourceViewInfo) - let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex) + let vc = GalleryViewController(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex) vc.transitioningDelegate = self return vc } diff --git a/Tusker/WeakArray.swift b/Tusker/WeakArray.swift new file mode 100644 index 00000000..c5affc98 --- /dev/null +++ b/Tusker/WeakArray.swift @@ -0,0 +1,40 @@ +// +// WeakArray.swift +// Tusker +// +// Created by Shadowfacts on 3/25/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import Foundation + +fileprivate class WeakWrapper { + weak var value: T? + + init(_ value: T?) { + self.value = value + } +} + +struct WeakArray: Collection { + private var array: [WeakWrapper] + + var startIndex: Int { array.startIndex } + var endIndex: Int { array.endIndex } + + init(_ elements: [Element]) { + array = elements.map { WeakWrapper($0) } + } + + init(_ elements: [Element?]) { + array = elements.map { WeakWrapper($0) } + } + + subscript(_ index: Int) -> Element? { + return array[index].value + } + + func index(after i: Int) -> Int { + return array.index(after: i) + } +}