import SwiftUI
struct AdaptiveTextWithLinkView: View { let text: String let maxLines: Int @State private var isExpanded = false @State private var isTruncated = false @State private var intrinsicSize: CGSize = .zero @State private var truncatedSize: CGSize = .zero
init(_ text: String, maxLines: Int = 2) {
self.text = text
self.maxLines = maxLines
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// 原始文本容器,用于测量
Text(text)
.background(
GeometryReader { geometry in
Color.clear
.preference(key: IntrinsicSizePreferenceKey.self, value: geometry.size)
}
)
.hidden()
// 显示文本
ZStack(alignment: .topLeading) {
// 展开状态显示完整文本
if isExpanded {
Text(text)
.fixedSize(horizontal: false, vertical: true)
} else {
// 截断状态
HStack(alignment: .top, spacing: 0) {
// 计算文本
Text(truncatedText)
.lineLimit(maxLines)
// 如果需要截断,显示 Learn More 链接
if isTruncated && !isExpanded {
Text(" Learn More »")
.foregroundColor(.blue)
.font(.body.weight(.medium))
.onTapGesture {
isExpanded = true
}
.alignmentGuide(.lastTextBaseline) { d in
d[.lastTextBaseline]
}
}
}
.background(
GeometryReader { geometry in
Color.clear
.preference(key: TruncatedSizePreferenceKey.self, value: geometry.size)
}
)
}
}
}
.onPreferenceChange(IntrinsicSizePreferenceKey.self) { size in
intrinsicSize = size
checkIfTruncated()
}
.onPreferenceChange(TruncatedSizePreferenceKey.self) { size in
truncatedSize = size
checkIfTruncated()
}
}
// 计算截断的文本
private var truncatedText: String {
guard isTruncated && !isExpanded else { return text }
// 尝试截断文本,留出空间给 " Learn More »"
let text = text as NSString
let ellipsis = "..."
let linkText = " Learn More »"
// 创建截断的文本
for i in stride(from: text.length - 1, to: 0, by: -1) {
let substring = text.substring(to: i) + ellipsis
let tempText = substring + linkText
// 这里应该使用更精确的文本尺寸计算
// 在实际项目中可以使用 Text 的 .measure 方法或自定义文本渲染来计算
if i > 10 { // 简单逻辑,实际应使用文本测量
return substring
}
}
return text as String
}
private func checkIfTruncated() {
// 如果截断后的高度小于原始高度,说明需要截断
// 这里使用简单的逻辑判断,实际项目中可能需要更精确的计算
isTruncated = truncatedSize.height < intrinsicSize.height
}
}
// MARK: - Preference Keys struct IntrinsicSizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } }
struct TruncatedSizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } }
// MARK: - 更简单的实现方案(推荐) struct SimpleAdaptiveTextView: View { let text: String let maxLines: Int = 2 @State private var isExpanded = false @State private var isTruncated = false @State private var textHeight: CGFloat = 0
var body: some View {
VStack(alignment: .leading, spacing: 4) {
if isExpanded {
// 展开状态
Text(text)
.fixedSize(horizontal: false, vertical: true)
} else {
// 截断状态
Text(text)
.lineLimit(maxLines)
.background(
GeometryReader { geometry in
Color.clear
.onAppear {
// 获取文本的实际高度
let size = text.boundingRect(
with: CGSize(width: geometry.size.width, height: .greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [.font: UIFont.preferredFont(forTextStyle: .body)],
context: nil
).size
textHeight = size.height
// 计算两行文本的大致高度
let lineHeight = UIFont.preferredFont(forTextStyle: .body).lineHeight
let maxHeight = lineHeight * CGFloat(maxLines)
// 判断是否需要截断
isTruncated = textHeight > maxHeight
}
}
)
.overlay(
Group {
if isTruncated && !isExpanded {
HStack(spacing: 0) {
Spacer()
Text("Learn More »")
.foregroundColor(.blue)
.font(.body.weight(.medium))
.onTapGesture {
withAnimation {
isExpanded = true
}
}
}
.offset(y: UIFont.preferredFont(forTextStyle: .body).lineHeight * 1.5)
}
},
alignment: .bottomTrailing
)
}
}
}
}
// MARK: - 使用示例 struct ContentView: View { let shortText = "Notice: Please be aware that there is a system maintenance." let longText = "Notice: Please be aware that there are several system maintenances scheduled on 29 Mar to 30 Mar. This may affect the availability of some services."
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("短文本示例:")
.font(.headline)
SimpleAdaptiveTextView(text: shortText)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
Text("长文本示例:")
.font(.headline)
SimpleAdaptiveTextView(text: longText)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
Spacer()
}
.padding()
}
}
// MARK: - 预览 struct AdaptiveTextWithLinkView_Previews: PreviewProvider { static var previews: some View { ContentView() } }