有没有办法在 SwiftUI 中创建自定义下划线?

问题描述 投票:0回答:1

我的目标是实现屏幕截图所示的 UI。简而言之,我有一个可以超过 1 行的文本视图,我需要添加类似于下划线的内容,它是单个文本行的一半粗,并且有一些垂直偏移。 Desired UI Screenshot

首先想到的是使用 .underline(),但是它不能让您自定义行高和位置。 下一个想法是使用 .overlay,但在这种情况下,背景应用于整个视图(可以超过 1 行),而我需要将它单独应用于每一行。

P.s. - 不能将文本拆分为两个或多个视图,因为字符串是动态的。

ios swift swiftui
1个回答
0
投票

我不相信这可以直接在 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)

我明白了。

Image of text with described

您可以调整您的精确目标。

下面是一个带有预览的完整示例:

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())
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.