Tusker/Tusker/Screens/Large Image/LargeImageViewController.swift

279 lines
10 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 {
weak var animationSourceView: UIImageView?
var largeImageController: LargeImageViewController? { self }
var animationImage: UIImage? { contentView.animationImage }
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: LargeImageContentView {
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
private var prevZoomScale: CGFloat?
private var isGrayscale = false
var isInteractivelyAnimatingDismissal: Bool = false {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
return !isInteractivelyAnimatingDismissal
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .none
}
override var prefersHomeIndicatorAutoHidden: Bool {
return !controlsVisible
}
init(contentView: LargeImageContentView, 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)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
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()
// 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()
let notchedDeviceTopInsets: [CGFloat] = [
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
48, // iPhone XR, 11
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
50, // iPhone 12 mini, 13 mini
]
let pillDeviceTopInsets: [CGFloat] = [
59, // iPhone 14 Pro, 14 Pro Max
]
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
// the notch width is not the same for the iPhones 13,
// but what we actually want is the same offset from the edges
// since the corner radius didn't change
let notchWidth: CGFloat = 210
let earWidth = (view.bounds.width - notchWidth) / 2
let offset = (earWidth - shareButton.bounds.width) / 2
shareButtonLeadingConstraint.constant = offset
closeButtonTrailingConstraint.constant = offset
} else if pillDeviceTopInsets.contains(view.safeAreaInsets.top) {
shareButtonLeadingConstraint.constant = 24
shareButtonTopConstraint.constant = 24
closeButtonTrailingConstraint.constant = 24
closeButtonTopConstraint.constant = 24
}
}
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
isGrayscale = Preferences.shared.grayscaleImages
contentView.grayscaleStateChanged()
}
}
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
self.controlsVisible = controlsVisible
if animated {
UIView.animate(withDuration: 0.2) {
self.contentView.setControlsVisible(controlsVisible)
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)
}
}