fix(qlpreview): inline share icon instead of fixed position (gitea#7 review)
Android UI Tests / ui-tests (pull_request) Has been cancelled

The previous copy "1. Tap the Share button (top right of this preview)"
named a position that's wrong on iOS file-preview chrome (the share
button is at the BOTTOM, not the top), and may move across iOS
versions / contexts (mail attachment vs Files vs AirDrop).

Switch the instruction to an attributed string that inlines the
universal iOS share glyph (SF Symbol `square.and.arrow.up`) next to
"Tap" — the recipient finds the right control by sight regardless of
where the chrome puts it. New `PreviewViewController.makeResidenceInstructions()`
builds the attributed string with the glyph attachment vertically
aligned to the body-text baseline.

`Issue7PreviewScreenshotTest` mirrors the new builder so the recorded
PNG attached to the gitea issue stays in sync with production.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-05-11 13:46:59 -05:00
parent d26714f043
commit f4c2780e34
3 changed files with 110 additions and 12 deletions
@@ -278,14 +278,13 @@ class PreviewViewController: UIViewController, QLPreviewingController {
titleLabel.text = residence.residenceName
subtitleLabel.text = "honeyDue Residence Invite"
// Numbered steps so non-technical recipients know exactly what to
// do. Replaces the prior "Tap the share button below" which
// implied an in-app button and didn't say where the share
// control lives (top-right of the QuickLook bar).
instructionLabel.text = "How to join:\n"
+ "1. Tap the Share button (top right of this preview)\n"
+ "2. Choose \"honeyDue\" from the share sheet\n"
+ "3. Sign in if prompted — the app finishes the rest"
// Numbered steps so non-technical recipients know exactly what
// to do. The share button's location varies across the iOS
// QuickLook chrome (bottom on file previews, top in mail
// previews, sometimes hidden behind a "More" menu) so we lean
// on the SF Symbol glyph inline instead of describing a
// position that may not match what the recipient sees.
instructionLabel.attributedText = Self.makeResidenceInstructions()
// Clear existing details
detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
@@ -362,6 +361,60 @@ class PreviewViewController: UIViewController, QLPreviewingController {
f.timeStyle = .short
return f
}()
/// Builds the "How to join" instruction copy as an attributed
/// string with the iOS share-icon glyph (square + up-arrow) inlined
/// next to "Tap [icon]". The glyph is the universal share symbol
/// across iOS, so the recipient finds the right control whether
/// it's at the top, bottom, or behind a More menu instead of us
/// claiming a fixed position the chrome can move (gitea#7 review
/// feedback).
private static func makeResidenceInstructions() -> NSAttributedString {
let bodyFont = UIFont.systemFont(ofSize: 15, weight: .medium)
let tint = UIColor(red: 7/255, green: 160/255, blue: 195/255, alpha: 1)
let paragraph = NSMutableParagraphStyle()
paragraph.lineSpacing = 2
paragraph.alignment = .left
let result = NSMutableAttributedString()
func appendText(_ s: String) {
result.append(NSAttributedString(
string: s,
attributes: [
.font: bodyFont,
.foregroundColor: tint,
.paragraphStyle: paragraph,
]
))
}
appendText("How to join:\n1. Tap ")
let shareImage = UIImage(
systemName: "square.and.arrow.up",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
)?.withTintColor(tint, renderingMode: .alwaysOriginal)
if let shareImage {
let attachment = NSTextAttachment()
attachment.image = shareImage
// Align the glyph baseline with the surrounding text by
// nudging the bounds down a few points; the SF Symbol's
// natural bounds sit a hair above the cap height.
attachment.bounds = CGRect(
x: 0,
y: -3,
width: shareImage.size.width,
height: shareImage.size.height
)
result.append(NSAttributedString(attachment: attachment))
}
appendText("\n2. Choose \"honeyDue\" from the share sheet")
appendText("\n3. Sign in if prompted — the app finishes the rest")
return result
}
}
// MARK: - Type Discriminator