Refactor StatusContentContainer to use an array of subviews

This commit is contained in:
Shadowfacts 2023-12-04 17:06:10 -05:00
parent 141e8b96a5
commit 158940f8e6
7 changed files with 185 additions and 119 deletions

View File

@ -61,13 +61,34 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside) $0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
} }
private let contentContainer = StatusContentContainer<StatusEditContentTextView, StatusEditPollView>(useTopSpacer: false).configure { private lazy var contentContainer = StatusContentContainer(arrangedSubviews: [
$0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont contentTextView,
$0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont cardView,
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle attachmentsView,
pollView,
] as! [any StatusContentView], useTopSpacer: false).configure {
$0.setContentHuggingPriority(.defaultLow, for: .vertical) $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? weak var delegate: StatusEditCollectionViewCellDelegate?
private var mastodonController: MastodonController! { delegate?.apiController } private var mastodonController: MastodonController! { delegate?.apiController }
@ -108,7 +129,7 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
} }
str += "collapsed" str += "collapsed"
} else { } else {
str += AttributedString(contentContainer.contentTextView.attributedText) str += AttributedString(contentTextView.attributedText)
if edit.attachments.count > 0 { if edit.attachments.count > 0 {
let includeDescriptions: Bool let includeDescriptions: Bool
@ -170,13 +191,13 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt) timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt)
contentContainer.contentTextView.setTextFrom(edit: edit, index: index) contentTextView.setTextFrom(edit: edit, index: index)
contentContainer.contentTextView.navigationDelegate = delegate contentTextView.navigationDelegate = delegate
contentContainer.attachmentsView.delegate = self attachmentsView.delegate = self
contentContainer.attachmentsView.updateUI(attachments: edit.attachments) attachmentsView.updateUI(attachments: edit.attachments)
contentContainer.pollView.isHidden = edit.poll == nil pollView.isHidden = edit.poll == nil
contentContainer.pollView.updateUI(poll: edit.poll, emojis: edit.emojis) pollView.updateUI(poll: edit.poll, emojis: edit.emojis)
contentContainer.cardView.isHidden = true cardView.isHidden = true
contentWarningLabel.text = edit.spoilerText contentWarningLabel.text = edit.spoilerText
contentWarningLabel.isHidden = edit.spoilerText.isEmpty contentWarningLabel.isHidden = edit.spoilerText.isEmpty
@ -219,9 +240,9 @@ extension StatusEditCollectionViewCell: AttachmentViewDelegate {
guard let delegate else { guard let delegate else {
return nil return nil
} }
let attachments = contentContainer.attachmentsView.attachments! let attachments = attachmentsView.attachments!
let sourceViews = attachments.map { let sourceViews = attachments.map {
contentContainer.attachmentsView.getAttachmentView(for: $0) attachmentsView.getAttachmentView(for: $0)
} }
let gallery = delegate.gallery(attachments: attachments, sourceViews: sourceViews, startIndex: index) let gallery = delegate.gallery(attachments: attachments, sourceViews: sourceViews, startIndex: index)
return gallery return gallery

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class StatusEditPollView: UIStackView, StatusContentPollView { class StatusEditPollView: UIStackView, StatusContentView {
private var titleLabels: [EmojiLabel] = [] private var titleLabels: [EmojiLabel] = []

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
class StatusPollView: UIView, StatusContentPollView { class StatusPollView: UIView, StatusContentView {
private static let formatter: DateComponentsFormatter = { private static let formatter: DateComponentsFormatter = {
let f = DateComponentsFormatter() let f = DateComponentsFormatter()

View File

@ -117,18 +117,38 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside) $0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
} }
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: true).configure { private(set) lazy var contentContainer = StatusContentContainer(arrangedSubviews: [
$0.contentTextView.defaultFont = ConversationMainStatusCollectionViewCell.contentFont contentTextView,
$0.contentTextView.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont cardView,
$0.contentTextView.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle attachmentsView,
$0.contentTextView.isSelectable = true pollView,
$0.contentTextView.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber] ] as! [any StatusContentView], useTopSpacer: true).configure {
if #available(iOS 16.0, *) {
$0.contentTextView.dataDetectorTypes.formUnion([.money, .physicalValue])
}
$0.setContentHuggingPriority(.defaultLow, for: .vertical) $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 { private lazy var favoritesCountButton = UIButton().configure {
$0.titleLabel!.adjustsFontForContentSizeCategory = true $0.titleLabel!.adjustsFontForContentSizeCategory = true
$0.addTarget(self, action: #selector(favoritesCountPressed), for: .touchUpInside) $0.addTarget(self, action: #selector(favoritesCountPressed), for: .touchUpInside)
@ -318,9 +338,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
accountDetailAccessibilityElement, accountDetailAccessibilityElement,
contentWarningLabel, contentWarningLabel,
collapseButton, collapseButton,
contentContainer.contentTextView, contentTextView,
contentContainer.attachmentsView, attachmentsView,
contentContainer.pollView, pollView,
favoritesCountButton, favoritesCountButton,
reblogsCountButton, reblogsCountButton,
timestampAndClientLabel, timestampAndClientLabel,

View File

@ -24,7 +24,11 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
var usernameLabel: UILabel { get } var usernameLabel: UILabel { get }
var contentWarningLabel: EmojiLabel { get } var contentWarningLabel: EmojiLabel { get }
var collapseButton: StatusCollapseButton { get } var collapseButton: StatusCollapseButton { get }
var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { 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 replyButton: UIButton { get }
var favoriteButton: ToggleableButton { get } var favoriteButton: ToggleableButton { get }
var reblogButton: ToggleableButton { get } var reblogButton: ToggleableButton { get }
@ -90,20 +94,20 @@ extension StatusCollectionViewCell {
updateAccountUI(account: status.account) updateAccountUI(account: status.account)
contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent) contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
contentContainer.contentTextView.navigationDelegate = delegate contentTextView.navigationDelegate = delegate
self.updateAttachmentsUI(status: status) self.updateAttachmentsUI(status: status)
contentContainer.pollView.isHidden = status.poll == nil pollView.isHidden = status.poll == nil
contentContainer.pollView.mastodonController = mastodonController pollView.mastodonController = mastodonController
contentContainer.pollView.delegate = delegate pollView.delegate = delegate
contentContainer.pollView.updateUI(status: status, poll: status.poll) pollView.updateUI(status: status, poll: status.poll)
if Preferences.shared.showLinkPreviews { if Preferences.shared.showLinkPreviews {
contentContainer.cardView.updateUI(status: status) cardView.updateUI(status: status)
contentContainer.cardView.isHidden = status.card == nil cardView.isHidden = status.card == nil
contentContainer.cardView.navigationDelegate = delegate cardView.navigationDelegate = delegate
contentContainer.cardView.actionProvider = delegate cardView.actionProvider = delegate
} else { } else {
contentContainer.cardView.isHidden = true cardView.isHidden = true
} }
updateUIForPreferences(status: status) updateUIForPreferences(status: status)
@ -168,8 +172,8 @@ extension StatusCollectionViewCell {
} }
func updateAttachmentsUI(status: StatusMO) { func updateAttachmentsUI(status: StatusMO) {
contentContainer.attachmentsView.delegate = self attachmentsView.delegate = self
contentContainer.attachmentsView.updateUI(attachments: status.attachments) attachmentsView.updateUI(attachments: status.attachments)
} }
func updateAccountUI(account: AccountMO) { func updateAccountUI(account: AccountMO) {
@ -182,20 +186,20 @@ extension StatusCollectionViewCell {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize
let newCardHidden = !Preferences.shared.showLinkPreviews || status.card == nil let newCardHidden = !Preferences.shared.showLinkPreviews || status.card == nil
if contentContainer.cardView.isHidden != newCardHidden { if cardView.isHidden != newCardHidden {
delegate?.statusCellNeedsReconfigure(self, animated: false, completion: nil) delegate?.statusCellNeedsReconfigure(self, animated: false, completion: nil)
} }
switch Preferences.shared.attachmentBlurMode { switch Preferences.shared.attachmentBlurMode {
case .never: case .never:
contentContainer.attachmentsView.contentHidden = false attachmentsView.contentHidden = false
case .always: case .always:
contentContainer.attachmentsView.contentHidden = true attachmentsView.contentHidden = true
default: default:
if status.sensitive { if status.sensitive {
contentContainer.attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning
} else { } else {
contentContainer.attachmentsView.contentHidden = false attachmentsView.contentHidden = false
} }
} }
@ -211,8 +215,8 @@ extension StatusCollectionViewCell {
// only called when isGrayscale does not match the pref // only called when isGrayscale does not match the pref
func updateGrayscaleableUI(status: StatusMO) { func updateGrayscaleableUI(status: StatusMO) {
isGrayscale = Preferences.shared.grayscaleImages isGrayscale = Preferences.shared.grayscaleImages
if contentContainer.contentTextView.hasEmojis { if contentTextView.hasEmojis {
contentContainer.contentTextView.setEmojis(status.emojis, identifier: status.id) contentTextView.setEmojis(status.emojis, identifier: status.id)
} }
displayNameLabel.updateForAccountDisplayName(account: status.account) 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 // 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) ?? []) moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? [])
contentContainer.pollView.isHidden = status.poll == nil pollView.isHidden = status.poll == nil
contentContainer.pollView.mastodonController = mastodonController pollView.mastodonController = mastodonController
contentContainer.pollView.delegate = delegate pollView.delegate = delegate
contentContainer.pollView.updateUI(status: status, poll: status.poll) pollView.updateUI(status: status, poll: status.poll)
} }
func setShowThreadLinks(prev: Bool, next: Bool) { func setShowThreadLinks(prev: Bool, next: Bool) {
@ -327,7 +331,7 @@ extension StatusCollectionViewCell {
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? { func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
guard let delegate = delegate, guard let delegate = delegate,
let status = mastodonController.persistentContainer.status(for: statusID) else { return nil } 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) let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
// TODO: PiP // TODO: PiP
// gallery.avPlayerViewControllerDelegate = self // gallery.avPlayerViewControllerDelegate = self

View File

@ -8,45 +8,11 @@
import UIKit import UIKit
protocol StatusContentPollView: UIView { class StatusContentContainer: UIView {
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat // TODO: this is a weird place for this
} static var cardViewHeight: CGFloat { 90 }
class StatusContentContainer<ContentView: ContentTextView, PollView: StatusContentPollView>: UIView {
private let useTopSpacer: Bool private let arrangedSubviews: [any StatusContentView]
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 var isHiddenObservations: [NSKeyValueObservation] = [] private var isHiddenObservations: [NSKeyValueObservation] = []
@ -61,8 +27,12 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
subviews.filter { !$0.isHidden }.map(\.bounds.height).reduce(0, +) subviews.filter { !$0.isHidden }.map(\.bounds.height).reduce(0, +)
} }
init(useTopSpacer: Bool) { init(arrangedSubviews: [any StatusContentView], useTopSpacer: Bool) {
self.useTopSpacer = useTopSpacer var arrangedSubviews = arrangedSubviews
if useTopSpacer {
arrangedSubviews.insert(TopSpacerView(), at: 0)
}
self.arrangedSubviews = arrangedSubviews
super.init(frame: .zero) super.init(frame: .zero)
@ -83,7 +53,7 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
setNeedsUpdateConstraints() setNeedsUpdateConstraints()
isHiddenObservations = arrangedSubviews.map { isHiddenObservations = arrangedSubviews.map {
$0.observe(\.isHidden) { [unowned self] _, _ in $0.observeIsHidden { [unowned self] in
self.setNeedsUpdateConstraints() self.setNeedsUpdateConstraints()
} }
} }
@ -147,18 +117,60 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
// just roughly inline with the content height // just roughly inline with the content height
func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> CGFloat { func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> CGFloat {
var height: CGFloat = 0 var height: CGFloat = 0
height += contentTextView.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height for view in arrangedSubviews where !view.isHidden {
if !cardView.isHidden { height += view.estimateHeight(effectiveWidth: effectiveWidth)
height += StatusContentContainer.cardViewHeight
}
if !attachmentsView.isHidden {
height += effectiveWidth / attachmentsView.aspectRatio
}
if !pollView.isHidden {
let pollHeight = pollView.estimateHeight(effectiveWidth: effectiveWidth)
height += pollHeight
} }
return height 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
}
}

View File

@ -186,25 +186,34 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
$0.setContentCompressionResistancePriority(.required, for: .vertical) $0.setContentCompressionResistancePriority(.required, for: .vertical)
} }
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: false).configure { private(set) lazy var contentContainer = StatusContentContainer(arrangedSubviews: [
$0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont contentTextView,
$0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont cardView,
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle attachmentsView,
pollView,
] as! [any StatusContentView], useTopSpacer: false).configure {
$0.setContentHuggingPriority(.defaultLow, for: .vertical) $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 let cardView = StatusCardView().configure {
} $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight).isActive = true
private var attachmentsView: AttachmentsContainerView {
contentContainer.attachmentsView
}
private var pollView: StatusPollView {
contentContainer.pollView
} }
let attachmentsView = AttachmentsContainerView()
let pollView = StatusPollView()
private var placeholderReplyButtonLeadingConstraint: NSLayoutConstraint! private var placeholderReplyButtonLeadingConstraint: NSLayoutConstraint!
private lazy var actionsContainer = UIView().configure { private lazy var actionsContainer = UIView().configure {
replyButton.translatesAutoresizingMaskIntoConstraints = false replyButton.translatesAutoresizingMaskIntoConstraints = false