session-ios/Session/Media Viewing & Editing/Transitions/MediaZoomAnimationControlle...

233 lines
10 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
import SessionUIKit
class MediaZoomAnimationController: NSObject {
private let mediaItem: Media
private let shouldBounce: Bool
init(galleryItem: MediaGalleryViewModel.Item, shouldBounce: Bool = true) {
self.mediaItem = .gallery(galleryItem)
self.shouldBounce = shouldBounce
}
}
extension MediaZoomAnimationController: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromContextProvider: MediaPresentationContextProvider
let toContextProvider: MediaPresentationContextProvider
guard let fromVC: UIViewController = transitionContext.viewController(forKey: .from) else {
transitionContext.completeTransition(false)
return
}
guard let toVC: UIViewController = transitionContext.viewController(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
switch fromVC {
case let contextProvider as MediaPresentationContextProvider:
fromContextProvider = contextProvider
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
case let topBannerController as TopBannerController:
guard
let firstChild: UIViewController = topBannerController.children.first,
let navController: UINavigationController = firstChild as? UINavigationController,
let contextProvider = navController.topViewController as? MediaPresentationContextProvider
else {
transitionContext.completeTransition(false)
return
}
fromContextProvider = contextProvider
case let navController as UINavigationController:
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {
transitionContext.completeTransition(false)
return
}
fromContextProvider = contextProvider
default:
transitionContext.completeTransition(false)
return
}
switch toVC {
case let contextProvider as MediaPresentationContextProvider:
toContextProvider = contextProvider
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
case let topBannerController as TopBannerController:
guard
let firstChild: UIViewController = topBannerController.children.first,
let navController: UINavigationController = firstChild as? UINavigationController,
let contextProvider = navController.topViewController as? MediaPresentationContextProvider
else {
transitionContext.completeTransition(false)
return
}
toContextProvider = contextProvider
case let navController as UINavigationController:
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {
transitionContext.completeTransition(false)
return
}
toContextProvider = contextProvider
default:
transitionContext.completeTransition(false)
return
}
// 'view(forKey: .to)' will be nil when using this transition for a modal dismiss, in which
// case we want to use the 'toVC.view' but need to ensure we add it back to it's original
// parent afterwards so we don't break the view hierarchy
//
// Note: We *MUST* call 'layoutIfNeeded' prior to 'toContextProvider.mediaPresentationContext'
// as the 'toContextProvider.mediaPresentationContext' is dependant on it having the correct
// positioning (and the navBar sizing isn't correct until after layout)
let toView: UIView = (transitionContext.view(forKey: .to) ?? toVC.view)
let duration: CGFloat = transitionDuration(using: transitionContext)
let oldToViewSuperview: UIView? = toView.superview
toView.layoutIfNeeded()
// If we can't retrieve the contextual info we need to perform the proper zoom animation then
// just fade the destination in (otherwise the user would get stuck on a blank screen)
guard
let fromMediaContext: MediaPresentationContext = fromContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView),
let toMediaContext: MediaPresentationContext = toContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView),
let presentationImage: UIImage = mediaItem.image
else {
toView.frame = containerView.bounds
toView.alpha = 0
containerView.addSubview(toView)
UIView.animate(
withDuration: (duration / 2),
delay: 0,
options: .curveEaseInOut,
animations: {
toView.alpha = 1
},
completion: { _ in
// Need to ensure we add the 'toView' back to it's old superview if it had one
oldToViewSuperview?.addSubview(toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
return
}
fromMediaContext.mediaView.alpha = 0
toMediaContext.mediaView.alpha = 0
toView.frame = containerView.bounds
toView.alpha = 0
containerView.addSubview(toView)
let transitionView: UIImageView = UIImageView(image: presentationImage)
transitionView.frame = fromMediaContext.presentationFrame
transitionView.contentMode = MediaView.contentMode
transitionView.layer.masksToBounds = true
transitionView.layer.cornerRadius = fromMediaContext.cornerRadius
transitionView.layer.maskedCorners = fromMediaContext.cornerMask
containerView.addSubview(transitionView)
// Note: We need to do this after adding the 'transitionView' and insert it at the back
// otherwise the screen can flicker since we have 'afterScreenUpdates: true' (if we use
// 'afterScreenUpdates: false' then the 'fromMediaContext.mediaView' won't be hidden
// during the transition)
let fromSnapshotView: UIView = (fromVC.view.snapshotView(afterScreenUpdates: true) ?? UIView())
containerView.insertSubview(fromSnapshotView, at: 0)
let overshootPercentage: CGFloat = 0.15
let overshootFrame: CGRect = (self.shouldBounce ?
CGRect(
x: (toMediaContext.presentationFrame.minX + ((toMediaContext.presentationFrame.minX - fromMediaContext.presentationFrame.minX) * overshootPercentage)),
y: (toMediaContext.presentationFrame.minY + ((toMediaContext.presentationFrame.minY - fromMediaContext.presentationFrame.minY) * overshootPercentage)),
width: (toMediaContext.presentationFrame.width + ((toMediaContext.presentationFrame.width - fromMediaContext.presentationFrame.width) * overshootPercentage)),
height: (toMediaContext.presentationFrame.height + ((toMediaContext.presentationFrame.height - fromMediaContext.presentationFrame.height) * overshootPercentage))
) :
toMediaContext.presentationFrame
)
// Add any UI elements which should appear above the media view
let fromTransitionalOverlayView: UIView? = {
guard let (overlayView, overlayViewFrame) = fromContextProvider.snapshotOverlayView(in: containerView) else {
return nil
}
overlayView.frame = overlayViewFrame
containerView.addSubview(overlayView)
return overlayView
}()
let toTransitionalOverlayView: UIView? = {
guard let (overlayView, overlayViewFrame) = toContextProvider.snapshotOverlayView(in: containerView) else {
return nil
}
overlayView.alpha = 0
overlayView.frame = overlayViewFrame
containerView.addSubview(overlayView)
return overlayView
}()
UIView.animate(
withDuration: (duration / 2),
delay: 0,
options: .curveEaseOut,
animations: {
// Only fade out the 'fromTransitionalOverlayView' if it's bigger than the destination
// one (makes it look cleaner as you don't get the crossfade effect)
if (fromTransitionalOverlayView?.frame.size.height ?? 0) > (toTransitionalOverlayView?.frame.size.height ?? 0) {
fromTransitionalOverlayView?.alpha = 0
}
toView.alpha = 1
toTransitionalOverlayView?.alpha = 1
transitionView.frame = overshootFrame
transitionView.layer.cornerRadius = toMediaContext.cornerRadius
},
completion: { _ in
UIView.animate(
withDuration: (duration / 2),
delay: 0,
options: .curveEaseInOut,
animations: {
transitionView.frame = toMediaContext.presentationFrame
},
completion: { _ in
transitionView.removeFromSuperview()
fromSnapshotView.removeFromSuperview()
fromTransitionalOverlayView?.removeFromSuperview()
toTransitionalOverlayView?.removeFromSuperview()
toMediaContext.mediaView.alpha = 1
fromMediaContext.mediaView.alpha = 1
// Need to ensure we add the 'toView' back to it's old superview if it had one
oldToViewSuperview?.addSubview(toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
)
}
}