// // AttachmentsContainerView.swift // Tusker // // Created by Shadowfacts on 6/16/19. // Copyright © 2019 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class AttachmentsContainerView: UIView { static let supportedAttachmentTypes = [Attachment.Kind.image, .video, .audio] weak var delegate: AttachmentViewDelegate? var statusID: String! var attachments: [Attachment]! let attachmentViews: NSHashTable = .weakObjects() 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 func awakeFromNib() { super.awakeFromNib() self.isUserInteractionEnabled = true createBlurView() createHideButton() } func getAttachmentView(for attachment: Attachment) -> AttachmentView? { return attachmentViews.allObjects.first { $0.attachment.id == attachment.id } } // MARK: - User Interaface func updateUI(status: Status) { self.statusID = status.id attachments = status.attachments.filter { AttachmentsContainerView.supportedAttachmentTypes.contains($0.kind) } attachmentViews.allObjects.forEach { $0.removeFromSuperview() } attachmentViews.removeAllObjects() if attachments.count > 0 { self.isHidden = false var accessibilityElements = [Any]() switch attachments.count { case 1: let attachmentView = createAttachmentView(index: 0) fillView(attachmentView) sendSubviewToBack(attachmentView) accessibilityElements.append(attachmentView) case 2: let left = createAttachmentView(index: 0) let right = createAttachmentView(index: 1) let stack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ left, right ]) fillView(stack) sendSubviewToBack(stack) NSLayoutConstraint.activate([ left.halfWidth() ]) accessibilityElements.append(left) accessibilityElements.append(right) case 3: let left = createAttachmentView(index: 0) let topRight = createAttachmentView(index: 1) let bottomRight = createAttachmentView(index: 2) let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ left, createAttachmentsStack(axis: .vertical, arrangedSubviews: [ topRight, bottomRight ]) ]) 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) let bottomLeft = createAttachmentView(index: 2) let left = createAttachmentsStack(axis: .vertical, arrangedSubviews: [ topLeft, bottomLeft ]) let topRight = createAttachmentView(index: 1) let bottomRight = createAttachmentView(index: 3) let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ left, createAttachmentsStack(axis: .vertical, arrangedSubviews: [ topRight, bottomRight ]) ]) 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() moreView.backgroundColor = .secondarySystemBackground moreView.translatesAutoresizingMaskIntoConstraints = false moreView.isUserInteractionEnabled = true moreView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(moreViewTapped))) 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) let bottomLeft = createAttachmentView(index: 2) let left = createAttachmentsStack(axis: .vertical, arrangedSubviews: [ topLeft, bottomLeft ]) let topRight = createAttachmentView(index: 1) let outerStack = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ left, createAttachmentsStack(axis: .vertical, arrangedSubviews: [ topRight, moreView ]) ]) 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.accessibilityElements = accessibilityElements } else { self.isHidden = true } contentHidden = Preferences.shared.blurAllMedia || status.sensitive } private func createAttachmentView(index: Int) -> AttachmentView { let attachmentView = AttachmentView(attachment: attachments[index], index: index) attachmentView.delegate = delegate attachmentView.translatesAutoresizingMaskIntoConstraints = false attachmentView.isAccessibilityElement = true attachmentView.accessibilityTraits = [.image, .button] attachmentView.accessibilityLabel = String(format: NSLocalizedString("Attachment %d", comment: "attachment at index accessiblity label"), index + 1) 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.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 delegate?.showAttachmentsGallery(startingAt: 3) } } fileprivate extension UIView { enum RelativeSize { case full, half var multiplier: CGFloat { switch self { case .full: return 1 case .half: return 0.5 } } } 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) } }