diff --git a/Tusker/Screens/Attachment/AttachmentViewController.swift b/Tusker/Screens/Attachment/AttachmentViewController.swift index c2a8c6cc..9fb79062 100644 --- a/Tusker/Screens/Attachment/AttachmentViewController.swift +++ b/Tusker/Screens/Attachment/AttachmentViewController.swift @@ -62,7 +62,7 @@ class AttachmentViewController: UIViewController { func createLargeImage(data: Data) { guard let image = UIImage(data: data) else { return } - largeImageVC = LargeImageViewController(image: image, description: attachment.description, sourceFrame: nil, sourceCornerRadius: nil) + largeImageVC = LargeImageViewController(image: image, description: attachment.description, sourceInfo: nil) largeImageVC!.initialControlsVisible = initialControlsVisible largeImageVC!.shrinkGestureEnabled = false if attachment.url.pathExtension == "gif" { diff --git a/Tusker/Screens/Gallery/GalleryViewController.swift b/Tusker/Screens/Gallery/GalleryViewController.swift index 6ad0a47a..0d55f5e4 100644 --- a/Tusker/Screens/Gallery/GalleryViewController.swift +++ b/Tusker/Screens/Gallery/GalleryViewController.swift @@ -13,7 +13,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc var dismissInteractionController: LargeImageInteractionController? let attachments: [Attachment] - let sourcesInfo: [(CGRect, CGFloat)] + let sourcesInfo: [LargeImageViewController.SourceInfo?] let startIndex: Int let pages: [AttachmentViewController] @@ -34,7 +34,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc viewControllers?.first } - init(attachments: [Attachment], sourcesInfo: [(CGRect, CGFloat)], startIndex: Int) { + init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) { self.attachments = attachments self.sourcesInfo = sourcesInfo self.startIndex = startIndex diff --git a/Tusker/Screens/Gallery/Transitions/GalleryExpandAnimationController.swift b/Tusker/Screens/Gallery/Transitions/GalleryExpandAnimationController.swift index 809957a2..096cbdd3 100644 --- a/Tusker/Screens/Gallery/Transitions/GalleryExpandAnimationController.swift +++ b/Tusker/Screens/Gallery/Transitions/GalleryExpandAnimationController.swift @@ -19,11 +19,16 @@ class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransi let toVC = transitionContext.viewController(forKey: .to) as? GalleryViewController else { return } - let attachment = toVC.attachments[toVC.startIndex] - let (sourceFrame, sourceCornerRadius) = toVC.sourcesInfo[toVC.startIndex] let finalVCFrame = transitionContext.finalFrame(for: toVC) + guard let (sourceFrame, sourceCornerRadius) = toVC.sourcesInfo[toVC.startIndex] else { + toVC.view.frame = finalVCFrame + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + return + } + let attachment = toVC.attachments[toVC.startIndex] + guard let data = ImageCache.attachments.get(attachment.url), let image = UIImage(data: data) else { toVC.view.frame = finalVCFrame transitionContext.completeTransition(!transitionContext.transitionWasCancelled) diff --git a/Tusker/Screens/Gallery/Transitions/GalleryShrinkAnimationController.swift b/Tusker/Screens/Gallery/Transitions/GalleryShrinkAnimationController.swift index 8b1e76ec..48f96de2 100644 --- a/Tusker/Screens/Gallery/Transitions/GalleryShrinkAnimationController.swift +++ b/Tusker/Screens/Gallery/Transitions/GalleryShrinkAnimationController.swift @@ -26,7 +26,10 @@ class GalleryShrinkAnimationController: NSObject, UIViewControllerAnimatedTransi return } - let (sourceFrame, sourceCornerRadius) = fromVC.sourcesInfo[fromVC.currentIndex] + guard let (sourceFrame, sourceCornerRadius) = fromVC.sourcesInfo[fromVC.currentIndex] else { + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + return + } let originalVCFrame = fromVC.view.frame let attachment = fromVC.attachments[fromVC.currentIndex] diff --git a/Tusker/Screens/Large Image/LargeImageViewController.swift b/Tusker/Screens/Large Image/LargeImageViewController.swift index 5bb643d9..1364e3ef 100644 --- a/Tusker/Screens/Large Image/LargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LargeImageViewController.swift @@ -13,8 +13,9 @@ import Gifu class LargeImageViewController: UIViewController, UIScrollViewDelegate { - var originFrame: CGRect? - var originCornerRadius: CGFloat? + typealias SourceInfo = (frame: CGRect, cornerRadius: CGFloat) + + var sourceInfo: SourceInfo? var dismissInteractionController: LargeImageInteractionController? @IBOutlet weak var scrollView: UIScrollView! @@ -60,12 +61,11 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate { return !controlsVisible } - init(image: UIImage, description: String?, sourceFrame: CGRect?, sourceCornerRadius: CGFloat?) { + init(image: UIImage, description: String?, sourceInfo: SourceInfo?) { self.image = image self.imageDescription = description - self.originFrame = sourceFrame - self.originCornerRadius = sourceCornerRadius - + self.sourceInfo = sourceInfo + super.init(nibName: "LargeImageViewController", bundle: nil) modalPresentationStyle = .fullScreen diff --git a/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift b/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift index db708fb1..af9086cb 100644 --- a/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift +++ b/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift @@ -17,13 +17,18 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from), - let toVC = transitionContext.viewController(forKey: .to) as? LargeImageViewController, - let originFrame = toVC.originFrame else { + let toVC = transitionContext.viewController(forKey: .to) as? LargeImageViewController else { return } - let containerView = transitionContext.containerView let finalVCFrame = transitionContext.finalFrame(for: toVC) + guard let (originFrame, originCornerRadius) = toVC.sourceInfo else { + toVC.view.frame = finalVCFrame + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + return + } + + let containerView = transitionContext.containerView let image = toVC.imageView.image! let ratio = image.size.width / image.size.height let width = finalVCFrame.width @@ -36,7 +41,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra imageView.animate(withGIFData: gifData) } imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = toVC.originCornerRadius! + imageView.layer.cornerRadius = originCornerRadius imageView.layer.masksToBounds = true let blackView = UIView(frame: finalVCFrame) diff --git a/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift b/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift index ee34bc8b..8a063a7a 100644 --- a/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift +++ b/Tusker/Screens/Large Image/Transitions/LargeImageShrinkAnimationController.swift @@ -23,11 +23,15 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageViewController, - let toVC = transitionContext.viewController(forKey: .to), - let finalFrame = fromVC.originFrame else { + let toVC = transitionContext.viewController(forKey: .to) else { return } + guard let (finalFrame, finalCornerRadius) = fromVC.sourceInfo else { + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + return + } + let originalVCFrame = fromVC.view.frame let containerView = transitionContext.containerView @@ -57,7 +61,7 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra let duration = transitionDuration(using: transitionContext) UIView.animate(withDuration: duration, animations: { imageView.frame = finalFrame - imageView.layer.cornerRadius = fromVC.originCornerRadius! + imageView.layer.cornerRadius = finalCornerRadius blackView.alpha = 0 }, completion: { _ in blackView.removeFromSuperview() diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 8789933b..7f51b8e4 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -34,9 +34,9 @@ protocol TuskerNavigationDelegate { func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIView) - func gallery(attachments: [Attachment], sourceViews: [UIView], startIndex: Int) -> GalleryViewController + func gallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) -> GalleryViewController - func showGallery(attachments: [Attachment], sourceViews: [UIView], startIndex: Int) + func showGallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) func showMoreOptions(forStatus statusID: String) @@ -99,7 +99,9 @@ extension TuskerNavigationDelegate where Self: UIViewController { present(vc, animated: true) } - private func sourceViewInfo(_ sourceView: UIView) -> (CGRect, CGFloat) { + private func sourceViewInfo(_ sourceView: UIView?) -> 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 @@ -109,19 +111,17 @@ extension TuskerNavigationDelegate where Self: UIViewController { let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY sourceFrame = CGRect(x: x, y: y, width: width, height: height) } - return (sourceFrame, sourceView.layer.cornerRadius) + return (frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius) } func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController { - let (sourceFrame, sourceCornerRadius) = sourceViewInfo(sourceView) - let vc = LargeImageViewController(image: image, description: description, sourceFrame: sourceFrame, sourceCornerRadius: sourceCornerRadius) + let vc = LargeImageViewController(image: image, description: description, sourceInfo: sourceViewInfo(sourceView)) vc.transitioningDelegate = self return vc } func largeImage(gifData: Data, description: String?, sourceView: UIView) -> LargeImageViewController { - let (sourceFrame, sourceCornerRadius) = sourceViewInfo(sourceView) - let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceFrame: sourceFrame, sourceCornerRadius: sourceCornerRadius) + let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceInfo: sourceViewInfo(sourceView)) vc.transitioningDelegate = self vc.gifData = gifData return vc @@ -135,14 +135,14 @@ extension TuskerNavigationDelegate where Self: UIViewController { present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true) } - func gallery(attachments: [Attachment], sourceViews: [UIView], startIndex: Int) -> GalleryViewController { + func gallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) -> GalleryViewController { let sourcesInfo = sourceViews.map(sourceViewInfo) let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex) vc.transitioningDelegate = self return vc } - func showGallery(attachments: [Attachment], sourceViews: [UIView], startIndex: Int) { + func showGallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) { present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true) } diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index d74cdbb1..72a59579 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -13,60 +13,79 @@ class AttachmentsContainerView: UIView { var delegate: AttachmentViewDelegate? + let attachmentViews: NSHashTable = .weakObjects() + override func awakeFromNib() { super.awakeFromNib() self.isUserInteractionEnabled = true } + func getAttachmentView(for attachment: Attachment) -> AttachmentView? { + return attachmentViews.allObjects.first { $0.attachment.id == attachment.id } + } + + // MARK: - User Interaface + func updateUI(status: Status) { let attachments = status.attachments.filter { $0.kind == .image } + attachmentViews.removeAllObjects() + subviews.forEach { $0.removeFromSuperview() } + if attachments.count > 0 { self.isHidden = false - let mainView: UIView switch attachments.count { case 1: - mainView = createAttachmentView(attachments[0]) + makeMainView(createAttachmentView(attachments[0])) case 2: - mainView = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ - createAttachmentView(attachments[0]), + let left = createAttachmentView(attachments[0]) + makeMainView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ + left, createAttachmentView(attachments[1]) + ])) + NSLayoutConstraint.activate([ + left.halfWidth() ]) case 3: - mainView = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ - createAttachmentView(attachments[0]), + let left = createAttachmentView(attachments[0]) + let topRight = createAttachmentView(attachments[1]) + makeMainView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ + left, createAttachmentsStack(axis: .vertical, arrangedSubviews: [ - createAttachmentView(attachments[1]), + topRight, createAttachmentView(attachments[2]) ]) + ])) + NSLayoutConstraint.activate([ + left.halfWidth(), + topRight.halfHeight(), ]) case 4: - mainView = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ + let topLeft = createAttachmentView(attachments[0]) + let left = createAttachmentsStack(axis: .vertical, arrangedSubviews: [ + topLeft, + createAttachmentView(attachments[2]) + ]) + let topRight = createAttachmentView(attachments[1]) + makeMainView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ + left, createAttachmentsStack(axis: .vertical, arrangedSubviews: [ - createAttachmentView(attachments[0]), - createAttachmentView(attachments[2]) - ]), - createAttachmentsStack(axis: .vertical, arrangedSubviews: [ - createAttachmentView(attachments[1]), + topRight, createAttachmentView(attachments[3]) ]) + ])) + NSLayoutConstraint.activate([ + left.halfWidth(), + topLeft.halfHeight(), + topRight.halfHeight(), ]) default: fatalError("Too many attachments") } - - addSubview(mainView) - NSLayoutConstraint.activate([ - mainView.leadingAnchor.constraint(equalTo: leadingAnchor), - mainView.trailingAnchor.constraint(equalTo: trailingAnchor), - mainView.topAnchor.constraint(equalTo: topAnchor), - mainView.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) } else { self.isHidden = true - subviews.forEach { $0.removeFromSuperview() } } } @@ -74,15 +93,49 @@ class AttachmentsContainerView: UIView { let attachmentView = AttachmentView(attachment: attachment) attachmentView.delegate = delegate attachmentView.translatesAutoresizingMaskIntoConstraints = false + attachmentViews.add(attachmentView) return attachmentView } private func createAttachmentsStack(axis: NSLayoutConstraint.Axis, arrangedSubviews: [UIView]) -> UIStackView { let stack = UIStackView(arrangedSubviews: arrangedSubviews) stack.axis = axis - stack.spacing = 8 + stack.spacing = 4 stack.translatesAutoresizingMaskIntoConstraints = false return stack } - + + private func makeMainView(_ view: UIView) { + addSubview(view) + NSLayoutConstraint.activate([ + view.leadingAnchor.constraint(equalTo: leadingAnchor), + view.trailingAnchor.constraint(equalTo: trailingAnchor), + view.topAnchor.constraint(equalTo: topAnchor), + view.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + +} + +fileprivate extension UIView { + enum RelativeSize { + case full, half + + var multiplier: CGFloat { + switch self { + case .full: + return 1 + case .half: + return 0.5 + } + } + } + + func halfWidth(spacing: CGFloat = 4) -> NSLayoutConstraint { + return widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.5, constant: -spacing / 2) + } + + func halfHeight(spacing: CGFloat = 4) -> NSLayoutConstraint { + return heightAnchor.constraint(equalTo: superview!.heightAnchor, multiplier: 0.5, constant: -spacing / 2) + } } diff --git a/Tusker/Views/Status/StatusTableViewCell.swift b/Tusker/Views/Status/StatusTableViewCell.swift index 84a16c29..d4ea1470 100644 --- a/Tusker/Views/Status/StatusTableViewCell.swift +++ b/Tusker/Views/Status/StatusTableViewCell.swift @@ -307,9 +307,7 @@ extension StatusTableViewCell: AttachmentViewDelegate { func showLargeAttachment(for attachmentView: AttachmentView) { guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } let startIndex = status.attachments.firstIndex { $0.id == attachmentView.attachment.id } ?? 0 - let sourceViews = status.attachments.map { attachment in - attachmentsView.subviews.first { ($0 as! AttachmentView).attachment.id == attachment.id }! - } + let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) delegate?.showGallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: startIndex) } }