diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index cd2e181d4..ae623cefb 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -24,6 +24,7 @@ class ArticleViewController: UIViewController { private struct MessageName { static let imageWasClicked = "imageWasClicked" + static let imageWasShown = "imageWasShown" } @IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem! @@ -43,7 +44,8 @@ class ArticleViewController: UIViewController { }() private var webView: WKWebView! - private var transition = ImageTransition() + private lazy var transition = ImageTransition(controller: self) + private var clickedImageCompletion: (() -> Void)? weak var coordinator: SceneCoordinator! @@ -109,7 +111,9 @@ class ArticleViewController: UIViewController { webView.uiDelegate = self webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked) + webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasShown) webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) + webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) // Even though page.html should be loaded into this webview, we have to do it again // to work around this bug: http://www.openradar.me/22855188 @@ -296,6 +300,15 @@ class ArticleViewController: UIViewController { webView.scrollView.setContentOffset(scrollToPoint, animated: true) } + func hideClickedImage() { + webView?.evaluateJavaScript("hideClickedImage();") + } + + func showClickedImage(completion: @escaping () -> Void) { + clickedImageCompletion = completion + webView?.evaluateJavaScript("showClickedImage();") + } + } // MARK: WKNavigationDelegate @@ -350,26 +363,13 @@ extension ArticleViewController: WKUIDelegate { extension ArticleViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - if message.name == MessageName.imageWasClicked, - let body = message.body as? String, - let data = body.data(using: .utf8), - let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data), - let range = clickMessage.imageURL.range(of: ";base64,") { - - let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound)) - if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) { - - let rect = CGRect(x: CGFloat(clickMessage.x), y: CGFloat(clickMessage.y), width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height)) - transition.originFrame = webView.convert(rect, to: nil) - transition.originImage = image - - let imageVC = UIStoryboard.main.instantiateController(ofType: ImageViewController.self) - imageVC.image = image - imageVC.modalPresentationStyle = .fullScreen - imageVC.transitioningDelegate = self - present(imageVC, animated: true) - - } + switch message.name { + case MessageName.imageWasShown: + clickedImageCompletion?() + case MessageName.imageWasClicked: + imageWasClicked(body: message.body as? String) + default: + return } } @@ -430,4 +430,25 @@ private extension ArticleViewController { } } + func imageWasClicked(body: String?) { + guard let body = body, + let data = body.data(using: .utf8), + let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data), + let range = clickMessage.imageURL.range(of: ";base64,") + else { return } + + let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound)) + if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) { + let rect = CGRect(x: CGFloat(clickMessage.x), y: CGFloat(clickMessage.y), width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height)) + transition.originFrame = webView.convert(rect, to: nil) + transition.originImage = image + + let imageVC = UIStoryboard.main.instantiateController(ofType: ImageViewController.self) + imageVC.image = image + imageVC.modalPresentationStyle = .fullScreen + imageVC.transitioningDelegate = self + present(imageVC, animated: true) + } + } + } diff --git a/iOS/Article/ImageTransition.swift b/iOS/Article/ImageTransition.swift index f76ab92b8..e3268c184 100644 --- a/iOS/Article/ImageTransition.swift +++ b/iOS/Article/ImageTransition.swift @@ -10,74 +10,84 @@ import UIKit class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning { - let duration = 0.4 + private weak var articleController: ArticleViewController? + private let duration = 0.4 var presenting = true var originFrame: CGRect! var originImage: UIImage! + init(controller: ArticleViewController) { + self.articleController = controller + } + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { - - let destFrame: CGRect = { - if presenting { - let imageController = transitionContext.viewController(forKey: .to) as! ImageViewController - return imageController.zoomedFrame - } else { - let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController - return imageController.zoomedFrame - } - }() + if presenting { + animateTransitionPresenting(using: transitionContext) + } else { + animateTransitionReturning(using: transitionContext) + } + } - let initialFrame = presenting ? originFrame! : destFrame - let targetFrame = presenting ? destFrame : originFrame! + private func animateTransitionPresenting(using transitionContext: UIViewControllerContextTransitioning) { let imageView = UIImageView(image: originImage) - imageView.frame = initialFrame + imageView.frame = originFrame + + let fromView = transitionContext.view(forKey: .from)! + fromView.removeFromSuperview() + + transitionContext.containerView.backgroundColor = UIColor.systemBackground + transitionContext.containerView.addSubview(imageView) + + articleController?.hideClickedImage() + + UIView.animate( + withDuration: duration, + delay:0.0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.2, + animations: { + let imageController = transitionContext.viewController(forKey: .to) as! ImageViewController + imageView.frame = imageController.zoomedFrame + }, completion: { _ in + imageView.removeFromSuperview() + let toView = transitionContext.view(forKey: .to)! + transitionContext.containerView.addSubview(toView) + transitionContext.completeTransition(true) + }) + } + + private func animateTransitionReturning(using transitionContext: UIViewControllerContextTransitioning) { + let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController + let imageView = UIImageView(image: originImage) + imageView.frame = imageController.zoomedFrame let fromView = transitionContext.view(forKey: .from)! fromView.removeFromSuperview() - if presenting { - - transitionContext.containerView.backgroundColor = UIColor.systemBackground - transitionContext.containerView.addSubview(imageView) - - UIView.animate( - withDuration: duration, - delay:0.0, - usingSpringWithDamping: 0.8, - initialSpringVelocity: 0.2, - animations: { - imageView.frame = targetFrame - }, completion: { _ in - imageView.removeFromSuperview() - let toView = transitionContext.view(forKey: .to)! - transitionContext.containerView.addSubview(toView) - transitionContext.completeTransition(true) - }) - - } else { + let toView = transitionContext.view(forKey: .to)! + transitionContext.containerView.addSubview(toView) + transitionContext.containerView.addSubview(imageView) - let toView = transitionContext.view(forKey: .to)! - transitionContext.containerView.addSubview(toView) - transitionContext.containerView.addSubview(imageView) - - UIView.animate( - withDuration: duration, - delay:0.0, - animations: { - imageView.frame = targetFrame - imageView.alpha = 0 - }, completion: { _ in - imageView.removeFromSuperview() - transitionContext.completeTransition(true) - }) - - } - + UIView.animate( + withDuration: duration, + delay:0.0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.2, + animations: { + imageView.frame = self.originFrame + }, completion: { _ in + self.articleController?.showClickedImage() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + imageView.removeFromSuperview() + transitionContext.completeTransition(true) + } + } + }) } } diff --git a/iOS/Resources/main_ios.js b/iOS/Resources/main_ios.js index 0e248a0d7..21d6bdb83 100644 --- a/iOS/Resources/main_ios.js +++ b/iOS/Resources/main_ios.js @@ -1,5 +1,7 @@ // Used to pop a resizable image view async function imageWasClicked(img) { + img.classList.add("nnwClicked"); + const rect = img.getBoundingClientRect(); var message = { @@ -31,7 +33,21 @@ async function imageWasClicked(img) { } -// Add the click listeners for images +// Used to animate the transition to a fullscreen image +function hideClickedImage() { + var img = document.querySelector('.nnwClicked') + img.style.opacity = 0 +} + +// Used to animate the transition from a fullscreen image +function showClickedImage() { + var img = document.querySelector('.nnwClicked') + img.classList.remove("nnwClicked"); + img.style.opacity = 1 + window.webkit.messageHandlers.imageWasShown.postMessage(""); +} + +// Add the click listener for images function imageClicks() { window.onclick = function(event) { if (event.target.matches('img')) {