forked from shadowfacts/Tusker
251 lines
9.2 KiB
Swift
251 lines
9.2 KiB
Swift
//
|
|
// 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 largeImageController: LargeImageViewController? { self }
|
|
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 {
|
|
didSet {
|
|
oldValue.removeFromSuperview()
|
|
setupContentView()
|
|
}
|
|
}
|
|
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 preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
|
return .none
|
|
}
|
|
|
|
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()
|
|
|
|
setupContentView()
|
|
|
|
setControlsVisible(initialControlsVisible, animated: false)
|
|
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
|
|
|
|
scrollView.delegate = self
|
|
|
|
if let imageDescription = imageDescription,
|
|
!imageDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
descriptionLabel.text = imageDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
} 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)
|
|
}
|
|
|
|
private func setupContentView() {
|
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
|
scrollView.addSubview(contentView)
|
|
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
|
|
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
|
|
NSLayoutConstraint.activate([
|
|
contentViewLeadingConstraint,
|
|
contentViewTopConstraint,
|
|
])
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
}
|