我的目标是实现屏幕截图所示的 UI。简而言之,我有一个可以超过 1 行的文本视图,我需要添加类似于下划线的内容,它是单个文本行的一半粗,并且有一些垂直偏移。
首先想到的是使用 .underline(),但是它不能让您自定义行高和位置。 下一个想法是使用 .overlay,但在这种情况下,背景应用于整个视图(可以超过 1 行),而我需要将它单独应用于每一行。
P.s. - 不能将文本拆分为两个或多个视图,因为字符串是动态的。
我不相信这可以直接在 SwiftUI 中完成。
Text
仍然极其有限。但这可以用UIViewRepresentable
来完成。很难让它完全匹配 Text
,你必须小心,只使用 UIKit 理解的属性,但它确实有效。
我在这里假设您真的只想将这个额外的背景覆盖在整个字符串上,而不是实际尝试实现一种特殊的下划线。 (如果您只需要某些文本,那么也应该可以构建它。)
首先,从
TextKitView
的基本样板结构开始:
struct TextKitView: UIViewRepresentable {
var text: AttributedString
class CustomTextView: UIView {
private let textStorage = NSTextStorage()
private let layoutManager = NSLayoutManager()
private let textContainer: NSTextContainer
init(text: AttributedString, frame: CGRect) {
self.textContainer = NSTextContainer(size: frame.size)
self.textContainer.lineFragmentPadding = 0 // Better match Text
super.init(frame: frame)
// Setup TextKit components
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
// Set the text
textStorage.setAttributedString(NSAttributedString(text))
backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let range = layoutManager.glyphRange(for: textContainer)
layoutManager.drawBackground(forGlyphRange: range, at: CGPoint.zero)
layoutManager.drawGlyphs(forGlyphRange: range, at: CGPoint.zero)
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
textContainer.size = size
layoutManager.ensureLayout(for: textContainer)
// Get the size of the content that fits the text container
let usedRect = layoutManager.usedRect(for: textContainer)
return CGSize(width: usedRect.width, height: usedRect.height)
}
}
func makeUIView(context: Context) -> CustomTextView {
CustomTextView(text: text, frame: .zero)
}
func updateUIView(_ uiView: CustomTextView, context: Context) {
uiView.setNeedsDisplay()
}
func sizeThatFits(_ proposal: ProposedViewSize,
uiView: CustomTextView,
context: Context) -> CGSize? {
uiView.sizeThatFits(proposal.replacingUnspecifiedDimensions())
}
}
是的,它有很多样板,但它是构建自定义文本视图的一个很好的起点。
有了这个,您需要进行两项更改才能获得您所描述的内容。添加到
CustomTextView
一个方法,将您想要的内容绘制为每个封闭矩形的“下划线”:
private func drawUnderline(range: NSRange) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let color = CGColor(red: 233.0/255,
green: 217.0/255,
blue: 190.0/255,
alpha: 1)
context.setFillColor(color)
layoutManager.enumerateEnclosingRects(forGlyphRange: range,
withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0),
in: textContainer) { rect, _ in
let halfHeight = rect.size.height / 2
let halfOrigin = CGPoint(x: rect.origin.x,
y: rect.origin.y + halfHeight)
let halfSize = CGSize(width: rect.size.width,
height: halfHeight)
let halfRect = CGRect(origin: halfOrigin,
size: halfSize)
context.fill([halfRect])
}
context.restoreGState()
}
然后将其添加到
draw(_:)
:
layoutManager.drawBackground(forGlyphRange: range, at: CGPoint.zero)
drawUnderline(range: range) // Add a call to your new method
layoutManager.drawGlyphs(forGlyphRange: range, at: CGPoint.zero)
我明白了。
您可以调整您的精确目标。
下面是一个带有预览的完整示例:
import SwiftUI
import UIKit
struct ContentView: View {
let string: AttributedString
init() {
let attributes = AttributeContainer()
.font(.preferredFont(forTextStyle: .body))
string = AttributedString("Your Guide to Building a Balanced and Fulfilling Lifestyle", attributes: attributes)
}
var body: some View {
VStack {
TextKitView(text: string)
.padding()
Text(string)
}
.padding()
}
}
#Preview {
ContentView()
}
struct TextKitView: UIViewRepresentable {
var text: AttributedString
class CustomTextView: UIView {
private let textStorage = NSTextStorage()
private let layoutManager = NSLayoutManager()
private let textContainer: NSTextContainer
init(text: AttributedString, frame: CGRect) {
self.textContainer = NSTextContainer(size: frame.size)
self.textContainer.lineFragmentPadding = 0 // Better match Text
super.init(frame: frame)
// Setup TextKit components
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
// Set the text
textStorage.setAttributedString(NSAttributedString(text))
backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func drawUnderline(range: NSRange) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let color = CGColor(red: 233.0/255,
green: 217.0/255,
blue: 190.0/255,
alpha: 1)
context.setFillColor(color)
layoutManager.enumerateEnclosingRects(forGlyphRange: range,
withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0),
in: textContainer) { rect, _ in
let halfHeight = rect.size.height / 2
let halfOrigin = CGPoint(x: rect.origin.x,
y: rect.origin.y + halfHeight)
let halfSize = CGSize(width: rect.size.width,
height: halfHeight)
let halfRect = CGRect(origin: halfOrigin,
size: halfSize)
context.fill([halfRect])
}
context.restoreGState()
}
override func draw(_ rect: CGRect) {
let range = layoutManager.glyphRange(for: textContainer)
layoutManager.drawBackground(forGlyphRange: range, at: CGPoint.zero)
drawUnderline(range: range)
layoutManager.drawGlyphs(forGlyphRange: range, at: CGPoint.zero)
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
textContainer.size = size
layoutManager.ensureLayout(for: textContainer)
// Get the size of the content that fits the text container
let usedRect = layoutManager.usedRect(for: textContainer)
return CGSize(width: usedRect.width, height: usedRect.height)
}
}
func makeUIView(context: Context) -> CustomTextView {
CustomTextView(text: text, frame: .zero)
}
func updateUIView(_ uiView: CustomTextView, context: Context) {
uiView.setNeedsDisplay()
}
func sizeThatFits(_ proposal: ProposedViewSize,
uiView: CustomTextView,
context: Context) -> CGSize? {
uiView.sizeThatFits(proposal.replacingUnspecifiedDimensions())
}
}