From 158940f8e6a85d20702ba6db8cd658174b6bae50 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 4 Dec 2023 17:06:10 -0500 Subject: [PATCH] Refactor StatusContentContainer to use an array of subviews --- .../StatusEditCollectionViewCell.swift | 49 +++++--- .../StatusEditPollView.swift | 2 +- Tusker/Views/Poll/StatusPollView.swift | 2 +- ...ersationMainStatusCollectionViewCell.swift | 44 +++++-- .../Status/StatusCollectionViewCell.swift | 56 +++++---- .../Views/Status/StatusContentContainer.swift | 114 ++++++++++-------- .../TimelineStatusCollectionViewCell.swift | 37 +++--- 7 files changed, 185 insertions(+), 119 deletions(-) diff --git a/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift b/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift index bbaa1382..7f6cef79 100644 --- a/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift +++ b/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift @@ -61,13 +61,34 @@ class StatusEditCollectionViewCell: UICollectionViewListCell { $0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside) } - private let contentContainer = StatusContentContainer(useTopSpacer: false).configure { - $0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont - $0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont - $0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle + private lazy var contentContainer = StatusContentContainer(arrangedSubviews: [ + contentTextView, + cardView, + attachmentsView, + pollView, + ] as! [any StatusContentView], useTopSpacer: false).configure { $0.setContentHuggingPriority(.defaultLow, for: .vertical) } + private let contentTextView = StatusEditContentTextView().configure { + $0.adjustsFontForContentSizeCategory = true + $0.isScrollEnabled = false + $0.backgroundColor = nil + $0.isEditable = false + $0.isSelectable = false + $0.defaultFont = TimelineStatusCollectionViewCell.contentFont + $0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont + $0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle + } + + private let cardView = StatusCardView().configure { + $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true + } + + private let attachmentsView = AttachmentsContainerView() + + private let pollView = StatusEditPollView() + weak var delegate: StatusEditCollectionViewCellDelegate? private var mastodonController: MastodonController! { delegate?.apiController } @@ -108,7 +129,7 @@ class StatusEditCollectionViewCell: UICollectionViewListCell { } str += "collapsed" } else { - str += AttributedString(contentContainer.contentTextView.attributedText) + str += AttributedString(contentTextView.attributedText) if edit.attachments.count > 0 { let includeDescriptions: Bool @@ -170,13 +191,13 @@ class StatusEditCollectionViewCell: UICollectionViewListCell { timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt) - contentContainer.contentTextView.setTextFrom(edit: edit, index: index) - contentContainer.contentTextView.navigationDelegate = delegate - contentContainer.attachmentsView.delegate = self - contentContainer.attachmentsView.updateUI(attachments: edit.attachments) - contentContainer.pollView.isHidden = edit.poll == nil - contentContainer.pollView.updateUI(poll: edit.poll, emojis: edit.emojis) - contentContainer.cardView.isHidden = true + contentTextView.setTextFrom(edit: edit, index: index) + contentTextView.navigationDelegate = delegate + attachmentsView.delegate = self + attachmentsView.updateUI(attachments: edit.attachments) + pollView.isHidden = edit.poll == nil + pollView.updateUI(poll: edit.poll, emojis: edit.emojis) + cardView.isHidden = true contentWarningLabel.text = edit.spoilerText contentWarningLabel.isHidden = edit.spoilerText.isEmpty @@ -219,9 +240,9 @@ extension StatusEditCollectionViewCell: AttachmentViewDelegate { guard let delegate else { return nil } - let attachments = contentContainer.attachmentsView.attachments! + let attachments = attachmentsView.attachments! let sourceViews = attachments.map { - contentContainer.attachmentsView.getAttachmentView(for: $0) + attachmentsView.getAttachmentView(for: $0) } let gallery = delegate.gallery(attachments: attachments, sourceViews: sourceViews, startIndex: index) return gallery diff --git a/Tusker/Screens/Status Edit History/StatusEditPollView.swift b/Tusker/Screens/Status Edit History/StatusEditPollView.swift index 67ac9762..50767e57 100644 --- a/Tusker/Screens/Status Edit History/StatusEditPollView.swift +++ b/Tusker/Screens/Status Edit History/StatusEditPollView.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -class StatusEditPollView: UIStackView, StatusContentPollView { +class StatusEditPollView: UIStackView, StatusContentView { private var titleLabels: [EmojiLabel] = [] diff --git a/Tusker/Views/Poll/StatusPollView.swift b/Tusker/Views/Poll/StatusPollView.swift index 73d98c08..57e8ee28 100644 --- a/Tusker/Views/Poll/StatusPollView.swift +++ b/Tusker/Views/Poll/StatusPollView.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -class StatusPollView: UIView, StatusContentPollView { +class StatusPollView: UIView, StatusContentView { private static let formatter: DateComponentsFormatter = { let f = DateComponentsFormatter() diff --git a/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift b/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift index ea0fcbb7..06d113cb 100644 --- a/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift @@ -117,18 +117,38 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status $0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside) } - let contentContainer = StatusContentContainer(useTopSpacer: true).configure { - $0.contentTextView.defaultFont = ConversationMainStatusCollectionViewCell.contentFont - $0.contentTextView.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont - $0.contentTextView.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle - $0.contentTextView.isSelectable = true - $0.contentTextView.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber] - if #available(iOS 16.0, *) { - $0.contentTextView.dataDetectorTypes.formUnion([.money, .physicalValue]) - } + private(set) lazy var contentContainer = StatusContentContainer(arrangedSubviews: [ + contentTextView, + cardView, + attachmentsView, + pollView, + ] as! [any StatusContentView], useTopSpacer: true).configure { $0.setContentHuggingPriority(.defaultLow, for: .vertical) } + let contentTextView = StatusContentTextView().configure { + $0.adjustsFontForContentSizeCategory = true + $0.isScrollEnabled = false + $0.backgroundColor = nil + $0.isEditable = false + $0.isSelectable = true + $0.defaultFont = ConversationMainStatusCollectionViewCell.contentFont + $0.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont + $0.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle + $0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber] + if #available(iOS 16.0, *) { + $0.dataDetectorTypes.formUnion([.money, .physicalValue]) + } + } + + let cardView = StatusCardView().configure { + $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true + } + + let attachmentsView = AttachmentsContainerView() + + let pollView = StatusPollView() + private lazy var favoritesCountButton = UIButton().configure { $0.titleLabel!.adjustsFontForContentSizeCategory = true $0.addTarget(self, action: #selector(favoritesCountPressed), for: .touchUpInside) @@ -318,9 +338,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status accountDetailAccessibilityElement, contentWarningLabel, collapseButton, - contentContainer.contentTextView, - contentContainer.attachmentsView, - contentContainer.pollView, + contentTextView, + attachmentsView, + pollView, favoritesCountButton, reblogsCountButton, timestampAndClientLabel, diff --git a/Tusker/Views/Status/StatusCollectionViewCell.swift b/Tusker/Views/Status/StatusCollectionViewCell.swift index 9fbe1b08..b82ad832 100644 --- a/Tusker/Views/Status/StatusCollectionViewCell.swift +++ b/Tusker/Views/Status/StatusCollectionViewCell.swift @@ -24,7 +24,11 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate var usernameLabel: UILabel { get } var contentWarningLabel: EmojiLabel { get } var collapseButton: StatusCollapseButton { get } - var contentContainer: StatusContentContainer { get } + var contentContainer: StatusContentContainer { get } + var contentTextView: StatusContentTextView { get } + var pollView: StatusPollView { get } + var cardView: StatusCardView { get } + var attachmentsView: AttachmentsContainerView { get } var replyButton: UIButton { get } var favoriteButton: ToggleableButton { get } var reblogButton: ToggleableButton { get } @@ -90,20 +94,20 @@ extension StatusCollectionViewCell { updateAccountUI(account: status.account) - contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent) - contentContainer.contentTextView.navigationDelegate = delegate + contentTextView.setTextFrom(status: status, precomputed: precomputedContent) + contentTextView.navigationDelegate = delegate self.updateAttachmentsUI(status: status) - contentContainer.pollView.isHidden = status.poll == nil - contentContainer.pollView.mastodonController = mastodonController - contentContainer.pollView.delegate = delegate - contentContainer.pollView.updateUI(status: status, poll: status.poll) + pollView.isHidden = status.poll == nil + pollView.mastodonController = mastodonController + pollView.delegate = delegate + pollView.updateUI(status: status, poll: status.poll) if Preferences.shared.showLinkPreviews { - contentContainer.cardView.updateUI(status: status) - contentContainer.cardView.isHidden = status.card == nil - contentContainer.cardView.navigationDelegate = delegate - contentContainer.cardView.actionProvider = delegate + cardView.updateUI(status: status) + cardView.isHidden = status.card == nil + cardView.navigationDelegate = delegate + cardView.actionProvider = delegate } else { - contentContainer.cardView.isHidden = true + cardView.isHidden = true } updateUIForPreferences(status: status) @@ -168,8 +172,8 @@ extension StatusCollectionViewCell { } func updateAttachmentsUI(status: StatusMO) { - contentContainer.attachmentsView.delegate = self - contentContainer.attachmentsView.updateUI(attachments: status.attachments) + attachmentsView.delegate = self + attachmentsView.updateUI(attachments: status.attachments) } func updateAccountUI(account: AccountMO) { @@ -182,20 +186,20 @@ extension StatusCollectionViewCell { avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize let newCardHidden = !Preferences.shared.showLinkPreviews || status.card == nil - if contentContainer.cardView.isHidden != newCardHidden { + if cardView.isHidden != newCardHidden { delegate?.statusCellNeedsReconfigure(self, animated: false, completion: nil) } switch Preferences.shared.attachmentBlurMode { case .never: - contentContainer.attachmentsView.contentHidden = false + attachmentsView.contentHidden = false case .always: - contentContainer.attachmentsView.contentHidden = true + attachmentsView.contentHidden = true default: if status.sensitive { - contentContainer.attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning + attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning } else { - contentContainer.attachmentsView.contentHidden = false + attachmentsView.contentHidden = false } } @@ -211,8 +215,8 @@ extension StatusCollectionViewCell { // only called when isGrayscale does not match the pref func updateGrayscaleableUI(status: StatusMO) { isGrayscale = Preferences.shared.grayscaleImages - if contentContainer.contentTextView.hasEmojis { - contentContainer.contentTextView.setEmojis(status.emojis, identifier: status.id) + if contentTextView.hasEmojis { + contentTextView.setEmojis(status.emojis, identifier: status.id) } displayNameLabel.updateForAccountDisplayName(account: status.account) } @@ -235,10 +239,10 @@ extension StatusCollectionViewCell { // do not include reply action here, because the cell already contains a button for it moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? []) - contentContainer.pollView.isHidden = status.poll == nil - contentContainer.pollView.mastodonController = mastodonController - contentContainer.pollView.delegate = delegate - contentContainer.pollView.updateUI(status: status, poll: status.poll) + pollView.isHidden = status.poll == nil + pollView.mastodonController = mastodonController + pollView.delegate = delegate + pollView.updateUI(status: status, poll: status.poll) } func setShowThreadLinks(prev: Bool, next: Bool) { @@ -327,7 +331,7 @@ extension StatusCollectionViewCell { func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? { guard let delegate = delegate, let status = mastodonController.persistentContainer.status(for: statusID) else { return nil } - let sourceViews = status.attachments.map(contentContainer.attachmentsView.getAttachmentView(for:)) + let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index) // TODO: PiP // gallery.avPlayerViewControllerDelegate = self diff --git a/Tusker/Views/Status/StatusContentContainer.swift b/Tusker/Views/Status/StatusContentContainer.swift index 658854b4..f8f7393d 100644 --- a/Tusker/Views/Status/StatusContentContainer.swift +++ b/Tusker/Views/Status/StatusContentContainer.swift @@ -8,45 +8,11 @@ import UIKit -protocol StatusContentPollView: UIView { - func estimateHeight(effectiveWidth: CGFloat) -> CGFloat -} - -class StatusContentContainer: UIView { +class StatusContentContainer: UIView { + // TODO: this is a weird place for this + static var cardViewHeight: CGFloat { 90 } - private let useTopSpacer: Bool - private lazy var topSpacer = UIView().configure { - $0.backgroundColor = .clear - // other 4pt is provided by this view's own spacing - $0.heightAnchor.constraint(equalToConstant: 4).isActive = true - } - - let contentTextView = ContentView().configure { - $0.adjustsFontForContentSizeCategory = true - $0.isScrollEnabled = false - $0.backgroundColor = nil - $0.isEditable = false - $0.isSelectable = false - } - - private static var cardViewHeight: CGFloat { 90 } - let cardView = StatusCardView().configure { - NSLayoutConstraint.activate([ - $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight), - ]) - } - - let attachmentsView = AttachmentsContainerView() - - let pollView = PollView() - - private var arrangedSubviews: [UIView] { - if useTopSpacer { - return [topSpacer, contentTextView, cardView, attachmentsView, pollView] - } else { - return [contentTextView, cardView, attachmentsView, pollView] - } - } + private let arrangedSubviews: [any StatusContentView] private var isHiddenObservations: [NSKeyValueObservation] = [] @@ -61,8 +27,12 @@ class StatusContentContainer CGFloat { var height: CGFloat = 0 - height += contentTextView.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height - if !cardView.isHidden { - height += StatusContentContainer.cardViewHeight - } - if !attachmentsView.isHidden { - height += effectiveWidth / attachmentsView.aspectRatio - } - if !pollView.isHidden { - let pollHeight = pollView.estimateHeight(effectiveWidth: effectiveWidth) - height += pollHeight + for view in arrangedSubviews where !view.isHidden { + height += view.estimateHeight(effectiveWidth: effectiveWidth) } return height } } + +extension StatusContentContainer { + private class TopSpacerView: UIView, StatusContentView { + init() { + super.init(frame: .zero) + + backgroundColor = .clear + // other 4pt is provided by this view's own spacing + heightAnchor.constraint(equalToConstant: 4).isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { + 4 + } + } +} + +private extension UIView { + func observeIsHidden(_ f: @escaping () -> Void) -> NSKeyValueObservation { + self.observe(\.isHidden) { _, _ in + f() + } + } +} + +protocol StatusContentView: UIView { + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat +} + +extension ContentTextView: StatusContentView { + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { + sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height + } +} + +extension StatusCardView: StatusContentView { + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { + StatusContentContainer.cardViewHeight + } +} + +extension AttachmentsContainerView: StatusContentView { + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { + effectiveWidth / aspectRatio + } +} diff --git a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift index c0ec2c6e..4a24f005 100644 --- a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift @@ -186,25 +186,34 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti $0.setContentCompressionResistancePriority(.required, for: .vertical) } - let contentContainer = StatusContentContainer(useTopSpacer: false).configure { - $0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont - $0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont - $0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle + private(set) lazy var contentContainer = StatusContentContainer(arrangedSubviews: [ + contentTextView, + cardView, + attachmentsView, + pollView, + ] as! [any StatusContentView], useTopSpacer: false).configure { $0.setContentHuggingPriority(.defaultLow, for: .vertical) } - private var contentTextView: StatusContentTextView { - contentContainer.contentTextView + + let contentTextView = StatusContentTextView().configure { + $0.adjustsFontForContentSizeCategory = true + $0.isScrollEnabled = false + $0.backgroundColor = nil + $0.isEditable = false + $0.isSelectable = false + $0.defaultFont = TimelineStatusCollectionViewCell.contentFont + $0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont + $0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle } - private var cardView: StatusCardView { - contentContainer.cardView - } - private var attachmentsView: AttachmentsContainerView { - contentContainer.attachmentsView - } - private var pollView: StatusPollView { - contentContainer.pollView + + let cardView = StatusCardView().configure { + $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true } + let attachmentsView = AttachmentsContainerView() + + let pollView = StatusPollView() + private var placeholderReplyButtonLeadingConstraint: NSLayoutConstraint! private lazy var actionsContainer = UIView().configure { replyButton.translatesAutoresizingMaskIntoConstraints = false