From 8c27a9368f0c179b96e403f0d85de8e2dddc5beb Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 13 May 2023 15:00:03 -0400 Subject: [PATCH] Estimate height when resolving status collapse state --- Tusker/Extensions/UIView+Configure.swift | 1 + .../StatusEditCollectionViewCell.swift | 4 +-- .../StatusEditPollView.swift | 16 ++++++++++- .../AttachmentsContainerView.swift | 25 +++++++++-------- Tusker/Views/Poll/PollOptionView.swift | 9 +++---- Tusker/Views/Poll/PollOptionsView.swift | 10 +++++++ Tusker/Views/Poll/StatusPollView.swift | 7 ++++- ...ersationMainStatusCollectionViewCell.swift | 5 ++++ .../Status/StatusCollectionViewCell.swift | 13 +++++---- .../Views/Status/StatusContentContainer.swift | 27 +++++++++++++++++-- .../TimelineStatusCollectionViewCell.swift | 5 ++++ 11 files changed, 93 insertions(+), 29 deletions(-) diff --git a/Tusker/Extensions/UIView+Configure.swift b/Tusker/Extensions/UIView+Configure.swift index 98253851..6c339d60 100644 --- a/Tusker/Extensions/UIView+Configure.swift +++ b/Tusker/Extensions/UIView+Configure.swift @@ -13,6 +13,7 @@ protocol Configurable { func configure(_ closure: (T) -> Void) -> T } extension Configurable where Self: UIView { + @inline(__always) func configure(_ closure: (Self) -> Void) -> Self { closure(self) return self diff --git a/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift b/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift index f0077cb7..536928d1 100644 --- a/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift +++ b/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift @@ -117,8 +117,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell { } _ = state.resolveFor(status: edit, height: { - layoutIfNeeded() - return contentContainer.visibleSubviewHeight + let width = self.bounds.width - 2*16 + return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width) }) collapseButton.isHidden = !state.collapsible! contentContainer.setCollapsed(state.collapsed!) diff --git a/Tusker/Screens/Status Edit History/StatusEditPollView.swift b/Tusker/Screens/Status Edit History/StatusEditPollView.swift index 0cc33ab6..3a8f8cf0 100644 --- a/Tusker/Screens/Status Edit History/StatusEditPollView.swift +++ b/Tusker/Screens/Status Edit History/StatusEditPollView.swift @@ -9,7 +9,9 @@ import UIKit import Pachyderm -class StatusEditPollView: UIStackView { +class StatusEditPollView: UIStackView, StatusContentPollView { + + private var titleLabels: [EmojiLabel] = [] init() { super.init(frame: .zero) @@ -25,6 +27,7 @@ class StatusEditPollView: UIStackView { func updateUI(poll: StatusEdit.Poll?, emojis: [Emoji]) { arrangedSubviews.forEach { $0.removeFromSuperview() } + titleLabels = [] for option in poll?.options ?? [] { // the edit poll doesn't actually include the multiple value @@ -33,6 +36,7 @@ class StatusEditPollView: UIStackView { let label = EmojiLabel() label.text = option.title label.setEmojis(emojis, identifier: Optional.none) + titleLabels.append(label) let stack = UIStackView(arrangedSubviews: [ icon, label, @@ -43,5 +47,15 @@ class StatusEditPollView: UIStackView { addArrangedSubview(stack) } } + + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { + var height: CGFloat = 0 + height += CGFloat(arrangedSubviews.count - 1) * 4 + let labelWidth = effectiveWidth /* checkbox size: */ - 20 /* spacing: */ - 8 + for titleLabel in titleLabels { + height += titleLabel.sizeThatFits(CGSize(width: labelWidth, height: UIView.layoutFittingCompressedSize.height)).height + } + return height + } } diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index 303ff31d..77d72d66 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -22,6 +22,15 @@ class AttachmentsContainerView: UIView { let attachmentStacks: NSHashTable = .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? @@ -93,7 +102,8 @@ class AttachmentsContainerView: UIView { fillView(attachmentView) sendSubviewToBack(attachmentView) accessibilityElements.append(attachmentView) - if let attachmentAspectRatio = attachmentView.attachmentAspectRatio { + if Preferences.shared.showUncroppedMediaInline, + let attachmentAspectRatio = attachmentView.attachmentAspectRatio { aspectRatio = attachmentAspectRatio } case 2: @@ -266,18 +276,7 @@ class AttachmentsContainerView: UIView { accessibilityElements.append(moreView) } - if Preferences.shared.showUncroppedMediaInline { - if aspectRatioConstraint?.multiplier != aspectRatio { - aspectRatioConstraint?.isActive = false - aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio) - aspectRatioConstraint!.isActive = true - } - } else { - if aspectRatioConstraint == nil { - aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: 16/9) - aspectRatioConstraint!.isActive = true - } - } + self.aspectRatio = aspectRatio } else { self.isHidden = true } diff --git a/Tusker/Views/Poll/PollOptionView.swift b/Tusker/Views/Poll/PollOptionView.swift index 26bff02d..56e1c60c 100644 --- a/Tusker/Views/Poll/PollOptionView.swift +++ b/Tusker/Views/Poll/PollOptionView.swift @@ -11,18 +11,18 @@ import Pachyderm class PollOptionView: UIView { - private let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25) + private static let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25) + private(set) var label: EmojiLabel! private(set) var checkbox: PollOptionCheckboxView? init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) { super.init(frame: .zero) - let minHeight: CGFloat = 35 layer.cornerRadius = 0.1 * minHeight layer.cornerCurve = .continuous - backgroundColor = unselectedBackgroundColor + backgroundColor = PollOptionView.unselectedBackgroundColor let showCheckbox = poll.ownVotes?.isEmpty == false || !poll.effectiveExpired if showCheckbox { @@ -31,7 +31,7 @@ class PollOptionView: UIView { addSubview(checkbox!) } - let label = EmojiLabel() + label = EmojiLabel() label.translatesAutoresizingMaskIntoConstraints = false label.numberOfLines = 0 label.font = .preferredFont(forTextStyle: .callout) @@ -91,7 +91,6 @@ class PollOptionView: UIView { NSLayoutConstraint.activate([ minHeightConstraint, - label.topAnchor.constraint(equalTo: topAnchor, constant: 4), label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4), label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor, constant: -4), diff --git a/Tusker/Views/Poll/PollOptionsView.swift b/Tusker/Views/Poll/PollOptionsView.swift index 811e7bd6..cfba545e 100644 --- a/Tusker/Views/Poll/PollOptionsView.swift +++ b/Tusker/Views/Poll/PollOptionsView.swift @@ -77,6 +77,16 @@ class PollOptionsView: UIControl { accessibilityElements = options } + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { + var height: CGFloat = 0 + height += CGFloat(options.count - 1) * stack.spacing + for option in options { + // this isn't the actual width, but it's close enough for the estimate + height += option.label.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height + } + return height + } + private func selectOption(_ option: PollOptionView) { if poll.multiple { option.checkbox?.isChecked.toggle() diff --git a/Tusker/Views/Poll/StatusPollView.swift b/Tusker/Views/Poll/StatusPollView.swift index 7798a49d..d1fcabb0 100644 --- a/Tusker/Views/Poll/StatusPollView.swift +++ b/Tusker/Views/Poll/StatusPollView.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -class StatusPollView: UIView { +class StatusPollView: UIView, StatusContentPollView { private static let formatter: DateComponentsFormatter = { let f = DateComponentsFormatter() @@ -140,6 +140,11 @@ class StatusPollView: UIView { voteButton.isEnabled = false } + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { + guard let poll else { return 0 } + return optionsView.estimateHeight(effectiveWidth: effectiveWidth) + infoLabel.sizeThatFits(UIView.layoutFittingExpandedSize).height + } + private func checkedOptionsChanged() { voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0 } diff --git a/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift b/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift index f996370b..d9a2e9ab 100644 --- a/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift @@ -398,6 +398,11 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status reblogsCountButton.configuration = reblogsConfig } + func estimateContentHeight() -> CGFloat { + let width = bounds.width - 2*16 + return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width) + } + func updateUIForPreferences(status: StatusMO) { baseUpdateUIForPreferences(status: status) } diff --git a/Tusker/Views/Status/StatusCollectionViewCell.swift b/Tusker/Views/Status/StatusCollectionViewCell.swift index 613a3ebf..3fa62ada 100644 --- a/Tusker/Views/Status/StatusCollectionViewCell.swift +++ b/Tusker/Views/Status/StatusCollectionViewCell.swift @@ -46,6 +46,7 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate func updateUIForPreferences(status: StatusMO) func updateStatusState(status: StatusMO) + func estimateContentHeight() -> CGFloat } // MARK: UI Configuration @@ -117,11 +118,13 @@ extension StatusCollectionViewCell { replyButton.isEnabled = mastodonController.loggedIn favoriteButton.isEnabled = mastodonController.loggedIn - let didResolve = statusState.resolveFor(status: status) { - // layout so that we can take the content height into consideration when deciding whether to collapse - layoutIfNeeded() - return contentContainer.visibleSubviewHeight - } + let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight) +// let didResolve = statusState.resolveFor(status: status) { +//// // layout so that we can take the content height into consideration when deciding whether to collapse +//// layoutIfNeeded() +//// return contentContainer.visibleSubviewHeight +// return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: ) +// } if didResolve { if statusState.collapsible! && showStatusAutomatically { statusState.collapsed = false diff --git a/Tusker/Views/Status/StatusContentContainer.swift b/Tusker/Views/Status/StatusContentContainer.swift index c80e30f8..06dcf917 100644 --- a/Tusker/Views/Status/StatusContentContainer.swift +++ b/Tusker/Views/Status/StatusContentContainer.swift @@ -8,7 +8,11 @@ import UIKit -class StatusContentContainer: UIView { +protocol StatusContentPollView: UIView { + func estimateHeight(effectiveWidth: CGFloat) -> CGFloat +} + +class StatusContentContainer: UIView { private var useTopSpacer = false private let topSpacer = UIView().configure { @@ -25,9 +29,10 @@ class StatusContentContainer: UI $0.isSelectable = false } + private static var cardViewHeight: CGFloat { 90 } let cardView = StatusCardView().configure { NSLayoutConstraint.activate([ - $0.heightAnchor.constraint(equalToConstant: 90), + $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight), ]) } @@ -132,4 +137,22 @@ class StatusContentContainer: UI zeroHeightConstraint.isActive = collapsed } + // used only for collapsing automatically based on height, doesn't need to be accurate + // just roughly inline with the content height + func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> 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 + } + return height + } + } diff --git a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift index 8852f668..085ebfdc 100644 --- a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift @@ -629,6 +629,11 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti baseUpdateStatusState(status: status) } + func estimateContentHeight() -> CGFloat { + let width = bounds.width /* leading spacing: */ - 16 /* avatar: */ - 50 /* spacing: 8 */ - 8 /* trailing spacing: */ - 16 + return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width) + } + private func updateTimestamp() { guard let mastodonController, let status = mastodonController.persistentContainer.status(for: statusID) else {