From 74f7f8afe3577d0f38c769a9c302ce6e0d082784 Mon Sep 17 00:00:00 2001 From: Johan Marais Date: Thu, 8 Oct 2026 15:22:52 +0200 Subject: [PATCH] fix(people): let a clipped biography be read in full on iPhone, iPad and Mac The person page cut the biography at eight lines with no way to read the rest. A More control now appears when the text is clipped; it, or a tap on the text, shows the whole biography. Short biographies show no control. Co-Authored-By: Claude Opus 5.5 --- .../Screens/People/PersonDetailView.swift | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/iosApp/iosApp/Screens/People/PersonDetailView.swift b/iosApp/iosApp/Screens/People/PersonDetailView.swift index 1d4d52ebd..774e9ad45 100644 --- a/iosApp/iosApp/Screens/People/PersonDetailView.swift +++ b/iosApp/iosApp/Screens/People/PersonDetailView.swift @@ -579,11 +579,7 @@ private struct PhonePersonDetailContent: View { metadataWrap if let bio = clean(person.bio) { - Text(bio) - .font(.siloBody) - .foregroundColor(.siloSecondaryText) - .lineLimit(8) - .fixedSize(horizontal: false, vertical: true) + PersonBiography(text: bio) } } } @@ -639,6 +635,56 @@ private struct PhonePersonDetailContent: View { } #endif +#if !os(tvOS) +/// The biography, clipped to eight lines. When that cuts text off, a More +/// control (or a tap on the text) shows all of it; a short biography shows +/// no control. +private struct PersonBiography: View { + let text: String + + @State private var isExpanded = false + @State private var clippedHeight: CGFloat = 0 + @State private var fullHeight: CGFloat = 0 + + private var isClipped: Bool { fullHeight > clippedHeight + 1 } + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(text) + .font(.siloBody) + .foregroundColor(.siloSecondaryText) + .lineLimit(isExpanded ? nil : 8) + .fixedSize(horizontal: false, vertical: true) + .onGeometryChange(for: CGFloat.self, of: \.size.height) { height in + if !isExpanded { clippedHeight = height } + } + // The same text at the same width with no line limit, to + // learn whether eight lines cut anything off. + .background(alignment: .top) { + Text(text) + .font(.siloBody) + .fixedSize(horizontal: false, vertical: true) + .hidden() + .onGeometryChange(for: CGFloat.self, of: \.size.height) { fullHeight = $0 } + } + .contentShape(Rectangle()) + .onTapGesture { if isClipped { toggle() } } + + if isClipped { + Button(isExpanded ? "Less" : "More", action: toggle) + .buttonStyle(.plain) + .font(.siloBody.weight(.semibold)) + .foregroundColor(.siloOnSurface) + } + } + } + + private func toggle() { + withAnimation(.easeInOut(duration: SiloTheme.normalDuration)) { isExpanded.toggle() } + } +} +#endif + private struct PersonPortrait: View { let person: Person let width: CGFloat