Skip to content

Commit 7c5243b

Browse files
committed
Hide long excerpts under the cut
1 parent ecf0070 commit 7c5243b

1 file changed

Lines changed: 95 additions & 1 deletion

File tree

Modules/Sources/WordPressReader/Detail/ReaderPostHeaderView.swift

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import UIKit
22
import AsyncImageKit
3+
import DesignSystem
34
import WordPressShared
45
import WordPressUI
56

@@ -145,6 +146,7 @@ public final class ReaderPostHeaderView: UIView {
145146
let label = UILabel()
146147
label.numberOfLines = 0
147148
label.adjustsFontForContentSizeCategory = true
149+
label.isUserInteractionEnabled = true
148150
label.isHidden = true
149151
return label
150152
}()
@@ -236,6 +238,9 @@ public final class ReaderPostHeaderView: UIView {
236238
private var featuredImageAspectConstraint: NSLayoutConstraint?
237239
private var avatarSizeConstraints: [NSLayoutConstraint] = []
238240
private var displaySettings: ReaderDisplaySettings = .standard
241+
private var fullExcerptText: String?
242+
private var isExcerptExpanded = false
243+
private var lastExcerptLayoutWidth: CGFloat = 0
239244

240245
public var isShowingSubscribeLoadingIndicator: Bool = false {
241246
didSet {
@@ -331,6 +336,9 @@ public final class ReaderPostHeaderView: UIView {
331336
separator.backgroundColor = colors.border
332337

333338
subscribeLoadingIndicator.color = colors.secondaryForeground
339+
340+
lastExcerptLayoutWidth = 0
341+
updateExcerptTruncation()
334342
}
335343

336344
// MARK: - Private
@@ -360,6 +368,8 @@ public final class ReaderPostHeaderView: UIView {
360368
authorRow.isUserInteractionEnabled = true
361369
authorRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(authorTapped)))
362370

371+
excerptLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(excerptTapped)))
372+
363373
featuredImageView.isUserInteractionEnabled = true
364374
featuredImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(featuredImageTapped)))
365375

@@ -368,6 +378,16 @@ public final class ReaderPostHeaderView: UIView {
368378
apply(.standard)
369379
}
370380

381+
public override func layoutSubviews() {
382+
super.layoutSubviews()
383+
384+
let width = mainStack.bounds.width
385+
if width > 0 && width != lastExcerptLayoutWidth {
386+
lastExcerptLayoutWidth = width
387+
updateExcerptTruncation()
388+
}
389+
}
390+
371391
// Extends tap area of the controls.
372392
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
373393
let expandedViews: [UIView] = [buttonSubscribe, siteNameLabel, authorRow, featuredImageView, buttonViewOriginal]
@@ -392,6 +412,14 @@ public final class ReaderPostHeaderView: UIView {
392412
delegate?.readerPostHeaderView(self, didTap: .author)
393413
}
394414

415+
@objc private func excerptTapped() {
416+
guard !isExcerptExpanded, let text = fullExcerptText else { return }
417+
isExcerptExpanded = true
418+
let font = displaySettings.font(with: .callout)
419+
let textColor = displaySettings.color.secondaryForeground
420+
excerptLabel.attributedText = NSAttributedString(string: text, attributes: [.font: font, .foregroundColor: textColor])
421+
}
422+
395423
@objc private func featuredImageTapped() {
396424
delegate?.readerPostHeaderView(self, didTap: .featuredImage)
397425
}
@@ -445,13 +473,73 @@ public final class ReaderPostHeaderView: UIView {
445473

446474
private func configureExcerpt(with excerpt: String?) {
447475
if let excerpt, !excerpt.isEmpty {
448-
excerptLabel.text = excerpt
476+
fullExcerptText = excerpt
449477
excerptLabel.isHidden = false
478+
lastExcerptLayoutWidth = 0
479+
updateExcerptTruncation()
450480
} else {
481+
fullExcerptText = nil
451482
excerptLabel.isHidden = true
452483
}
453484
}
454485

486+
private func updateExcerptTruncation() {
487+
guard let text = fullExcerptText, !text.isEmpty, !isExcerptExpanded else { return }
488+
489+
let font = displaySettings.font(with: .callout)
490+
let textColor = displaySettings.color.secondaryForeground
491+
let atttributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: textColor]
492+
let availableWidth = mainStack.bounds.width
493+
494+
guard availableWidth > 0 else {
495+
excerptLabel.attributedText = NSAttributedString(string: text, attributes: atttributes)
496+
return
497+
}
498+
499+
let maxHeight = font.lineHeight * CGFloat(Constants.excerptMaxLines) + 1
500+
501+
func isEnoughSpace(for string: String, maxHeight: CGFloat) -> Bool {
502+
let height = (string as NSString).boundingRect(
503+
with: CGSize(width: availableWidth, height: .greatestFiniteMagnitude),
504+
options: [.usesLineFragmentOrigin, .usesFontLeading],
505+
attributes: atttributes,
506+
context: nil
507+
).height
508+
return height <= maxHeight
509+
}
510+
511+
// Hide under the cut only if there is enough text to warrant it. If there is only one extra
512+
// line, there is no reason to cut it.
513+
if isEnoughSpace(for: text, maxHeight: maxHeight + font.leading * 1) {
514+
excerptLabel.attributedText = NSAttributedString(string: text, attributes: atttributes)
515+
return
516+
}
517+
518+
let suffix = " " + Strings.viewMore
519+
520+
// Find the longest prefix that fits with the suffix.
521+
var low = 0, high = text.count, bestCut = 0
522+
while low <= high {
523+
let mid = (low + high) / 2
524+
if isEnoughSpace(for: String(text.prefix(mid)) + suffix, maxHeight: maxHeight) {
525+
bestCut = mid
526+
low = mid + 1
527+
} else {
528+
high = mid - 1
529+
}
530+
}
531+
532+
let trimmed = String(text.prefix(bestCut)).trimmingCharacters(in: .whitespacesAndNewlines)
533+
let result = NSMutableAttributedString(string: trimmed, attributes: atttributes)
534+
result.append(
535+
NSAttributedString(string: suffix, attributes: [
536+
.font: font.withWeight(.regular),
537+
.foregroundColor: UIColor.label,
538+
])
539+
)
540+
excerptLabel.attributedText = result
541+
}
542+
455543
private func configureReadingTime(with readingTime: String) {
456544
readingTimeLabel.text = readingTime
457545
}
@@ -464,6 +552,7 @@ private extension ReaderPostHeaderView {
464552
static let padding: CGFloat = 16
465553
static let avatarSize: CGFloat = 32
466554
static let avatarSizeRegular: CGFloat = 40
555+
static let excerptMaxLines: Int = 5
467556
static let defaultFeaturedImageAspectRatio: CGFloat = 9.0 / 16.0
468557
static let maxFeaturedImageAspectRatio: CGFloat = 2.0
469558
}
@@ -480,6 +569,11 @@ private enum Strings {
480569
value: "View Original",
481570
comment: "Button in the reader post header to view the original post in a browser"
482571
)
572+
static let viewMore = AppLocalizedString(
573+
"reader.post.header.viewMore",
574+
value: "\u{2026}view more",
575+
comment: "Appended to the truncated excerpt in the reader post header to indicate more content is available"
576+
)
483577
}
484578

485579
// MARK: - Preview

0 commit comments

Comments
 (0)