mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
aabf656d89
Updated the config message generation for GRDB Migrated more preferences into GRDB Added paging to the MediaTileViewController and sorted out the various animations/transitions Fixed an issue where the 'recipientState' for the 'baseQuery' on the ConversationCell.ViewModel wasn't grouping correctly Fixed an issue where the MediaZoomAnimationController could fail if the contextual info wasn't available Fixed an issue where the MediaZoomAnimationController bounce looked odd when returning to the detail screen from the tile screen Fixed an issue where the MediaZoomAnimationController didn't work for videos Fixed a bug where the YDB to GRDB migration wasn't properly handling video files Fixed a number of minor UI bugs with the GalleryRailView Deleted a bunch of legacy code
245 lines
10 KiB
Swift
245 lines
10 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import PromiseKit
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
}
|
|
}
|