2024-03-19 18:58:51 +00:00
|
|
|
//
|
|
|
|
// GalleryItemViewController.swift
|
|
|
|
// GalleryVC
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 12/28/23.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
import AVFoundation
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
protocol GalleryItemViewControllerDelegate: AnyObject {
|
|
|
|
func isGalleryBeingPresented() -> Bool
|
|
|
|
func addPresentationAnimationCompletion(_ block: @escaping () -> Void)
|
|
|
|
func galleryItemClose(_ item: GalleryItemViewController)
|
|
|
|
func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]?
|
|
|
|
}
|
|
|
|
|
|
|
|
class GalleryItemViewController: UIViewController {
|
|
|
|
private weak var delegate: GalleryItemViewControllerDelegate?
|
|
|
|
|
|
|
|
let itemIndex: Int
|
|
|
|
let content: GalleryContentViewController
|
2024-03-29 01:32:11 +00:00
|
|
|
private var overlayVC: UIViewController?
|
2024-03-19 18:58:51 +00:00
|
|
|
|
|
|
|
private var activityIndicator: UIActivityIndicatorView?
|
|
|
|
private(set) var scrollView: UIScrollView!
|
|
|
|
private var topControlsView: UIView!
|
|
|
|
private var shareButton: UIButton!
|
|
|
|
private var shareButtonLeadingConstraint: NSLayoutConstraint!
|
|
|
|
private var shareButtonTopConstraint: NSLayoutConstraint!
|
|
|
|
private var closeButtonTrailingConstraint: NSLayoutConstraint!
|
|
|
|
private var closeButtonTopConstraint: NSLayoutConstraint!
|
|
|
|
private var bottomControlsView: UIStackView!
|
|
|
|
private(set) var captionTextView: UITextView!
|
2024-03-29 01:32:11 +00:00
|
|
|
|
|
|
|
private var singleTap: UITapGestureRecognizer!
|
|
|
|
private var doubleTap: UITapGestureRecognizer!
|
2024-03-19 18:58:51 +00:00
|
|
|
|
|
|
|
private var contentViewLeadingConstraint: NSLayoutConstraint?
|
|
|
|
private var contentViewTopConstraint: NSLayoutConstraint?
|
|
|
|
|
|
|
|
private(set) var controlsVisible: Bool = true
|
|
|
|
private(set) var scrollAndZoomEnabled = true
|
|
|
|
|
2024-04-01 16:40:30 +00:00
|
|
|
private var scrollViewSizeForLastZoomScaleUpdate: CGSize?
|
2024-03-19 18:58:51 +00:00
|
|
|
override var prefersHomeIndicatorAutoHidden: Bool {
|
|
|
|
return !controlsVisible
|
|
|
|
}
|
|
|
|
|
|
|
|
init(delegate: GalleryItemViewControllerDelegate, itemIndex: Int, content: GalleryContentViewController) {
|
|
|
|
self.delegate = delegate
|
|
|
|
self.itemIndex = itemIndex
|
|
|
|
self.content = content
|
|
|
|
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
|
|
|
|
content.container = self
|
|
|
|
}
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
2024-03-30 02:06:28 +00:00
|
|
|
scrollView = UIScrollView()
|
2024-03-19 18:58:51 +00:00
|
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
scrollView.delegate = self
|
|
|
|
|
|
|
|
view.addSubview(scrollView)
|
|
|
|
|
|
|
|
addContent()
|
|
|
|
centerContent()
|
|
|
|
|
2024-03-29 01:32:11 +00:00
|
|
|
overlayVC = content.contentOverlayAccessoryViewController
|
|
|
|
if let overlayVC {
|
|
|
|
overlayVC.view.isHidden = activityIndicator != nil
|
|
|
|
overlayVC.view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
view.addSubview(overlayVC.view)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
overlayVC.view.leadingAnchor.constraint(equalTo: content.view.leadingAnchor),
|
|
|
|
overlayVC.view.trailingAnchor.constraint(equalTo: content.view.trailingAnchor),
|
|
|
|
overlayVC.view.topAnchor.constraint(equalTo: content.view.topAnchor),
|
|
|
|
overlayVC.view.bottomAnchor.constraint(equalTo: content.view.bottomAnchor),
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
2024-03-19 18:58:51 +00:00
|
|
|
topControlsView = UIView()
|
|
|
|
topControlsView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
view.addSubview(topControlsView)
|
|
|
|
|
2024-03-30 02:10:14 +00:00
|
|
|
var shareConfig = UIButton.Configuration.gray()
|
2024-03-30 19:21:23 +00:00
|
|
|
shareConfig.cornerStyle = .dynamic
|
2024-03-30 02:10:14 +00:00
|
|
|
shareConfig.background.backgroundColor = .black.withAlphaComponent(0.25)
|
2024-03-19 18:58:51 +00:00
|
|
|
shareConfig.baseForegroundColor = .white
|
|
|
|
shareConfig.image = UIImage(systemName: "square.and.arrow.up")
|
2024-03-30 19:21:23 +00:00
|
|
|
shareConfig.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
|
2024-03-19 18:58:51 +00:00
|
|
|
shareButton = UIButton(configuration: shareConfig)
|
|
|
|
shareButton.addTarget(self, action: #selector(shareButtonPressed), for: .touchUpInside)
|
2024-03-30 19:17:26 +00:00
|
|
|
shareButton.isPointerInteractionEnabled = true
|
|
|
|
shareButton.pointerStyleProvider = { button, effect, shape in
|
|
|
|
return UIPointerStyle(effect: .highlight(effect.preview), shape: .roundedRect(button.frame))
|
|
|
|
}
|
2024-03-31 18:12:07 +00:00
|
|
|
shareButton.preferredBehavioralStyle = .pad
|
2024-03-19 18:58:51 +00:00
|
|
|
shareButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
updateShareButton()
|
|
|
|
topControlsView.addSubview(shareButton)
|
|
|
|
|
2024-03-30 02:10:14 +00:00
|
|
|
var closeConfig = UIButton.Configuration.gray()
|
2024-03-30 19:21:23 +00:00
|
|
|
closeConfig.cornerStyle = .dynamic
|
2024-03-30 02:10:14 +00:00
|
|
|
closeConfig.background.backgroundColor = .black.withAlphaComponent(0.25)
|
2024-03-19 18:58:51 +00:00
|
|
|
closeConfig.baseForegroundColor = .white
|
|
|
|
closeConfig.image = UIImage(systemName: "xmark")
|
2024-03-30 19:21:23 +00:00
|
|
|
closeConfig.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
|
2024-03-19 18:58:51 +00:00
|
|
|
let closeButton = UIButton(configuration: closeConfig)
|
|
|
|
closeButton.addTarget(self, action: #selector(closeButtonPressed), for: .touchUpInside)
|
2024-03-30 19:17:26 +00:00
|
|
|
closeButton.isPointerInteractionEnabled = true
|
|
|
|
closeButton.pointerStyleProvider = { button, effect, shape in
|
|
|
|
return UIPointerStyle(effect: .highlight(effect.preview), shape: .roundedRect(button.frame))
|
|
|
|
}
|
2024-03-31 18:12:07 +00:00
|
|
|
closeButton.preferredBehavioralStyle = .pad
|
2024-03-19 18:58:51 +00:00
|
|
|
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
topControlsView.addSubview(closeButton)
|
|
|
|
|
|
|
|
bottomControlsView = UIStackView()
|
|
|
|
bottomControlsView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
bottomControlsView.axis = .vertical
|
|
|
|
bottomControlsView.alignment = .fill
|
|
|
|
bottomControlsView.backgroundColor = .black.withAlphaComponent(0.5)
|
|
|
|
view.addSubview(bottomControlsView)
|
|
|
|
|
|
|
|
if let controlsAccessory = content.bottomControlsAccessoryViewController {
|
|
|
|
addChild(controlsAccessory)
|
|
|
|
bottomControlsView.addArrangedSubview(controlsAccessory.view)
|
|
|
|
controlsAccessory.didMove(toParent: self)
|
|
|
|
|
|
|
|
// Make sure the controls accessory is within the safe area.
|
|
|
|
let spacer = UIView()
|
|
|
|
bottomControlsView.addArrangedSubview(spacer)
|
|
|
|
let spacerTopConstraint = spacer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
|
|
|
|
spacerTopConstraint.priority = .init(999)
|
|
|
|
spacerTopConstraint.isActive = true
|
|
|
|
}
|
|
|
|
|
|
|
|
captionTextView = UITextView()
|
|
|
|
captionTextView.backgroundColor = .clear
|
|
|
|
captionTextView.textColor = .white
|
|
|
|
captionTextView.isEditable = false
|
|
|
|
captionTextView.isSelectable = true
|
|
|
|
captionTextView.font = .preferredFont(forTextStyle: .body)
|
|
|
|
captionTextView.adjustsFontForContentSizeCategory = true
|
2024-03-29 01:32:11 +00:00
|
|
|
captionTextView.alwaysBounceVertical = true
|
2024-03-19 18:58:51 +00:00
|
|
|
updateCaptionTextView()
|
|
|
|
bottomControlsView.addArrangedSubview(captionTextView)
|
|
|
|
|
2024-03-31 18:12:07 +00:00
|
|
|
#if targetEnvironment(macCatalyst)
|
|
|
|
closeButtonTopConstraint = closeButton.topAnchor.constraint(equalTo: topControlsView.safeAreaLayoutGuide.topAnchor)
|
|
|
|
shareButtonTopConstraint = shareButton.topAnchor.constraint(equalTo: topControlsView.safeAreaLayoutGuide.topAnchor)
|
|
|
|
#else
|
2024-03-19 18:58:51 +00:00
|
|
|
closeButtonTopConstraint = closeButton.topAnchor.constraint(equalTo: topControlsView.topAnchor)
|
|
|
|
shareButtonTopConstraint = shareButton.topAnchor.constraint(equalTo: topControlsView.topAnchor)
|
2024-03-31 18:12:07 +00:00
|
|
|
#endif
|
|
|
|
closeButtonTrailingConstraint = topControlsView.trailingAnchor.constraint(equalTo: closeButton.trailingAnchor)
|
|
|
|
shareButtonLeadingConstraint = shareButton.leadingAnchor.constraint(equalTo: topControlsView.leadingAnchor)
|
2024-03-19 18:58:51 +00:00
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
|
|
|
|
|
topControlsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
topControlsView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
|
topControlsView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
|
|
|
|
shareButtonLeadingConstraint,
|
|
|
|
shareButtonTopConstraint,
|
|
|
|
shareButton.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
|
2024-03-30 19:21:23 +00:00
|
|
|
shareButton.widthAnchor.constraint(equalTo: shareButton.heightAnchor),
|
2024-03-19 18:58:51 +00:00
|
|
|
|
|
|
|
closeButtonTrailingConstraint,
|
|
|
|
closeButtonTopConstraint,
|
|
|
|
closeButton.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
|
2024-03-30 19:21:23 +00:00
|
|
|
closeButton.widthAnchor.constraint(equalTo: closeButton.heightAnchor),
|
2024-03-19 18:58:51 +00:00
|
|
|
|
|
|
|
bottomControlsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
bottomControlsView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
|
bottomControlsView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
|
|
|
|
|
captionTextView.heightAnchor.constraint(equalToConstant: 150),
|
|
|
|
])
|
|
|
|
|
2024-03-30 19:18:17 +00:00
|
|
|
updateTopControlsInsets()
|
|
|
|
|
2024-03-29 01:32:11 +00:00
|
|
|
singleTap = UITapGestureRecognizer(target: self, action: #selector(viewPressed))
|
|
|
|
singleTap.delegate = self
|
|
|
|
doubleTap = UITapGestureRecognizer(target: self, action: #selector(viewDoublePressed))
|
|
|
|
doubleTap.delegate = self
|
2024-03-19 18:58:51 +00:00
|
|
|
doubleTap.numberOfTapsRequired = 2
|
2024-03-31 19:44:15 +00:00
|
|
|
// This is needed to prevent a delay between tapping a button on and the action firing on Catalyst and Designed for iPad
|
|
|
|
doubleTap.delaysTouchesEnded = false
|
2024-03-19 18:58:51 +00:00
|
|
|
// this requirement is needed to make sure the double tap is ever recognized
|
|
|
|
singleTap.require(toFail: doubleTap)
|
|
|
|
view.addGestureRecognizer(singleTap)
|
|
|
|
view.addGestureRecognizer(doubleTap)
|
|
|
|
}
|
|
|
|
|
|
|
|
override func viewSafeAreaInsetsDidChange() {
|
|
|
|
super.viewSafeAreaInsetsDidChange()
|
|
|
|
|
|
|
|
updateZoomScale(resetZoom: false)
|
|
|
|
// Ensure the transform is correct if the controls are hidden
|
|
|
|
setControlsVisible(controlsVisible, animated: false)
|
|
|
|
|
|
|
|
updateTopControlsInsets()
|
|
|
|
}
|
|
|
|
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
|
|
super.viewDidLayoutSubviews()
|
|
|
|
|
2024-04-01 16:40:30 +00:00
|
|
|
// When the scrollView size changes, make sure the zoom scale is up-to-date since it depends on the scrollView's bounds.
|
|
|
|
// This might also fix an issue on macOS (Designed for iPad) where the content isn't placed correctly. See #446
|
|
|
|
if scrollViewSizeForLastZoomScaleUpdate != scrollView.bounds.size {
|
|
|
|
scrollViewSizeForLastZoomScaleUpdate = scrollView.bounds.size
|
|
|
|
updateZoomScale(resetZoom: true)
|
|
|
|
}
|
2024-03-19 18:58:51 +00:00
|
|
|
centerContent()
|
|
|
|
}
|
|
|
|
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
|
|
super.viewDidAppear(animated)
|
|
|
|
|
|
|
|
if controlsVisible && !captionTextView.isHidden {
|
|
|
|
captionTextView.flashScrollIndicators()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func takeContent() -> GalleryContentViewController {
|
|
|
|
content.willMove(toParent: nil)
|
|
|
|
content.removeFromParent()
|
|
|
|
content.view.removeFromSuperview()
|
|
|
|
return content
|
|
|
|
}
|
|
|
|
|
|
|
|
func addContent() {
|
2024-03-29 01:32:11 +00:00
|
|
|
content.loadViewIfNeeded()
|
|
|
|
|
2024-03-20 15:49:00 +00:00
|
|
|
content.setControlsVisible(controlsVisible, animated: false)
|
|
|
|
|
2024-03-19 18:58:51 +00:00
|
|
|
content.view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
if content.parent != self {
|
|
|
|
addChild(content)
|
|
|
|
content.didMove(toParent: self)
|
|
|
|
}
|
|
|
|
if scrollAndZoomEnabled {
|
|
|
|
scrollView.addSubview(content.view)
|
|
|
|
contentViewLeadingConstraint = content.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
|
|
|
|
contentViewLeadingConstraint!.isActive = true
|
|
|
|
contentViewTopConstraint = content.view.topAnchor.constraint(equalTo: scrollView.topAnchor)
|
|
|
|
contentViewTopConstraint!.isActive = true
|
|
|
|
updateZoomScale(resetZoom: true)
|
|
|
|
} else {
|
|
|
|
// If the content was previously added, deactivate the old constraints.
|
|
|
|
contentViewLeadingConstraint?.isActive = false
|
|
|
|
contentViewTopConstraint?.isActive = false
|
|
|
|
|
|
|
|
view.addSubview(content.view)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
content.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
content.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
|
content.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
|
])
|
|
|
|
}
|
2024-03-29 01:32:11 +00:00
|
|
|
|
|
|
|
if let overlayVC {
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
overlayVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
overlayVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
|
overlayVC.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
overlayVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
2024-03-19 18:58:51 +00:00
|
|
|
content.view.layoutIfNeeded()
|
|
|
|
}
|
|
|
|
|
|
|
|
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
|
|
|
controlsVisible = visible
|
|
|
|
guard let topControlsView,
|
|
|
|
let bottomControlsView else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
func updateControlsViews() {
|
|
|
|
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
|
|
|
|
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
|
2024-03-20 15:49:00 +00:00
|
|
|
content.setControlsVisible(visible, animated: animated)
|
2024-03-19 18:58:51 +00:00
|
|
|
}
|
|
|
|
if animated {
|
|
|
|
let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters())
|
|
|
|
animator.addAnimations(updateControlsViews)
|
|
|
|
animator.startAnimation()
|
|
|
|
} else {
|
|
|
|
updateControlsViews()
|
|
|
|
}
|
|
|
|
|
|
|
|
setNeedsUpdateOfHomeIndicatorAutoHidden()
|
|
|
|
}
|
|
|
|
|
2024-03-31 16:35:10 +00:00
|
|
|
func updateZoomScale(resetZoom: Bool) {
|
2024-04-01 16:40:30 +00:00
|
|
|
scrollView.contentSize = content.contentSize
|
|
|
|
|
2024-03-19 18:58:51 +00:00
|
|
|
guard scrollAndZoomEnabled else {
|
|
|
|
scrollView.maximumZoomScale = 1
|
|
|
|
scrollView.minimumZoomScale = 1
|
|
|
|
scrollView.zoomScale = 1
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard content.contentSize.width > 0 && content.contentSize.height > 0 else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-30 02:06:28 +00:00
|
|
|
let heightScale = view.bounds.height / content.contentSize.height
|
|
|
|
let widthScale = view.bounds.width / content.contentSize.width
|
2024-03-19 18:58:51 +00:00
|
|
|
let minScale = min(widthScale, heightScale)
|
|
|
|
let maxScale = minScale >= 1 ? minScale + 2 : 2
|
|
|
|
|
|
|
|
scrollView.minimumZoomScale = minScale
|
|
|
|
scrollView.maximumZoomScale = maxScale
|
|
|
|
if resetZoom {
|
|
|
|
scrollView.zoomScale = minScale
|
|
|
|
} else {
|
|
|
|
scrollView.zoomScale = max(minScale, min(maxScale, scrollView.zoomScale))
|
|
|
|
}
|
|
|
|
|
|
|
|
centerContent()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func centerContent() {
|
|
|
|
guard scrollAndZoomEnabled else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: use frame for the content.view, because that's in the coordinate space of the scroll view
|
|
|
|
// which means it's already been scaled by the zoom factor.
|
|
|
|
let yOffset = max(0, (view.bounds.height - content.view.frame.height) / 2)
|
|
|
|
contentViewTopConstraint!.constant = yOffset
|
|
|
|
|
|
|
|
let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2)
|
|
|
|
contentViewLeadingConstraint!.constant = xOffset
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateShareButton() {
|
|
|
|
shareButton.isEnabled = !content.activityItemsForSharing.isEmpty
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateCaptionTextView() {
|
|
|
|
guard let caption = content.caption,
|
|
|
|
!caption.isEmpty else {
|
|
|
|
captionTextView.isHidden = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
captionTextView.text = caption
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateTopControlsInsets() {
|
|
|
|
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 islandDeviceTopInsets: [CGFloat] = [
|
|
|
|
59, // iPhone 14 Pro, 14 Pro Max, 15 Pro, 15 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.imageView?.bounds.width ?? 0)) / 2
|
|
|
|
shareButtonLeadingConstraint.constant = offset
|
|
|
|
closeButtonTrailingConstraint.constant = offset
|
|
|
|
} else if islandDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
|
|
|
shareButtonLeadingConstraint.constant = 24
|
|
|
|
shareButtonTopConstraint.constant = 24
|
|
|
|
closeButtonTrailingConstraint.constant = 24
|
|
|
|
closeButtonTopConstraint.constant = 24
|
2024-03-30 19:18:17 +00:00
|
|
|
} else {
|
|
|
|
shareButtonLeadingConstraint.constant = 8
|
|
|
|
shareButtonTopConstraint.constant = 8
|
|
|
|
closeButtonTrailingConstraint.constant = 8
|
|
|
|
closeButtonTopConstraint.constant = 8
|
2024-03-19 18:58:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
|
|
|
|
var zoomRect = CGRect.zero
|
|
|
|
zoomRect.size.width = content.view.frame.width / scale
|
|
|
|
zoomRect.size.height = content.view.frame.height / scale
|
|
|
|
let newCenter = scrollView.convert(center, to: content.view)
|
|
|
|
zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
|
|
|
|
zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
|
|
|
|
return zoomRect
|
|
|
|
}
|
|
|
|
|
|
|
|
private func animateZoomOut() {
|
|
|
|
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
|
|
|
|
animator.addAnimations {
|
|
|
|
self.scrollView.zoomScale = self.scrollView.minimumZoomScale
|
|
|
|
self.scrollView.layoutIfNeeded()
|
|
|
|
}
|
|
|
|
animator.startAnimation()
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Interaction
|
|
|
|
|
|
|
|
@objc private func viewPressed() {
|
|
|
|
if scrollAndZoomEnabled,
|
|
|
|
scrollView.zoomScale > scrollView.minimumZoomScale {
|
|
|
|
animateZoomOut()
|
|
|
|
} else {
|
|
|
|
setControlsVisible(!controlsVisible, animated: true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func viewDoublePressed(_ recognizer: UITapGestureRecognizer) {
|
|
|
|
guard scrollAndZoomEnabled else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
|
|
|
let point = recognizer.location(in: recognizer.view)
|
|
|
|
let scale = min(
|
|
|
|
max(
|
|
|
|
scrollView.bounds.width / content.contentSize.width,
|
|
|
|
scrollView.bounds.height / content.contentSize.height,
|
|
|
|
scrollView.zoomScale + 0.75
|
|
|
|
),
|
|
|
|
scrollView.maximumZoomScale
|
|
|
|
)
|
|
|
|
let rect = zoomRectFor(scale: scale, center: point)
|
|
|
|
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
|
|
|
|
animator.addAnimations {
|
|
|
|
self.scrollView.zoom(to: rect, animated: false)
|
|
|
|
self.view.layoutIfNeeded()
|
|
|
|
}
|
|
|
|
animator.startAnimation()
|
|
|
|
} else {
|
|
|
|
animateZoomOut()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func closeButtonPressed() {
|
|
|
|
delegate?.galleryItemClose(self)
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func shareButtonPressed() {
|
|
|
|
let items = content.activityItemsForSharing
|
|
|
|
guard !items.isEmpty else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: delegate?.galleryItemApplicationActivities(self))
|
|
|
|
activityVC.popoverPresentationController?.sourceView = shareButton
|
|
|
|
present(activityVC, animated: true)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
2024-03-20 15:49:00 +00:00
|
|
|
var galleryControlsVisible: Bool {
|
|
|
|
controlsVisible
|
|
|
|
}
|
|
|
|
|
2024-03-19 18:58:51 +00:00
|
|
|
func setGalleryContentLoading(_ loading: Bool) {
|
|
|
|
if loading {
|
2024-03-29 01:32:11 +00:00
|
|
|
overlayVC?.view.isHidden = true
|
2024-03-19 18:58:51 +00:00
|
|
|
if activityIndicator == nil {
|
|
|
|
let activityIndicator = UIActivityIndicatorView(style: .large)
|
|
|
|
self.activityIndicator = activityIndicator
|
|
|
|
activityIndicator.startAnimating()
|
|
|
|
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
view.addSubview(activityIndicator)
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
|
|
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
|
|
|
])
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if let activityIndicator {
|
|
|
|
// If we're in the middle of the presentation animation,
|
|
|
|
// wait until it finishes to hide the loading indicator.
|
|
|
|
// Since the updated content frame won't affect the animation,
|
|
|
|
// make sure the loading indicator remains visible.
|
|
|
|
if let delegate,
|
|
|
|
delegate.isGalleryBeingPresented() {
|
|
|
|
delegate.addPresentationAnimationCompletion { [unowned self] in
|
|
|
|
self.setGalleryContentLoading(false)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
activityIndicator.removeFromSuperview()
|
|
|
|
self.activityIndicator = nil
|
2024-03-29 01:32:11 +00:00
|
|
|
self.overlayVC?.view.isHidden = false
|
2024-03-19 18:58:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func galleryContentChanged() {
|
|
|
|
updateZoomScale(resetZoom: true)
|
|
|
|
updateShareButton()
|
|
|
|
updateCaptionTextView()
|
|
|
|
}
|
|
|
|
|
|
|
|
func disableGalleryScrollAndZoom() {
|
|
|
|
scrollAndZoomEnabled = false
|
|
|
|
updateZoomScale(resetZoom: true)
|
|
|
|
scrollView.isScrollEnabled = false
|
|
|
|
// Make sure the content is re-added with the correct constraints
|
|
|
|
if content.parent == self {
|
|
|
|
addContent()
|
|
|
|
}
|
|
|
|
}
|
2024-03-29 01:32:11 +00:00
|
|
|
|
|
|
|
func setGalleryControlsVisible(_ visible: Bool, animated: Bool) {
|
|
|
|
setControlsVisible(visible, animated: animated)
|
|
|
|
}
|
2024-03-19 18:58:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
extension GalleryItemViewController: UIScrollViewDelegate {
|
|
|
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
|
|
if scrollAndZoomEnabled {
|
|
|
|
return content.view
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
|
|
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
|
|
|
setControlsVisible(true, animated: true)
|
|
|
|
} else {
|
|
|
|
setControlsVisible(false, animated: true)
|
|
|
|
}
|
|
|
|
|
|
|
|
centerContent()
|
|
|
|
scrollView.layoutIfNeeded()
|
|
|
|
}
|
|
|
|
}
|
2024-03-29 01:32:11 +00:00
|
|
|
|
|
|
|
extension GalleryItemViewController: UIGestureRecognizerDelegate {
|
|
|
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
|
|
if gestureRecognizer == singleTap {
|
|
|
|
let loc = gestureRecognizer.location(in: view)
|
|
|
|
return !topControlsView.frame.contains(loc) && !bottomControlsView.frame.contains(loc)
|
|
|
|
} else if gestureRecognizer == doubleTap {
|
|
|
|
let loc = gestureRecognizer.location(in: content.view)
|
|
|
|
return content.view.bounds.contains(loc)
|
|
|
|
} else {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|