Text文本布局

23 阅读2分钟

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() } }