metatext/View Controllers/ImageViewController.swift

308 lines
13 KiB
Swift
Raw Normal View History

2020-10-21 08:07:13 +00:00
// Copyright © 2020 Metabolist. All rights reserved.
2021-01-18 06:03:32 +00:00
import AVFoundation
2021-03-04 01:15:40 +00:00
import BlurHash
2021-02-02 18:02:30 +00:00
import Mastodon
2021-02-22 23:59:33 +00:00
import SDWebImage
2020-10-21 08:07:13 +00:00
import UIKit
import ViewModels
2021-03-05 03:26:57 +00:00
enum ImageError: Error {
case unableToLoad
}
extension ImageError: LocalizedError {
var errorDescription: String? {
switch self {
case .unableToLoad:
return NSLocalizedString("image-error.unable-to-load", comment: "")
}
}
}
2020-11-09 06:22:20 +00:00
final class ImageViewController: UIViewController {
2020-10-22 05:05:50 +00:00
let scrollView = UIScrollView()
2021-02-22 23:59:33 +00:00
let imageView = SDAnimatedImageView()
2020-10-22 05:05:50 +00:00
let playerView = PlayerView()
2020-10-22 22:16:06 +00:00
private let viewModel: AttachmentViewModel?
private let imageURL: URL?
2020-10-21 08:07:13 +00:00
private let contentView = UIView()
private let descriptionBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
private let descriptionTextView = UITextView()
2020-10-22 22:16:06 +00:00
init(viewModel: AttachmentViewModel? = nil, imageURL: URL? = nil) {
2020-10-21 08:07:13 +00:00
self.viewModel = viewModel
2020-10-22 22:16:06 +00:00
self.imageURL = imageURL
2020-10-21 08:07:13 +00:00
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// swiftlint:disable:next function_body_length
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .secondarySystemBackground
view.addSubview(scrollView)
scrollView.delegate = self
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
scrollView.maximumZoomScale = Self.maximumZoomScale
2020-10-22 05:38:37 +00:00
let doubleTapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(handleDoubleTap(gestureRecognizer:)))
doubleTapGestureRecognizer.numberOfTapsRequired = 2
scrollView.addGestureRecognizer(doubleTapGestureRecognizer)
2020-10-21 08:07:13 +00:00
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
2021-02-22 23:59:33 +00:00
imageView.sd_imageIndicator = SDWebImageActivityIndicator.large
imageView.autoPlayAnimatedImage = false
2020-10-21 08:07:13 +00:00
contentView.addSubview(playerView)
playerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(descriptionBackgroundView)
descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false
2020-10-22 22:16:06 +00:00
descriptionBackgroundView.isHidden = viewModel?.attachment.description == nil
|| viewModel?.attachment.description == ""
2020-10-21 08:07:13 +00:00
descriptionBackgroundView.contentView.addSubview(descriptionTextView)
descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
descriptionTextView.backgroundColor = .clear
descriptionTextView.font = .preferredFont(forTextStyle: .caption1)
descriptionTextView.adjustsFontForContentSizeCategory = true
2020-10-22 22:16:06 +00:00
descriptionTextView.text = viewModel?.attachment.description
2020-10-21 08:07:13 +00:00
descriptionTextView.isScrollEnabled = false
descriptionTextView.isEditable = false
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
contentView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
playerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
playerView.topAnchor.constraint(equalTo: contentView.topAnchor),
playerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
playerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
descriptionBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
descriptionBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
descriptionBackgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
2021-03-11 01:40:03 +00:00
descriptionTextView.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor, multiplier: 1 / 4),
2020-10-21 08:07:13 +00:00
descriptionTextView.leadingAnchor.constraint(
equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor),
descriptionTextView.topAnchor.constraint(equalTo: descriptionBackgroundView.topAnchor),
descriptionTextView.trailingAnchor.constraint(
equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor),
descriptionTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
2020-10-22 22:16:06 +00:00
if let viewModel = viewModel {
switch viewModel.attachment.type {
case .image:
imageView.tag = viewModel.tag
playerView.isHidden = true
2021-02-22 23:59:33 +00:00
2021-03-04 01:15:40 +00:00
let placeholderImage: UIImage?
2021-03-29 06:04:14 +00:00
let cachedImageKey = viewModel.attachment.previewUrl?.url.absoluteString
2021-03-04 01:15:40 +00:00
let cachedImage = SDImageCache.shared.imageFromCache(forKey: cachedImageKey)
2021-02-22 23:59:33 +00:00
2021-03-04 01:15:40 +00:00
if cachedImage != nil {
placeholderImage = cachedImage
imageView.sd_imageIndicator = nil
2021-03-04 01:15:40 +00:00
} else if let blurHash = viewModel.attachment.blurhash {
placeholderImage = UIImage(blurHash: blurHash, size: .blurHashSize)
} else {
placeholderImage = nil
}
2021-03-29 06:04:14 +00:00
imageView.sd_setImage(with: viewModel.attachment.url.url,
placeholderImage: placeholderImage) { [weak self] _, error, _, _ in
2021-03-05 03:26:57 +00:00
if error != nil {
let alertItem = AlertItem(error: ImageError.unableToLoad)
self?.present(alertItem: alertItem)
2021-03-05 03:26:57 +00:00
}
}
2020-10-22 22:16:06 +00:00
case .gifv:
playerView.tag = viewModel.tag
imageView.isHidden = true
2021-03-29 06:04:14 +00:00
let player = PlayerCache.shared.player(url: viewModel.attachment.url.url)
2020-10-22 22:16:06 +00:00
player.isMuted = true
playerView.player = player
player.play()
default: break
}
2021-02-02 18:02:30 +00:00
var accessibilityLabel = viewModel.attachment.type.accessibilityName
if let description = viewModel.attachment.description {
accessibilityLabel.appendWithSeparator(description)
}
2020-10-22 22:16:06 +00:00
} else if let imageURL = imageURL {
imageView.tag = imageURL.hashValue
2020-10-21 08:07:13 +00:00
playerView.isHidden = true
2021-02-22 23:59:33 +00:00
imageView.sd_setImage(with: imageURL)
2020-10-21 08:07:13 +00:00
}
2021-02-02 18:02:30 +00:00
contentView.accessibilityLabel = viewModel?.attachment.type.accessibilityName
?? Attachment.AttachmentType.image.accessibilityName
contentView.isAccessibilityElement = true
2020-10-21 08:07:13 +00:00
}
2021-03-11 01:40:03 +00:00
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let textHeight = descriptionTextView.sizeThatFits(.init(
width: descriptionTextView.frame.width,
height: .greatestFiniteMagnitude)).height
descriptionTextView.isScrollEnabled = textHeight > descriptionTextView.frame.height
}
2020-10-21 08:07:13 +00:00
}
extension ImageViewController {
func toggleDescriptionVisibility() {
UIView.animate(withDuration: .shortAnimationDuration) {
self.descriptionBackgroundView.alpha = self.descriptionBackgroundView.alpha > 0 ? 0 : 1
}
}
2021-01-18 06:03:32 +00:00
func presentActivityViewController() {
2021-05-10 06:56:12 +00:00
if let imageData = imageView.image?.sd_imageData(), let url = imageURL ?? viewModel?.attachment.url.url {
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(url.lastPathComponent)
do {
try imageData.write(to: tempURL)
2021-01-19 19:00:26 +00:00
2021-05-10 06:56:12 +00:00
let activityViewController = UIActivityViewController(
activityItems: [tempURL],
applicationActivities: [])
if UIDevice.current.userInterfaceIdiom == .pad {
activityViewController.popoverPresentationController?
.barButtonItem = parent?.navigationItem.rightBarButtonItem
}
present(activityViewController, animated: true)
} catch {
alertUnableToExportMedia()
}
2021-01-18 06:03:32 +00:00
} else if let asset = playerView.player?.currentItem?.asset as? AVURLAsset {
asset.exportWithoutAudioTrack { result in
DispatchQueue.main.async {
switch result {
case let .success(url):
let activityViewController = UIActivityViewController(
activityItems: [url],
applicationActivities: [])
2021-01-19 19:00:26 +00:00
if UIDevice.current.userInterfaceIdiom == .pad {
activityViewController.popoverPresentationController?
.barButtonItem = self.parent?.navigationItem.rightBarButtonItem
}
2021-01-18 06:03:32 +00:00
activityViewController.completionWithItemsHandler = { _, _, _, _ in
try? FileManager.default.removeItem(at: url.deletingLastPathComponent())
}
self.present(activityViewController, animated: true)
case .failure:
2021-05-10 06:56:12 +00:00
self.alertUnableToExportMedia()
2021-01-18 06:03:32 +00:00
}
}
}
}
}
2020-10-21 08:07:13 +00:00
}
extension ImageViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
contentView
}
// https://stackoverflow.com/a/40480610/2484482
func scrollViewDidZoom(_ scrollView: UIScrollView) {
if scrollView.zoomScale > 1,
let contentSize = imageView.image?.size ?? playerView.player?.currentItem?.presentationSize {
let ratio = min(contentView.frame.width / contentSize.width, contentView.frame.height / contentSize.height)
let newWidth = contentSize.width * ratio
let newHeight = contentSize.height * ratio
let horizontalInset = 0.5 * (newWidth * scrollView.zoomScale > contentView.frame.width
? (newWidth - contentView.frame.width)
: (scrollView.frame.width - scrollView.contentSize.width))
let verticalInset = 0.5 * (newHeight * scrollView.zoomScale > contentView.frame.height
? (newHeight - contentView.frame.height)
: (scrollView.frame.height - scrollView.contentSize.height))
scrollView.contentInset = .init(
top: verticalInset,
left: horizontalInset,
bottom: verticalInset,
right: horizontalInset)
} else {
scrollView.contentInset = .zero
}
}
}
private extension ImageViewController {
2020-10-22 05:38:37 +00:00
static let maximumZoomScale: CGFloat = 4
@objc func handleDoubleTap(gestureRecognizer: UITapGestureRecognizer) {
if scrollView.zoomScale == scrollView.minimumZoomScale {
let width = contentView.frame.size.width / scrollView.maximumZoomScale
let height = contentView.frame.size.height / scrollView.maximumZoomScale
let center = scrollView.convert(gestureRecognizer.location(in: gestureRecognizer.view), from: contentView)
scrollView.zoom(
to: CGRect(x: center.x - (width / 2), y: center.y - (height / 2), width: width, height: height),
animated: true)
} else {
scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
}
}
2021-05-10 06:56:12 +00:00
func alertUnableToExportMedia() {
let alertController = UIAlertController(
title: nil,
message: NSLocalizedString("attachment.unable-to-export-media", comment: ""),
preferredStyle: .alert)
let okAction = UIAlertAction(
title: NSLocalizedString("ok", comment: ""),
style: .default) { _ in }
alertController.addAction(okAction)
self.present(alertController, animated: true)
}
2020-10-21 08:07:13 +00:00
}