mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			233 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			233 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import UIKit
 | 
						|
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
 | 
						|
                
 | 
						|
            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
 | 
						|
                
 | 
						|
            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)
 | 
						|
                    }
 | 
						|
                )
 | 
						|
            }
 | 
						|
        )
 | 
						|
    }
 | 
						|
}
 |