// // StatusMetaIndicatorsView.swift // Tusker // // Created by Shadowfacts on 1/22/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class StatusMetaIndicatorsView: UIView { var allowedIndicators: Indicator = .all var squeezeHorizontal = false // The axis in which the indicators grow var primaryAxis: NSLayoutConstraint.Axis = .vertical // Only used when using single axis mode var secondaryAxisAlignment: Alignment = .leading private var images: [UIImageView] = [] private var isUsingSingleAxis = false private var statusID: String? private var needsSingleAxis: Bool { traitCollection.preferredContentSizeCategory > .extraLarge } override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { NotificationCenter.default.addObserver(self, selector: #selector(configureImageViews), name: UIAccessibility.boldTextStatusDidChangeNotification, object: nil) #if os(visionOS) registerForTraitChanges([UITraitPreferredContentSizeCategory.self]) { (self: Self, previousTraitCollection) in if self.isUsingSingleAxis != self.needsSingleAxis { for image in self.images { self.configureImageView(image) } self.placeImageViews(self.images) } } #endif } #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if isUsingSingleAxis != needsSingleAxis { for image in images { configureImageView(image) } placeImageViews(images) } } #endif @objc private func configureImageViews() { for image in images { configureImageView(image) } } private func configureImageView(_ imageView: UIImageView) { let weight: UIImage.SymbolWeight = UIAccessibility.isBoldTextEnabled ? .regular : traitCollection.preferredContentSizeCategory > .large ? .light : .thin let scale: UIImage.SymbolScale = traitCollection.preferredContentSizeCategory > .extraLarge ? .large : .default imageView.preferredSymbolConfiguration = .init(pointSize: 0, weight: weight, scale: scale) } func updateUI(status: StatusMO) { guard statusID != status.id else { return } statusID = status.id var indicators: Indicator = [] if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil { indicators.insert(.reply) } if allowedIndicators.contains(.visibility) && Preferences.shared.alwaysShowStatusVisibilityIcon { indicators.insert(.visibility) } if allowedIndicators.contains(.localOnly) && status.localOnly { indicators.insert(.localOnly) } setIndicators(indicators, visibility: status.visibility) } // Used by MockStatusView func setIndicators(_ indicators: Indicator, visibility: Visibility) { var images: [UIImage] = [] if indicators.contains(.reply) { images.append(UIImage(systemName: "bubble.left.and.bubble.right")!) } if indicators.contains(.visibility) { images.append(UIImage(systemName: visibility.unfilledImageName)!) } if indicators.contains(.localOnly) { images.append(UIImage(named: "link.broken")!) } let views = images.map { let v = UIImageView(image: $0) v.translatesAutoresizingMaskIntoConstraints = false v.contentMode = .scaleAspectFit v.tintColor = .secondaryLabel configureImageView(v) return v } placeImageViews(views) } private func placeImageViews(_ imageViews: [UIImageView]) { images.forEach { $0.removeFromSuperview() } images = imageViews guard !images.isEmpty else { return } isUsingSingleAxis = needsSingleAxis if needsSingleAxis { for v in images { addSubview(v) switch (primaryAxis, secondaryAxisAlignment) { case (.horizontal, .leading): v.topAnchor.constraint(equalTo: topAnchor).isActive = true case (.horizontal, .trailing): v.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true case (.vertical, .leading): v.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true case (.vertical, .trailing): v.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true case (_, _): fatalError() } } if primaryAxis == .vertical { images.first!.topAnchor.constraint(equalTo: topAnchor).isActive = true images.last!.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true } else { images.first!.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true images.last!.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true } for (a, b) in zip(images, images.dropFirst()) { if primaryAxis == .vertical { b.topAnchor.constraint(equalTo: a.bottomAnchor, constant: 4).isActive = true } else { b.leadingAnchor.constraint(equalTo: a.trailingAnchor, constant: 4).isActive = true } } return } guard primaryAxis == .vertical || imageViews.count <= 2 else { fatalError("StatusMetaIndicatorsView does not support horizontal primary axis with more than 2 views yet") } for (index, v) in images.enumerated() { addSubview(v) if index % 2 == 0 { if index == images.count - 1 { v.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true v.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor).isActive = true } else { v.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true } } else { if squeezeHorizontal { v.leadingAnchor.constraint(equalTo: self.images[index - 1].trailingAnchor, constant: 4).isActive = true } else { v.leadingAnchor.constraint(greaterThanOrEqualTo: self.images[index - 1].trailingAnchor, constant: 4).isActive = true } v.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true } let row = index / 2 if row == 0 { v.topAnchor.constraint(equalTo: topAnchor).isActive = true } else { v.topAnchor.constraint(equalTo: self.images[index - 1].bottomAnchor, constant: 4).isActive = true } v.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor).isActive = true } } struct Indicator: OptionSet { let rawValue: Int static let reply = Indicator(rawValue: 1 << 0) static let visibility = Indicator(rawValue: 1 << 1) static let localOnly = Indicator(rawValue: 1 << 2) static let all: Indicator = [.reply, .visibility, .localOnly] } enum Alignment { case leading, trailing } }