From a991e0f42996023126c76271f4a3f96043d8b717 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 3 Nov 2022 22:15:54 -0400 Subject: [PATCH] Dynamic Type support in status cells --- .../AttachmentsContainerView.swift | 2 + .../ConversationMainStatusTableViewCell.swift | 29 ++++- .../ConversationMainStatusTableViewCell.xib | 10 +- Tusker/Views/Status/StatusCardView.swift | 2 + .../Views/Status/StatusContentContainer.swift | 3 +- .../Status/StatusMetaIndicatorsView.swift | 111 ++++++++++++++++-- .../TimelineStatusCollectionViewCell.swift | 11 +- .../Status/TimelineStatusTableViewCell.swift | 46 +++++++- 8 files changed, 185 insertions(+), 29 deletions(-) diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index aada5c97..b313d46e 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -292,6 +292,8 @@ class AttachmentsContainerView: UIView { let imageView = UIImageView(image: image) imageView.translatesAutoresizingMaskIntoConstraints = false let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.adjustsFontForContentSizeCategory = true label.text = "Sensitive Content" let stack = UIStackView(arrangedSubviews: [ imageView, diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index ee447bcb..5cae0150 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -49,16 +49,35 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell { moreButton!, ] - contentTextView.defaultFont = .systemFont(ofSize: 18) + profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self)) + + displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold)) + displayNameLabel.adjustsFontForContentSizeCategory = true + + usernameLabel.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: .systemFont(ofSize: 17, weight: .light)) + usernameLabel.adjustsFontForContentSizeCategory = true + + metaIndicatorsView.allowedIndicators = [.visibility, .localOnly] + metaIndicatorsView.squeezeHorizontal = true + metaIndicatorsView.primaryAxis = .horizontal + + contentWarningLabel.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)! + contentWarningLabel.adjustsFontForContentSizeCategory = true + + contentTextView.defaultFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 18)) + contentTextView.adjustsFontForContentSizeCategory = true contentTextView.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber] if #available(iOS 16.0, *) { contentTextView.dataDetectorTypes.formUnion([.money, .physicalValue]) } - profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self)) - - metaIndicatorsView.allowedIndicators = [.visibility, .localOnly] - metaIndicatorsView.squeezeHorizontal = true + let metaFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15)) + totalFavoritesButton.titleLabel!.font = metaFont + totalFavoritesButton.titleLabel!.adjustsFontForContentSizeCategory = true + totalReblogsButton.titleLabel!.font = metaFont + totalReblogsButton.titleLabel!.adjustsFontForContentSizeCategory = true + timestampAndClientLabel.font = metaFont + timestampAndClientLabel.adjustsFontForContentSizeCategory = true } override func doUpdateUI(status: StatusMO, state: StatusState) { diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib b/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib index fe52bd1b..cbd1962a 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -261,13 +261,13 @@ - - + + - + diff --git a/Tusker/Views/Status/StatusCardView.swift b/Tusker/Views/Status/StatusCardView.swift index 089be89c..ef813c2e 100644 --- a/Tusker/Views/Status/StatusCardView.swift +++ b/Tusker/Views/Status/StatusCardView.swift @@ -51,10 +51,12 @@ class StatusCardView: UIView { titleLabel = UILabel() titleLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!, size: 0) + titleLabel.adjustsFontForContentSizeCategory = true titleLabel.numberOfLines = 2 descriptionLabel = UILabel() descriptionLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1), size: 0) + descriptionLabel.adjustsFontForContentSizeCategory = true descriptionLabel.numberOfLines = 2 descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical) diff --git a/Tusker/Views/Status/StatusContentContainer.swift b/Tusker/Views/Status/StatusContentContainer.swift index 5abceb53..5ae4652e 100644 --- a/Tusker/Views/Status/StatusContentContainer.swift +++ b/Tusker/Views/Status/StatusContentContainer.swift @@ -11,7 +11,8 @@ import UIKit class StatusContentContainer: UIView { let contentTextView = StatusContentTextView().configure { - $0.defaultFont = .systemFont(ofSize: 16) + $0.defaultFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 16)) + $0.adjustsFontForContentSizeCategory = true $0.isScrollEnabled = false $0.backgroundColor = nil $0.isEditable = false diff --git a/Tusker/Views/Status/StatusMetaIndicatorsView.swift b/Tusker/Views/Status/StatusMetaIndicatorsView.swift index 30f065ea..dd8edb45 100644 --- a/Tusker/Views/Status/StatusMetaIndicatorsView.swift +++ b/Tusker/Views/Status/StatusMetaIndicatorsView.swift @@ -13,11 +13,48 @@ 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 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) + NotificationCenter.default.addObserver(self, selector: #selector(configureImageViews), name: UIContentSizeCategory.didChangeNotification, object: nil) + } + + @objc private func configureImageViews() { + for image in images { + configureImageView(image) + } + if isUsingSingleAxis != needsSingleAxis { + placeImageViews(images) + } + } + + 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) { - images.forEach { $0.removeFromSuperview() } - var images: [UIImage] = [] if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil { @@ -32,13 +69,66 @@ class StatusMetaIndicatorsView: UIView { images.append(UIImage(named: "link.broken")!) } - self.images = [] - for (index, image) in images.enumerated() { - let v = UIImageView(image: image) + let views = images.map { + let v = UIImageView(image: $0) v.translatesAutoresizingMaskIntoConstraints = false v.contentMode = .scaleAspectFit v.tintColor = .secondaryLabel - v.preferredSymbolConfiguration = .init(weight: .thin) + 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 { @@ -64,14 +154,9 @@ class StatusMetaIndicatorsView: UIView { v.topAnchor.constraint(equalTo: self.images[index - 1].bottomAnchor, constant: 4).isActive = true } v.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor).isActive = true - - self.images.append(v) } } -} - -extension StatusMetaIndicatorsView { struct Indicator: OptionSet { let rawValue: Int @@ -81,4 +166,8 @@ extension StatusMetaIndicatorsView { static let all: Indicator = [.reply, .visibility, .localOnly] } + + enum Alignment { + case leading, trailing + } } diff --git a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift index b61455e7..a964fb22 100644 --- a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift @@ -18,6 +18,8 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti private lazy var reblogLabel = EmojiLabel().configure { $0.textColor = .secondaryLabel + $0.font = .preferredFont(forTextStyle: .body) + $0.adjustsFontForContentSizeCategory = true // this needs to have a higher priorty than the content container's zero height constraint $0.setContentHuggingPriority(.defaultHigh, for: .vertical) $0.isUserInteractionEnabled = true @@ -58,7 +60,10 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti $0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) } - private let metaIndicatorsView = StatusMetaIndicatorsView() + private let metaIndicatorsView = StatusMetaIndicatorsView().configure { + $0.primaryAxis = .vertical + $0.secondaryAxisAlignment = .trailing + } private lazy var contentVStack = UIStackView(arrangedSubviews: [ nameHStack, @@ -87,6 +92,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue, ] ]), size: 0) + $0.adjustsFontForContentSizeCategory = true $0.setContentHuggingPriority(.init(251), for: .horizontal) $0.setContentCompressionResistancePriority(.init(749), for: .horizontal) } @@ -98,6 +104,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, ] ]), size: 0) + $0.adjustsFontForContentSizeCategory = true $0.setContentHuggingPriority(.init(249), for: .horizontal) $0.setContentCompressionResistancePriority(.init(748), for: .horizontal) } @@ -114,6 +121,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, ] ]), size: 0) + $0.adjustsFontForContentSizeCategory = true } private(set) lazy var contentWarningLabel = EmojiLabel().configure { @@ -124,6 +132,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold.rawValue, ] ]), size: 0) + $0.adjustsFontForContentSizeCategory = true // this needs to have a higher priorty than the content container's zero height constraint $0.setContentHuggingPriority(.defaultHigh, for: .vertical) $0.isUserInteractionEnabled = true diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 8114c789..81471e45 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -45,18 +45,52 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { override func awakeFromNib() { super.awakeFromNib() - - reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed))) isAccessibilityElement = true - // todo: double check this on RTL layouts - replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true - - contentTextView.defaultFont = .systemFont(ofSize: 16) + reblogLabel.font = .preferredFont(forTextStyle: .body) + reblogLabel.adjustsFontForContentSizeCategory = true + reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed))) avatarImageView.addInteraction(UIContextMenuInteraction(delegate: self)) + displayNameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ + .traits: [ + UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue, + ] + ]), size: 0) + displayNameLabel.adjustsFontForContentSizeCategory = true + + usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ + .traits: [ + UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, + ] + ]), size: 0) + usernameLabel.adjustsFontForContentSizeCategory = true + + timestampLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ + .traits: [ + UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, + ] + ]), size: 0) + timestampLabel.adjustsFontForContentSizeCategory = true + + metaIndicatorsView.primaryAxis = .vertical + metaIndicatorsView.secondaryAxisAlignment = .trailing + + contentWarningLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ + .traits: [ + UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold.rawValue, + ] + ]), size: 0) + contentWarningLabel.adjustsFontForContentSizeCategory = true + + contentTextView.defaultFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 16)) + contentTextView.adjustsFontForContentSizeCategory = true + + // todo: double check this on RTL layouts + replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true + updateActionsVisibility() }