// // LargeImageViewController.swift // Tusker // // Created by Shadowfacts on 8/31/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController { typealias ContentView = UIView & LargeImageContentView weak var animationSourceView: UIImageView? var animationImage: UIImage? { contentView.animationImage } var animationGifData: Data? { contentView.animationGifData } var dismissInteractionController: LargeImageInteractionController? @IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var topControlsView: UIView! @IBOutlet weak var topControlsHeightConstraint: NSLayoutConstraint! @IBOutlet weak var shareButton: UIButton! @IBOutlet weak var shareButtonTopConstraint: NSLayoutConstraint! @IBOutlet weak var shareButtonLeadingConstraint: NSLayoutConstraint! @IBOutlet weak var closeButton: UIButton! @IBOutlet weak var closeButtonTopConstraint: NSLayoutConstraint! @IBOutlet weak var closeButtonTrailingConstraint: NSLayoutConstraint! @IBOutlet weak var bottomControlsView: UIView! @IBOutlet weak var descriptionLabel: UILabel! var contentView: ContentView var contentViewLeadingConstraint: NSLayoutConstraint! var contentViewTopConstraint: NSLayoutConstraint! var imageDescription: String? var initialControlsVisible = true private(set) var controlsVisible = true { didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() } } var shrinkGestureEnabled = true var prevZoomScale: CGFloat? override var prefersStatusBarHidden: Bool { return true } override var prefersHomeIndicatorAutoHidden: Bool { return !controlsVisible } init(contentView: ContentView, description: String?, sourceView: UIImageView?) { self.imageDescription = description self.animationSourceView = sourceView self.contentView = contentView super.init(nibName: "LargeImageViewController", bundle: nil) modalPresentationStyle = .fullScreen } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() contentView.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(contentView) contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor) contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor) NSLayoutConstraint.activate([ contentViewLeadingConstraint, contentViewTopConstraint, ]) setControlsVisible(initialControlsVisible, animated: false) shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty scrollView.delegate = self if let imageDescription = imageDescription { descriptionLabel.text = imageDescription } else { bottomControlsView.isHidden = true } if shrinkGestureEnabled { dismissInteractionController = LargeImageInteractionController(viewController: self) } view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(scrollViewPressed(_:)))) let doubleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:))) doubleTap.numberOfTapsRequired = 2 view.addGestureRecognizer(doubleTap) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // todo: does this need to be in viewDidLayoutSubviews? // limit the image height to the safe area height, so the image doesn't overlap the top controls // while zoomed all the way out let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom let heightScale = maxHeight / contentView.intrinsicContentSize.height let widthScale = view.bounds.width / contentView.intrinsicContentSize.width let minScale = min(widthScale, heightScale) scrollView.minimumZoomScale = minScale scrollView.zoomScale = minScale scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2 centerImage() // todo: does this need to be in viewDidLayoutSubviews? if view.safeAreaInsets.top == 44 { // running on iPhone X style notched device let notchWidth: CGFloat = 209 let earWidth = (view.bounds.width - notchWidth) / 2 let offset = (earWidth - shareButton.bounds.width) / 2 shareButtonLeadingConstraint.constant = offset closeButtonTrailingConstraint.constant = offset } } func setControlsVisible(_ controlsVisible: Bool, animated: Bool) { self.controlsVisible = controlsVisible if animated { UIView.animate(withDuration: 0.2) { self.updateControlsView() } } else { updateControlsView() } } func updateControlsView() { let topOffset = self.controlsVisible ? 0 : -self.topControlsView.bounds.height self.topControlsView.transform = CGAffineTransform(translationX: 0, y: topOffset) if self.imageDescription != nil { let bottomOffset = self.controlsVisible ? 0 : self.bottomControlsView.bounds.height + self.view.safeAreaInsets.bottom self.bottomControlsView.transform = CGAffineTransform(translationX: 0, y: bottomOffset) } } func viewForZooming(in scrollView: UIScrollView) -> UIView? { return contentView } func scrollViewDidZoom(_ scrollView: UIScrollView) { centerImage() let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale if scrollView.zoomScale <= scrollView.minimumZoomScale { setControlsVisible(true, animated: true) } else if scrollView.zoomScale > prevZoomScale { setControlsVisible(false, animated: true) } self.prevZoomScale = scrollView.zoomScale } func centerImage() { let yOffset = max(0, (view.bounds.size.height - contentView.frame.height) / 2) contentViewTopConstraint.constant = yOffset let xOffset = max(0, (view.bounds.size.width - contentView.frame.width) / 2) contentViewLeadingConstraint.constant = xOffset } func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect { var zoomRect = CGRect.zero zoomRect.size.width = contentView.frame.width / scale zoomRect.size.height = contentView.frame.height / scale let newCenter = scrollView.convert(center, to: contentView) zoomRect.origin.x = newCenter.x - (zoomRect.width / 2) zoomRect.origin.y = newCenter.y - (zoomRect.height / 2) return zoomRect } // MARK: Interaction func animateZoomOut() { UIView.animate(withDuration: 0.3, animations: { self.scrollView.zoomScale = self.scrollView.minimumZoomScale self.view.layoutIfNeeded() }) } @objc func scrollViewPressed(_ sender: UITapGestureRecognizer) { if scrollView.zoomScale > scrollView.minimumZoomScale { animateZoomOut() } else { setControlsVisible(!controlsVisible, animated: true) } } @objc func scrollViewDoubleTapped(_ recognizer: UITapGestureRecognizer) { if scrollView.zoomScale <= scrollView.minimumZoomScale { let point = recognizer.location(in: recognizer.view) let scale: CGFloat if scrollView.minimumZoomScale < 1 { if 1 - scrollView.zoomScale <= 0.5 { scale = scrollView.zoomScale + 1 } else { scale = 1 } } else { scale = scrollView.maximumZoomScale } let rect = zoomRectFor(scale: scale, center: point) UIView.animate(withDuration: 0.3) { self.scrollView.zoom(to: rect, animated: false) self.view.layoutIfNeeded() } } else { animateZoomOut() } } @IBAction func closeButtonPressed(_ sender: Any) { dismiss(animated: true) } @IBAction func sharePressed(_ sender: Any) { let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: nil) activityVC.popoverPresentationController?.sourceView = shareButton present(activityVC, animated: true) } }