475 lines
20 KiB
Swift
475 lines
20 KiB
Swift
//
|
|
// AttachmentsContainerView.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 6/16/19.
|
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Pachyderm
|
|
|
|
class AttachmentsContainerView: UIView {
|
|
|
|
weak var delegate: AttachmentViewDelegate?
|
|
|
|
private var attachmentTokens: [AttachmentToken] = []
|
|
var attachments: [Attachment]!
|
|
|
|
let attachmentViews: NSHashTable<AttachmentView> = .weakObjects()
|
|
let attachmentStacks: NSHashTable<UIStackView> = .weakObjects()
|
|
var moreView: UIView?
|
|
private var aspectRatioConstraint: NSLayoutConstraint?
|
|
private(set) var aspectRatio: CGFloat = 16/9 {
|
|
didSet {
|
|
if aspectRatio != aspectRatioConstraint?.multiplier {
|
|
aspectRatioConstraint?.isActive = false
|
|
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio)
|
|
aspectRatioConstraint!.isActive = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var blurView: UIVisualEffectView?
|
|
var hideButtonView: UIVisualEffectView?
|
|
var contentHidden: Bool! {
|
|
didSet {
|
|
guard let blurView = blurView,
|
|
let hideButtonView = hideButtonView else { return }
|
|
|
|
blurView.alpha = self.contentHidden ? 1 : 0
|
|
hideButtonView.alpha = self.contentHidden ? 0 : 1
|
|
}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
commonInit()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
commonInit()
|
|
}
|
|
|
|
private func commonInit() {
|
|
self.isUserInteractionEnabled = true
|
|
self.layer.cornerRadius = 5
|
|
self.layer.masksToBounds = true
|
|
|
|
createBlurView()
|
|
createHideButton()
|
|
}
|
|
|
|
func getAttachmentView(for attachment: Attachment) -> AttachmentView? {
|
|
return attachmentViews.allObjects.first { $0.attachment.id == attachment.id }
|
|
}
|
|
|
|
// MARK: - User Interaface
|
|
|
|
func updateUI(attachments: [Attachment]) {
|
|
let newTokens = attachments.map { AttachmentToken(attachment: $0) }
|
|
|
|
guard self.attachmentTokens != newTokens else {
|
|
return
|
|
}
|
|
|
|
self.attachments = attachments
|
|
self.attachmentTokens = newTokens
|
|
|
|
attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
|
|
attachmentViews.removeAllObjects()
|
|
attachmentStacks.allObjects.forEach { $0.removeFromSuperview() }
|
|
attachmentStacks.removeAllObjects()
|
|
moreView?.removeFromSuperview()
|
|
|
|
var accessibilityElements = [Any]()
|
|
|
|
if attachments.count > 0 {
|
|
self.isHidden = false
|
|
|
|
var aspectRatio: CGFloat = 16/9
|
|
|
|
switch attachments.count {
|
|
case 1:
|
|
let attachmentView = createAttachmentView(index: 0, hSize: .full, vSize: .full)
|
|
attachmentView.layer.cornerRadius = 5
|
|
attachmentView.layer.cornerCurve = .continuous
|
|
attachmentView.layer.masksToBounds = true
|
|
fillView(attachmentView)
|
|
sendSubviewToBack(attachmentView)
|
|
accessibilityElements.append(attachmentView)
|
|
if Preferences.shared.showUncroppedMediaInline,
|
|
let attachmentAspectRatio = attachmentView.attachmentAspectRatio {
|
|
aspectRatio = attachmentAspectRatio
|
|
}
|
|
case 2:
|
|
let left = createAttachmentView(index: 0, hSize: .half, vSize: .full)
|
|
left.layer.cornerRadius = 5
|
|
left.layer.cornerCurve = .continuous
|
|
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
left.layer.masksToBounds = true
|
|
let right = createAttachmentView(index: 1, hSize: .half, vSize: .full)
|
|
right.layer.cornerRadius = 5
|
|
right.layer.cornerCurve = .continuous
|
|
right.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
|
right.layer.masksToBounds = true
|
|
let stack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
|
|
left,
|
|
right
|
|
])
|
|
attachmentStacks.add(stack)
|
|
fillView(stack)
|
|
sendSubviewToBack(stack)
|
|
NSLayoutConstraint.activate([
|
|
left.halfWidth()
|
|
])
|
|
accessibilityElements.append(left)
|
|
accessibilityElements.append(right)
|
|
case 3:
|
|
let left = createAttachmentView(index: 0, hSize: .half, vSize: .full)
|
|
left.layer.cornerRadius = 5
|
|
left.layer.cornerCurve = .continuous
|
|
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
left.layer.masksToBounds = true
|
|
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
|
|
topRight.layer.cornerRadius = 5
|
|
topRight.layer.cornerCurve = .continuous
|
|
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
|
topRight.layer.masksToBounds = true
|
|
let bottomRight = createAttachmentView(index: 2, hSize: .half, vSize: .half)
|
|
bottomRight.layer.cornerRadius = 5
|
|
bottomRight.layer.cornerCurve = .continuous
|
|
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
|
|
bottomRight.layer.masksToBounds = true
|
|
let innerStack = createAttachmentsStack(axis: .vertical, arrangedSubviews: [
|
|
topRight,
|
|
bottomRight
|
|
])
|
|
attachmentStacks.add(innerStack)
|
|
let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
|
|
left,
|
|
innerStack,
|
|
])
|
|
attachmentStacks.add(outerStack)
|
|
fillView(outerStack)
|
|
sendSubviewToBack(outerStack)
|
|
NSLayoutConstraint.activate([
|
|
left.halfWidth(),
|
|
topRight.halfHeight(),
|
|
])
|
|
accessibilityElements.append(left)
|
|
accessibilityElements.append(topRight)
|
|
accessibilityElements.append(bottomRight)
|
|
case 4:
|
|
let topLeft = createAttachmentView(index: 0, hSize: .half, vSize: .half)
|
|
topLeft.layer.cornerRadius = 5
|
|
topLeft.layer.cornerCurve = .continuous
|
|
topLeft.layer.maskedCorners = .layerMinXMinYCorner
|
|
topLeft.layer.masksToBounds = true
|
|
let bottomLeft = createAttachmentView(index: 2, hSize: .half, vSize: .half)
|
|
bottomLeft.layer.cornerRadius = 5
|
|
bottomLeft.layer.cornerCurve = .continuous
|
|
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
|
|
bottomLeft.layer.masksToBounds = true
|
|
let left = createAttachmentsStack(axis: .vertical, arrangedSubviews: [
|
|
topLeft,
|
|
bottomLeft
|
|
])
|
|
attachmentStacks.add(left)
|
|
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
|
|
topRight.layer.cornerRadius = 5
|
|
topRight.layer.cornerCurve = .continuous
|
|
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
|
topRight.layer.masksToBounds = true
|
|
let bottomRight = createAttachmentView(index: 3, hSize: .half, vSize: .half)
|
|
bottomRight.layer.cornerRadius = 5
|
|
bottomRight.layer.cornerCurve = .continuous
|
|
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
|
|
bottomRight.layer.masksToBounds = true
|
|
let right = createAttachmentsStack(axis: .vertical, arrangedSubviews: [
|
|
topRight,
|
|
bottomRight
|
|
])
|
|
attachmentStacks.add(right)
|
|
let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
|
|
left,
|
|
right,
|
|
])
|
|
attachmentStacks.add(outerStack)
|
|
fillView(outerStack)
|
|
sendSubviewToBack(outerStack)
|
|
NSLayoutConstraint.activate([
|
|
left.halfWidth(),
|
|
topLeft.halfHeight(),
|
|
topRight.halfHeight(),
|
|
])
|
|
accessibilityElements.append(topLeft)
|
|
accessibilityElements.append(topRight)
|
|
accessibilityElements.append(bottomLeft)
|
|
accessibilityElements.append(bottomRight)
|
|
default: // more than 4
|
|
let moreView = UIView()
|
|
self.moreView = moreView
|
|
moreView.backgroundColor = .secondarySystemBackground
|
|
moreView.translatesAutoresizingMaskIntoConstraints = false
|
|
moreView.isUserInteractionEnabled = true
|
|
moreView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(moreViewTapped)))
|
|
moreView.layer.cornerRadius = 5
|
|
moreView.layer.cornerCurve = .continuous
|
|
moreView.layer.maskedCorners = .layerMaxXMaxYCorner
|
|
moreView.layer.masksToBounds = true
|
|
let moreLabel = UILabel()
|
|
moreLabel.text = "\(attachments.count - 3) more..."
|
|
moreLabel.textColor = .secondaryLabel
|
|
moreLabel.textAlignment = .center
|
|
moreLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
moreView.addSubview(moreLabel)
|
|
moreView.accessibilityLabel = moreLabel.text
|
|
|
|
let topLeft = createAttachmentView(index: 0, hSize: .half, vSize: .half)
|
|
topLeft.layer.cornerRadius = 5
|
|
topLeft.layer.cornerCurve = .continuous
|
|
topLeft.layer.maskedCorners = .layerMinXMinYCorner
|
|
topLeft.layer.masksToBounds = true
|
|
let bottomLeft = createAttachmentView(index: 2, hSize: .half, vSize: .half)
|
|
bottomLeft.layer.cornerRadius = 5
|
|
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
|
|
bottomLeft.layer.masksToBounds = true
|
|
let left = createAttachmentsStack(axis: .vertical, arrangedSubviews: [
|
|
topLeft,
|
|
bottomLeft
|
|
])
|
|
attachmentStacks.add(left)
|
|
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
|
|
topRight.layer.cornerRadius = 5
|
|
topRight.layer.cornerCurve = .continuous
|
|
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
|
topRight.layer.masksToBounds = true
|
|
let right = createAttachmentsStack(axis: .vertical, arrangedSubviews: [
|
|
topRight,
|
|
moreView
|
|
])
|
|
attachmentStacks.add(right)
|
|
let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
|
|
left,
|
|
right,
|
|
])
|
|
attachmentStacks.add(outerStack)
|
|
fillView(outerStack)
|
|
sendSubviewToBack(outerStack)
|
|
NSLayoutConstraint.activate([
|
|
left.halfWidth(),
|
|
topLeft.halfHeight(),
|
|
topRight.halfHeight(),
|
|
moreView.leadingAnchor.constraint(equalTo: moreLabel.leadingAnchor),
|
|
moreLabel.trailingAnchor.constraint(equalTo: moreView.trailingAnchor),
|
|
moreView.topAnchor.constraint(equalTo: moreLabel.topAnchor),
|
|
moreLabel.bottomAnchor.constraint(equalTo: moreView.bottomAnchor),
|
|
])
|
|
accessibilityElements.append(topLeft)
|
|
accessibilityElements.append(topRight)
|
|
accessibilityElements.append(bottomLeft)
|
|
accessibilityElements.append(moreView)
|
|
}
|
|
|
|
self.aspectRatio = aspectRatio
|
|
} else {
|
|
self.isHidden = true
|
|
}
|
|
|
|
// Make sure accessibilityElements is set every time the UI is updated, otherwise it holds
|
|
// on to strong references to the old set of attachment views
|
|
self.accessibilityElements = accessibilityElements
|
|
}
|
|
|
|
private func createAttachmentView(index: Int, hSize: RelativeSize, vSize: RelativeSize) -> AttachmentView {
|
|
let attachmentView = AttachmentView(attachment: attachments[index], index: index)
|
|
attachmentView.delegate = delegate
|
|
attachmentView.translatesAutoresizingMaskIntoConstraints = false
|
|
attachmentView.accessibilityLabel = String(format: NSLocalizedString("Attachment %d", comment: "attachment at index accessiblity label"), index + 1)
|
|
attachmentView.accessibilityLabel = "Attachment \(index + 1)"
|
|
if let desc = attachments[index].description {
|
|
attachmentView.accessibilityLabel! += ", \(desc)"
|
|
}
|
|
attachmentViews.add(attachmentView)
|
|
return attachmentView
|
|
}
|
|
|
|
private func createAttachmentsStack(axis: NSLayoutConstraint.Axis, arrangedSubviews: [UIView]) -> UIStackView {
|
|
let stack = UIStackView(arrangedSubviews: arrangedSubviews)
|
|
stack.axis = axis
|
|
stack.spacing = 4
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
return stack
|
|
}
|
|
|
|
private func createBlurView() {
|
|
let blur = UIBlurEffect(style: .dark)
|
|
let blurView = UIVisualEffectView(effect: blur)
|
|
blurView.alpha = 0
|
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
|
fillView(blurView)
|
|
let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blur, style: .label))
|
|
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
|
fillView(vibrancyView, in: blurView.contentView)
|
|
blurView.contentView.addSubview(vibrancyView)
|
|
|
|
let image = UIImage(systemName: "eye")!
|
|
let imageView = UIImageView(image: image)
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
let label = UILabel()
|
|
label.font = .preferredFont(forTextStyle: .body)
|
|
label.adjustsFontForContentSizeCategory = true
|
|
label.text = "Sensitive Content"
|
|
let stack = UIStackView(arrangedSubviews: [
|
|
imageView,
|
|
label
|
|
])
|
|
stack.axis = .vertical
|
|
stack.alignment = .center
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
vibrancyView.contentView.addSubview(stack)
|
|
NSLayoutConstraint.activate([
|
|
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: image.size.width / image.size.height),
|
|
imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.2),
|
|
stack.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
stack.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
stack.widthAnchor.constraint(equalTo: widthAnchor)
|
|
])
|
|
|
|
self.blurView = blurView
|
|
|
|
blurView.isUserInteractionEnabled = true
|
|
blurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(blurViewTapped)))
|
|
}
|
|
|
|
private func createHideButton() {
|
|
let blurEffect = UIBlurEffect(style: .regular)
|
|
let hideButtonBlurView = UIVisualEffectView(effect: blurEffect)
|
|
hideButtonBlurView.translatesAutoresizingMaskIntoConstraints = false
|
|
hideButtonBlurView.alpha = 1
|
|
hideButtonBlurView.isUserInteractionEnabled = true
|
|
hideButtonBlurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hideButtonTapped)))
|
|
addSubview(hideButtonBlurView)
|
|
self.hideButtonView = hideButtonBlurView
|
|
|
|
let maskLayer = CALayer()
|
|
let image = UIImage(systemName: "eye.slash.fill")!
|
|
maskLayer.contents = image.cgImage!
|
|
maskLayer.frame = CGRect(origin: .zero, size: image.size)
|
|
hideButtonBlurView.layer.mask = maskLayer
|
|
|
|
let hideButtonVibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect, style: .label))
|
|
hideButtonVibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
|
hideButtonBlurView.contentView.addSubview(hideButtonVibrancyView)
|
|
let fillView = UIView()
|
|
fillView.translatesAutoresizingMaskIntoConstraints = false
|
|
fillView.backgroundColor = UIColor(displayP3Red: 0, green: 0, blue: 0, alpha: 0.5)
|
|
hideButtonVibrancyView.contentView.addSubview(fillView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
hideButtonBlurView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
hideButtonBlurView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
|
hideButtonBlurView.widthAnchor.constraint(equalToConstant: image.size.width),
|
|
hideButtonBlurView.heightAnchor.constraint(equalToConstant: image.size.height),
|
|
hideButtonVibrancyView.leadingAnchor.constraint(equalTo: hideButtonBlurView.contentView.leadingAnchor),
|
|
hideButtonVibrancyView.trailingAnchor.constraint(equalTo: hideButtonBlurView.contentView.trailingAnchor),
|
|
hideButtonVibrancyView.topAnchor.constraint(equalTo: hideButtonBlurView.contentView.topAnchor),
|
|
hideButtonVibrancyView.bottomAnchor.constraint(equalTo: hideButtonBlurView.contentView.bottomAnchor),
|
|
fillView.leadingAnchor.constraint(equalTo: hideButtonBlurView.contentView.leadingAnchor),
|
|
fillView.trailingAnchor.constraint(equalTo: hideButtonBlurView.contentView.trailingAnchor),
|
|
fillView.topAnchor.constraint(equalTo: hideButtonBlurView.contentView.topAnchor),
|
|
fillView.bottomAnchor.constraint(equalTo: hideButtonBlurView.contentView.bottomAnchor),
|
|
])
|
|
}
|
|
|
|
private func fillView(_ view: UIView, in parentView: UIView? = nil) {
|
|
let parentView = parentView ?? self
|
|
parentView.addSubview(view)
|
|
NSLayoutConstraint.activate([
|
|
view.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
|
|
view.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
|
|
view.topAnchor.constraint(equalTo: parentView.topAnchor),
|
|
view.bottomAnchor.constraint(equalTo: parentView.bottomAnchor)
|
|
])
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
@objc func blurViewTapped() {
|
|
UIView.animate(withDuration: 0.2) {
|
|
self.contentHidden = false
|
|
}
|
|
}
|
|
|
|
@objc func hideButtonTapped() {
|
|
UIView.animate(withDuration: 0.2) {
|
|
self.contentHidden = true
|
|
}
|
|
}
|
|
|
|
@objc func showSensitiveContent() {
|
|
guard let blurView = blurView else { return }
|
|
|
|
blurView.alpha = 1
|
|
UIView.animate(withDuration: 0.2) {
|
|
blurView.alpha = 0
|
|
}
|
|
}
|
|
|
|
@objc func hideSensitiveContent() {
|
|
guard let blurView = self.blurView else { return }
|
|
|
|
blurView.alpha = 0
|
|
UIView.animate(withDuration: 0.2) {
|
|
blurView.alpha = 1
|
|
}
|
|
}
|
|
|
|
@objc func moreViewTapped() {
|
|
guard attachments.count > 4 else { return }
|
|
// the more view shows up in place of the fourth attachemtn view, show tapping it should start at the fourth attachment
|
|
if let delegate = delegate,
|
|
let gallery = delegate.attachmentViewGallery(startingAt: 3) {
|
|
delegate.attachmentViewPresent(gallery, animated: true)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
fileprivate enum RelativeSize {
|
|
case full, half
|
|
|
|
var multiplier: CGFloat {
|
|
switch self {
|
|
case .full:
|
|
return 1
|
|
case .half:
|
|
return 0.5
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate extension UIView {
|
|
func halfWidth(spacing: CGFloat = 4) -> NSLayoutConstraint {
|
|
return widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.5, constant: -spacing / 2)
|
|
}
|
|
|
|
func halfHeight(spacing: CGFloat = 4) -> NSLayoutConstraint {
|
|
return heightAnchor.constraint(equalTo: superview!.heightAnchor, multiplier: 0.5, constant: -spacing / 2)
|
|
}
|
|
}
|
|
|
|
// A token that represents properties of attachments that the container needs to take into account when deciding whether to update
|
|
fileprivate struct AttachmentToken: Equatable {
|
|
let url: URL
|
|
// to show the alt badge or not
|
|
let hasDescription: Bool
|
|
|
|
init(attachment: Attachment) {
|
|
self.url = attachment.url
|
|
self.hasDescription = attachment.description != nil
|
|
}
|
|
}
|