Skip to content

Read More Text

A paragraph truncated with a fade that expands in place when tapped.

Read More Text, playing
  • Text
  • iOS 18.0+
  • beginner
Download .swift
Origin
Original work
Found by
read more · expandable text · truncate · show more · fade · paragraph · leer más · texto expandible · truncar · mostrar más · difuminado · párrafo

A paragraph truncated with a fade that expands in place when tapped.

The button only appears when the text does not fit. With Reduce Motion on, the paragraph changes height at once. VoiceOver always reads the whole paragraph.

struct ReadMoreTextDemo: View {
private let story = "The sea looks blue because water absorbs the red end of the spectrum and scatters the blue back towards your eyes. The deeper the water, the more red is lost, which is why the open ocean looks so much darker than a shallow bay. Near the coast, sand and algae add greens and browns, and a bright sky adds its own reflection to the surface."
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
RoundedRectangle(cornerRadius: 24)
.fill(LinearGradient(colors: [.cyan, .blue, .indigo], startPoint: .topLeading, endPoint: .bottomTrailing))
.frame(height: 220)
.overlay {
Image(systemName: "water.waves")
.font(.largeTitle)
.imageScale(.large)
.foregroundStyle(.white)
}
.accessibilityHidden(true)
Text("Why the sea is blue")
.font(.title.bold())
ReadMoreText(text: story)
.font(.body)
.foregroundStyle(.secondary)
}
.padding(20)
}
}
}
ParameterTypeDefault
textStringRequired
collapsedLinesInt3
moreLabelString"Read more"
lessLabelString"Show less"

Copy this code into your project, or download the file.

ReadMoreText.swift
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// A paragraph truncated with a fade that expands in place when tapped.
///
/// The button only appears when the text does not fit. With Reduce Motion on, the
/// paragraph changes height at once. VoiceOver always reads the whole paragraph.
struct ReadMoreText: View {
let text: String
var collapsedLines = 3
var moreLabel = "Read more"
var lessLabel = "Show less"
@State private var isExpanded = false
@State private var fullHeight: CGFloat = 0
@State private var collapsedHeight: CGFloat = 0
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private let touchPadding: CGFloat = 13
private var isTruncated: Bool { fullHeight > collapsedHeight + 1 && collapsedHeight > 0 }
private var isFaded: Bool { isTruncated && !isExpanded }
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(text)
.fixedSize(horizontal: false, vertical: true)
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { fullHeight = $0 }
.background { collapsedMeasure }
.frame(height: isTruncated ? (isExpanded ? fullHeight : collapsedHeight) : nil, alignment: .top)
.clipped()
.mask { fade }
if isTruncated {
Button {
isExpanded.toggle()
} label: {
// A line of text is too short to hit. The padding widens what answers to a
// touch, and the same padding taken back keeps the layout as it was.
Text(isExpanded ? lessLabel : moreLabel)
.padding(.vertical, touchPadding)
.contentShape(.rect)
.padding(.vertical, -touchPadding)
}
.font(.subheadline.weight(.semibold))
.foregroundStyle(.tint)
}
}
.animation(reduceMotion ? nil : .smooth(duration: 0.4), value: isExpanded)
}
// A hidden copy, limited to the collapsed lines, tells how tall the collapsed text is.
private var collapsedMeasure: some View {
Text(text)
.lineLimit(max(collapsedLines, 1))
.fixedSize(horizontal: false, vertical: true)
.hidden()
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { collapsedHeight = $0 }
.accessibilityRepresentation { EmptyView() }
}
private var fade: LinearGradient {
LinearGradient(
stops: [
.init(color: .black, location: 0),
.init(color: .black, location: isFaded ? 0.55 : 1),
.init(color: isFaded ? .clear : .black, location: 1),
],
startPoint: .top,
endPoint: .bottom
)
}
}
// MARK: - Demo
struct ReadMoreTextDemo: View {
private let story = "The sea looks blue because water absorbs the red end of the spectrum and scatters the blue back towards your eyes. The deeper the water, the more red is lost, which is why the open ocean looks so much darker than a shallow bay. Near the coast, sand and algae add greens and browns, and a bright sky adds its own reflection to the surface."
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
RoundedRectangle(cornerRadius: 24)
.fill(LinearGradient(colors: [.cyan, .blue, .indigo], startPoint: .topLeading, endPoint: .bottomTrailing))
.frame(height: 220)
.overlay {
Image(systemName: "water.waves")
.font(.largeTitle)
.imageScale(.large)
.foregroundStyle(.white)
}
.accessibilityHidden(true)
Text("Why the sea is blue")
.font(.title.bold())
ReadMoreText(text: story)
.font(.body)
.foregroundStyle(.secondary)
}
.padding(20)
}
}
}
#Preview {
ReadMoreTextDemo()
}