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)
}
private let contentContainer = StatusContentContainer<StatusEditContentTextView, StatusEditPollView>(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

View File

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

View File

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

View File

@ -117,18 +117,38 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
}
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(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,

View File

@ -24,7 +24,11 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
var usernameLabel: UILabel { get }
var contentWarningLabel: EmojiLabel { 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 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

View File

@ -8,45 +8,11 @@
import UIKit
protocol StatusContentPollView: UIView {
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat
}
class StatusContentContainer<ContentView: ContentTextView, PollView: StatusContentPollView>: 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<ContentView: ContentTextView, PollView: StatusConte
subviews.filter { !$0.isHidden }.map(\.bounds.height).reduce(0, +)
}
init(useTopSpacer: Bool) {
self.useTopSpacer = useTopSpacer
init(arrangedSubviews: [any StatusContentView], useTopSpacer: Bool) {
var arrangedSubviews = arrangedSubviews
if useTopSpacer {
arrangedSubviews.insert(TopSpacerView(), at: 0)
}
self.arrangedSubviews = arrangedSubviews
super.init(frame: .zero)
@ -83,7 +53,7 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
setNeedsUpdateConstraints()
isHiddenObservations = arrangedSubviews.map {
$0.observe(\.isHidden) { [unowned self] _, _ in
$0.observeIsHidden { [unowned self] in
self.setNeedsUpdateConstraints()
}
}
@ -147,18 +117,60 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
// 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
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
}
}

View File

@ -186,25 +186,34 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
$0.setContentCompressionResistancePriority(.required, for: .vertical)
}
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(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