mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
Estimate status cell heights
This commit is contained in:
parent
b164a6265e
commit
083b52c802
10 changed files with 209 additions and 5 deletions
|
@ -25,4 +25,17 @@ extension CollectionItem {
|
||||||
return ConversationListCell.self
|
return ConversationListCell.self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func estimatedHeight(width: CGFloat, identification: Identification) -> CGFloat {
|
||||||
|
switch self {
|
||||||
|
case let .status(status, configuration):
|
||||||
|
return StatusView.estimatedHeight(
|
||||||
|
width: width,
|
||||||
|
identification: identification,
|
||||||
|
status: status,
|
||||||
|
configuration: configuration)
|
||||||
|
default:
|
||||||
|
return UITableView.automaticDimension
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,15 @@ import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
func height(width: CGFloat, font: UIFont) -> CGFloat {
|
||||||
|
(self as NSString).boundingRect(
|
||||||
|
with: CGSize(width: width, height: .greatestFiniteMagnitude),
|
||||||
|
options: .usesLineFragmentOrigin,
|
||||||
|
attributes: [.font: font],
|
||||||
|
context: nil)
|
||||||
|
.height
|
||||||
|
}
|
||||||
|
|
||||||
func countEmphasizedAttributedString(count: Int, highlighted: Bool = false) -> NSAttributedString {
|
func countEmphasizedAttributedString(count: Int, highlighted: Bool = false) -> NSAttributedString {
|
||||||
let countRange = (self as NSString).range(of: String.localizedStringWithFormat("%ld", count))
|
let countRange = (self as NSString).range(of: String.localizedStringWithFormat("%ld", count))
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,9 @@ class TableViewController: UITableViewController {
|
||||||
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
|
||||||
|
|
||||||
return cellHeightCaches[tableView.frame.width]?[item] ?? UITableView.automaticDimension
|
return cellHeightCaches[tableView.frame.width]?[item]
|
||||||
|
?? item.estimatedHeight(width: tableView.readableContentGuide.layoutFrame.width,
|
||||||
|
identification: identification)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -80,6 +81,20 @@ final class AttachmentsView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttachmentsView {
|
extension AttachmentsView {
|
||||||
|
static func estimatedHeight(width: CGFloat,
|
||||||
|
identification: Identification,
|
||||||
|
status: Status,
|
||||||
|
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
|
||||||
|
let height: CGFloat
|
||||||
|
if status.displayStatus.mediaAttachments.count == 1,
|
||||||
|
let aspectRatio = status.mediaAttachments.first?.aspectRatio {
|
||||||
|
height = width / max(CGFloat(aspectRatio), 16 / 9)
|
||||||
|
} else {
|
||||||
|
height = width / (16 / 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
return height
|
||||||
|
}
|
||||||
var shouldAutoplay: Bool {
|
var shouldAutoplay: Bool {
|
||||||
guard !isHidden, let viewModel = viewModel, viewModel.shouldShowAttachments else { return false }
|
guard !isHidden, let viewModel = viewModel, viewModel.shouldShowAttachments else { return false }
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,12 @@ final class PollOptionButton: UIButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PollOptionButton {
|
||||||
|
static func estimatedHeight(width: CGFloat, title: String) -> CGFloat {
|
||||||
|
title.height(width: width, font: .preferredFont(forTextStyle: .callout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension PollOptionButton {
|
private extension PollOptionButton {
|
||||||
static let titleEdgeInsets = UIEdgeInsets(top: 0, left: .compactSpacing, bottom: 0, right: .compactSpacing)
|
static let titleEdgeInsets = UIEdgeInsets(top: 0, left: .compactSpacing, bottom: 0, right: .compactSpacing)
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,14 @@ final class PollResultView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PollResultView {
|
||||||
|
static func estimatedHeight(width: CGFloat, title: String) -> CGFloat {
|
||||||
|
title.height(width: width, font: .preferredFont(forTextStyle: .callout))
|
||||||
|
+ .compactSpacing
|
||||||
|
+ 4 // progress view height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension PollResultView {
|
private extension PollResultView {
|
||||||
private static var percentFormatter: NumberFormatter = {
|
private static var percentFormatter: NumberFormatter = {
|
||||||
let percentageFormatter = NumberFormatter()
|
let percentageFormatter = NumberFormatter()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -112,6 +113,34 @@ final class PollView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PollView {
|
extension PollView {
|
||||||
|
static func estimatedHeight(width: CGFloat,
|
||||||
|
identification: Identification,
|
||||||
|
status: Status,
|
||||||
|
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
|
||||||
|
if let poll = status.displayStatus.poll {
|
||||||
|
var height: CGFloat = 0
|
||||||
|
let open = !poll.expired && !poll.voted
|
||||||
|
|
||||||
|
for option in poll.options {
|
||||||
|
height += open ? PollOptionButton.estimatedHeight(width: width, title: option.title)
|
||||||
|
: PollResultView.estimatedHeight(width: width, title: option.title)
|
||||||
|
height += .defaultSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if open {
|
||||||
|
height += .minimumButtonDimension + .defaultSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
height += .minimumButtonDimension / 2
|
||||||
|
|
||||||
|
return height
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension PollView {
|
||||||
func initialSetup() {
|
func initialSetup() {
|
||||||
addSubview(stackView)
|
addSubview(stackView)
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -146,11 +175,20 @@ extension PollView {
|
||||||
|
|
||||||
bottomStackView.addArrangedSubview(UIView())
|
bottomStackView.addArrangedSubview(UIView())
|
||||||
|
|
||||||
|
let voteButtonHeightConstraint = voteButton.heightAnchor.constraint(equalToConstant: .minimumButtonDimension)
|
||||||
|
let refreshButtonHeightConstraint = refreshButton.heightAnchor.constraint(
|
||||||
|
equalToConstant: .minimumButtonDimension / 2)
|
||||||
|
|
||||||
|
refreshButtonHeightConstraint.priority = .justBelowMax
|
||||||
|
refreshButtonHeightConstraint.priority = .justBelowMax
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
stackView.topAnchor.constraint(equalTo: topAnchor),
|
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
voteButtonHeightConstraint,
|
||||||
|
refreshButtonHeightConstraint
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -44,6 +45,23 @@ final class CardView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension CardView {
|
||||||
|
static func estimatedHeight(width: CGFloat,
|
||||||
|
identification: Identification,
|
||||||
|
status: Status,
|
||||||
|
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
|
||||||
|
if status.displayStatus.card != nil {
|
||||||
|
return round(UIFont.preferredFont(forTextStyle: .headline).lineHeight
|
||||||
|
+ UIFont.preferredFont(forTextStyle: .subheadline).lineHeight
|
||||||
|
+ UIFont.preferredFont(forTextStyle: .footnote).lineHeight
|
||||||
|
+ .defaultSpacing * 2
|
||||||
|
+ .compactSpacing * 2)
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension CardView {
|
private extension CardView {
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length
|
||||||
func initialSetup() {
|
func initialSetup() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ final class StatusBodyView: UIView {
|
||||||
attachmentsView.isHidden = viewModel.attachmentViewModels.isEmpty
|
attachmentsView.isHidden = viewModel.attachmentViewModels.isEmpty
|
||||||
attachmentsView.viewModel = viewModel
|
attachmentsView.viewModel = viewModel
|
||||||
|
|
||||||
pollView.isHidden = viewModel.pollOptions.isEmpty
|
pollView.isHidden = viewModel.pollOptions.isEmpty || !viewModel.shouldShowContent
|
||||||
pollView.viewModel = viewModel
|
pollView.viewModel = viewModel
|
||||||
|
|
||||||
cardView.viewModel = viewModel.cardViewModel
|
cardView.viewModel = viewModel.cardViewModel
|
||||||
|
@ -69,6 +70,64 @@ final class StatusBodyView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusBodyView {
|
||||||
|
static func estimatedHeight(width: CGFloat,
|
||||||
|
identification: Identification,
|
||||||
|
status: Status,
|
||||||
|
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
|
||||||
|
let contentFont = UIFont.preferredFont(forTextStyle: configuration.isContextParent ? .title3 : .callout)
|
||||||
|
var height: CGFloat = 0
|
||||||
|
|
||||||
|
var contentHeight = status.displayStatus.content.attributed.string.height(
|
||||||
|
width: width,
|
||||||
|
font: contentFont)
|
||||||
|
|
||||||
|
if status.displayStatus.card != nil {
|
||||||
|
contentHeight += .compactSpacing
|
||||||
|
contentHeight += CardView.estimatedHeight(
|
||||||
|
width: width,
|
||||||
|
identification: identification,
|
||||||
|
status: status,
|
||||||
|
configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.displayStatus.poll != nil {
|
||||||
|
contentHeight += .defaultSpacing
|
||||||
|
contentHeight += PollView.estimatedHeight(
|
||||||
|
width: width,
|
||||||
|
identification: identification,
|
||||||
|
status: status,
|
||||||
|
configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.displayStatus.spoilerText.isEmpty {
|
||||||
|
height += contentHeight
|
||||||
|
} else {
|
||||||
|
height += status.displayStatus.spoilerText.height(width: width, font: contentFont)
|
||||||
|
height += .compactSpacing
|
||||||
|
height += NSLocalizedString("status.show-more", comment: "").height(
|
||||||
|
width: width,
|
||||||
|
font: .preferredFont(forTextStyle: .headline))
|
||||||
|
|
||||||
|
if configuration.showContentToggled && !identification.identity.preferences.readingExpandSpoilers {
|
||||||
|
height += .compactSpacing
|
||||||
|
height += contentHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !status.displayStatus.mediaAttachments.isEmpty {
|
||||||
|
height += .compactSpacing
|
||||||
|
height += AttachmentsView.estimatedHeight(
|
||||||
|
width: width,
|
||||||
|
identification: identification,
|
||||||
|
status: status,
|
||||||
|
configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension StatusBodyView: UITextViewDelegate {
|
extension StatusBodyView: UITextViewDelegate {
|
||||||
func textView(
|
func textView(
|
||||||
_ textView: UITextView,
|
_ textView: UITextView,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
// swiftlint:disable file_length
|
// swiftlint:disable file_length
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -56,6 +57,36 @@ final class StatusView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusView {
|
||||||
|
static func estimatedHeight(width: CGFloat,
|
||||||
|
identification: Identification,
|
||||||
|
status: Status,
|
||||||
|
configuration: CollectionItem.StatusConfiguration) -> CGFloat {
|
||||||
|
var height = CGFloat.defaultSpacing * 2
|
||||||
|
let bodyWidth = width - .defaultSpacing - .avatarDimension
|
||||||
|
|
||||||
|
if status.reblog != nil || configuration.isPinned {
|
||||||
|
height += UIFont.preferredFont(forTextStyle: .caption1).lineHeight + .compactSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.isContextParent {
|
||||||
|
height += .avatarDimension + .minimumButtonDimension * 2.5 + .hairline * 2 + .compactSpacing * 4
|
||||||
|
} else {
|
||||||
|
height += UIFont.preferredFont(forTextStyle: .headline).lineHeight
|
||||||
|
+ .compactSpacing + .minimumButtonDimension / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
height += StatusBodyView.estimatedHeight(
|
||||||
|
width: bodyWidth,
|
||||||
|
identification: identification,
|
||||||
|
status: status,
|
||||||
|
configuration: configuration)
|
||||||
|
+ .compactSpacing
|
||||||
|
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension StatusView: UIContentView {
|
extension StatusView: UIContentView {
|
||||||
var configuration: UIContentConfiguration {
|
var configuration: UIContentConfiguration {
|
||||||
get { statusConfiguration }
|
get { statusConfiguration }
|
||||||
|
@ -142,6 +173,7 @@ private extension StatusView {
|
||||||
mainStackView.addArrangedSubview(nameAccountContainerStackView)
|
mainStackView.addArrangedSubview(nameAccountContainerStackView)
|
||||||
|
|
||||||
mainStackView.addArrangedSubview(bodyView)
|
mainStackView.addArrangedSubview(bodyView)
|
||||||
|
bodyView.tag = 666
|
||||||
|
|
||||||
contextParentTimeLabel.font = .preferredFont(forTextStyle: .footnote)
|
contextParentTimeLabel.font = .preferredFont(forTextStyle: .footnote)
|
||||||
contextParentTimeLabel.adjustsFontForContentSizeCategory = true
|
contextParentTimeLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
@ -264,7 +296,10 @@ private extension StatusView {
|
||||||
avatarButton.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
|
avatarButton.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
|
||||||
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
||||||
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
||||||
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor)
|
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
||||||
|
contextParentTimeApplicationStackView.heightAnchor.constraint(
|
||||||
|
greaterThanOrEqualToConstant: .minimumButtonDimension / 2),
|
||||||
|
interactionsStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,7 +409,8 @@ private extension StatusView {
|
||||||
if isContextParent {
|
if isContextParent {
|
||||||
button.heightAnchor.constraint(equalToConstant: .minimumButtonDimension).isActive = true
|
button.heightAnchor.constraint(equalToConstant: .minimumButtonDimension).isActive = true
|
||||||
} else {
|
} else {
|
||||||
button.heightAnchor.constraint(greaterThanOrEqualToConstant: 0).isActive = true
|
button.heightAnchor.constraint(
|
||||||
|
greaterThanOrEqualToConstant: .minimumButtonDimension / 2).isActive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue