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

270 lines
12 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 MediaDismissAnimationController: NSObject {
private let mediaItem: Media
public let interactionController: MediaInteractiveDismiss?
var fromView: UIView?
var transitionView: UIView?
var fromTransitionalOverlayView: UIView?
var toTransitionalOverlayView: UIView?
var fromMediaFrame: CGRect?
var pendingCompletion: (() -> ())?
init(galleryItem: MediaGalleryViewModel.Item, interactionController: MediaInteractiveDismiss? = nil) {
self.mediaItem = .gallery(galleryItem)
self.interactionController = interactionController
}
init(image: UIImage, interactionController: MediaInteractiveDismiss? = nil) {
self.mediaItem = .image(image)
self.interactionController = interactionController
}
}
extension MediaDismissAnimationController: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
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:
toVC.view.layoutIfNeeded()
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
}
toVC.view.layoutIfNeeded()
toContextProvider = contextProvider
case let navController as UINavigationController:
guard let contextProvider = navController.topViewController as? MediaPresentationContextProvider else {
transitionContext.completeTransition(false)
return
}
toVC.view.layoutIfNeeded()
toContextProvider = contextProvider
default:
transitionContext.completeTransition(false)
return
}
guard let fromMediaContext: MediaPresentationContext = fromContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView) else {
transitionContext.completeTransition(false)
return
}
guard let presentationImage: UIImage = mediaItem.image else {
transitionContext.completeTransition(true)
return
}
// fromView will be nil if doing a presentation, in which case we don't want to add the view -
// it will automatically be added to the view hierarchy, in front of the VC we're presenting from
if let fromView: UIView = transitionContext.view(forKey: .from) {
self.fromView = fromView
containerView.addSubview(fromView)
}
// toView will be nil if doing a modal dismiss, in which case we don't want to add the view -
// it's already in the view hierarchy, behind the VC we're dismissing.
if let toView: UIView = transitionContext.view(forKey: .to) {
containerView.insertSubview(toView, at: 0)
}
let toMediaContext: MediaPresentationContext? = toContextProvider.mediaPresentationContext(mediaItem: mediaItem, in: containerView)
let duration: CGFloat = transitionDuration(using: transitionContext)
fromMediaContext.mediaView.alpha = 0
toMediaContext?.mediaView.alpha = 0
let transitionView = UIImageView(image: presentationImage)
transitionView.frame = fromMediaContext.presentationFrame
transitionView.contentMode = MediaView.contentMode
transitionView.layer.masksToBounds = true
transitionView.layer.cornerRadius = fromMediaContext.cornerRadius
transitionView.layer.maskedCorners = (toMediaContext?.cornerMask ?? fromMediaContext.cornerMask)
containerView.addSubview(transitionView)
// Add any UI elements which should appear above the media view
self.fromTransitionalOverlayView = {
guard let (overlayView, overlayViewFrame) = fromContextProvider.snapshotOverlayView(in: containerView) else {
return nil
}
overlayView.frame = overlayViewFrame
containerView.addSubview(overlayView)
return overlayView
}()
self.toTransitionalOverlayView = { [weak self] in
guard let (overlayView, overlayViewFrame) = toContextProvider.snapshotOverlayView(in: containerView) else {
return nil
}
// Only fade in the 'toTransitionalOverlayView' if it's bigger than the origin
// one (makes it look cleaner as you don't get the crossfade effect)
if (self?.fromTransitionalOverlayView?.frame.size.height ?? 0) > overlayViewFrame.height {
overlayView.alpha = 0
}
overlayView.frame = overlayViewFrame
if let fromTransitionalOverlayView = self?.fromTransitionalOverlayView {
containerView.insertSubview(overlayView, belowSubview: fromTransitionalOverlayView)
}
else {
containerView.addSubview(overlayView)
}
return overlayView
}()
self.transitionView = transitionView
self.fromMediaFrame = transitionView.frame
self.pendingCompletion = {
let destinationFromAlpha: CGFloat
let destinationFrame: CGRect
let destinationCornerRadius: CGFloat
if transitionContext.transitionWasCancelled {
destinationFromAlpha = 1
destinationFrame = fromMediaContext.presentationFrame
destinationCornerRadius = fromMediaContext.cornerRadius
}
else if let toMediaContext: MediaPresentationContext = toMediaContext {
destinationFromAlpha = 0
destinationFrame = toMediaContext.presentationFrame
destinationCornerRadius = toMediaContext.cornerRadius
}
else {
// `toMediaContext` can be nil if the target item is scrolled off of the
// contextProvider's screen, so we synthesize a context to dismiss the item
// off screen
destinationFromAlpha = 0
destinationFrame = fromMediaContext.presentationFrame
.offsetBy(dx: 0, dy: (containerView.bounds.height * 2))
destinationCornerRadius = fromMediaContext.cornerRadius
}
UIView.animate(
withDuration: duration,
delay: 0,
options: [.beginFromCurrentState, .curveEaseInOut],
animations: { [weak self] in
self?.fromTransitionalOverlayView?.alpha = destinationFromAlpha
self?.fromView?.alpha = destinationFromAlpha
self?.toTransitionalOverlayView?.alpha = (1.0 - destinationFromAlpha)
transitionView.frame = destinationFrame
transitionView.layer.cornerRadius = destinationCornerRadius
},
completion: { [weak self] _ in
self?.fromView?.alpha = 1
fromMediaContext.mediaView.alpha = 1
toMediaContext?.mediaView.alpha = 1
transitionView.removeFromSuperview()
self?.fromTransitionalOverlayView?.removeFromSuperview()
self?.toTransitionalOverlayView?.removeFromSuperview()
if transitionContext.transitionWasCancelled {
// The "to" view will be nil if we're doing a modal dismiss, in which case
// we wouldn't want to remove the toView.
transitionContext.view(forKey: .to)?.removeFromSuperview()
// Note: We shouldn't need to do this but for some reason it's not
// automatically getting re-enabled so we manually enable it
transitionContext.view(forKey: .from)?.isUserInteractionEnabled = true
}
else {
transitionContext.view(forKey: .from)?.removeFromSuperview()
// Note: We shouldn't need to do this but for some reason it's not
// automatically getting re-enabled so we manually enable it
transitionContext.view(forKey: .to)?.isUserInteractionEnabled = true
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
// The interactive transition will call the 'pendingCompletion' when it completes so don't call it here
guard !transitionContext.isInteractive else { return }
self.pendingCompletion?()
self.pendingCompletion = nil
}
}
extension MediaDismissAnimationController: InteractiveDismissDelegate {
func interactiveDismissUpdate(_ interactiveDismiss: UIPercentDrivenInteractiveTransition, didChangeTouchOffset offset: CGPoint) {
guard let transitionView: UIView = transitionView else { return } // Transition hasn't started yet
guard let fromMediaFrame: CGRect = fromMediaFrame else { return }
fromView?.alpha = (1.0 - interactiveDismiss.percentComplete)
transitionView.center = fromMediaFrame.offsetBy(dx: offset.x, dy: offset.y).center
}
func interactiveDismissDidFinish(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) {
self.pendingCompletion?()
self.pendingCompletion = nil
}
}