如何在 SwiftUI 中制作文字描边?

How to make text stroke in SwiftUI?

我正在尝试在 SwiftUI 中创建文字描边或在我的文字上添加边框,但字母不是 Text() 项目。

可能吗?

我想用边框做这个效果:
(来源:noelshack.com

我认为没有办法做到这一点 "out of the box"。
到目前为止(测试版 5)我们只能将笔画应用于 Shapes

例如:

struct SomeView: View {
    var body: some View {
        Circle().stroke(Color.red)
    }
}

但同样不适用于 Text

UIViewRepresentable

另一种方法是通过 UIViewRepresentable 在 SwiftUI 中使用好的 ol' UIKit \ NSAttributedString

像这样:

import SwiftUI
import UIKit

struct SomeView: View {
    var body: some View {
        StrokeTextLabel()
    }
}

struct StrokeTextLabel: UIViewRepresentable {
    func makeUIView(context: Context) -> UILabel {
        let attributedStringParagraphStyle = NSMutableParagraphStyle()
        attributedStringParagraphStyle.alignment = NSTextAlignment.center
        let attributedString = NSAttributedString(
            string: "Classic",
            attributes:[
                NSAttributedString.Key.paragraphStyle: attributedStringParagraphStyle,
                NSAttributedString.Key.strokeWidth: 3.0,
                NSAttributedString.Key.foregroundColor: UIColor.black,
                NSAttributedString.Key.strokeColor: UIColor.black,
                NSAttributedString.Key.font: UIFont(name:"Helvetica", size:30.0)!
            ]
        )

        let strokeLabel = UILabel(frame: CGRect.zero)
        strokeLabel.attributedText = attributedString
        strokeLabel.backgroundColor = UIColor.clear
        strokeLabel.sizeToFit()
        strokeLabel.center = CGPoint.init(x: 0.0, y: 0.0)
        return strokeLabel
    }

    func updateUIView(_ uiView: UILabel, context: Context) {}
}

#if DEBUG
struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        SomeView()
    }
}
#endif

Result

当然,您必须调整 NSAttributedString 的属性(大小、字体、颜色等)才能生成所需的输出。为此,我会推荐 Visual Attributed String macOS 应用程序。

这是一个 100% SwiftUI 解决方案。不完美,但它可以工作,它使您可以完全 SwiftUI 控制结果视图。

import SwiftUI

struct SomeView: View {
    var body: some View {
        StrokeText(text: "Sample Text", width: 0.5, color: .red)
            .foregroundColor(.black)
            .font(.system(size: 12, weight: .bold))

    }
}

struct StrokeText: View {
    let text: String
    let width: CGFloat
    let color: Color

    var body: some View {
        ZStack{
            ZStack{
                Text(text).offset(x:  width, y:  width)
                Text(text).offset(x: -width, y: -width)
                Text(text).offset(x: -width, y:  width)
                Text(text).offset(x:  width, y: -width)
            }
            .foregroundColor(color)
            Text(text)
        }
    }
}

我建议使用粗体。它适用于合理大小的字体和笔画宽度。对于更大的尺寸,您可能需要添加更多角度的文本偏移以覆盖该区域。

您可以使用 SwiftFX

import SwiftUI
import SwiftFX

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .fxEdge()
    }
}

这是 Swift 包裹:

.package(url: "https://github.com/hexagons/SwiftFX.git", from: "0.1.0")

设置说明here

我找到了另一个创建笔画的技巧,但只有当你想要的笔画宽度不超过 1 时才有效

Text("Hello World")
   .shadow(color: .black, radius: 1)

我用的是shadow,但要确保半径刚好为1,才能达到同样的效果

在改为使用此解决方案之前,我使用了 'offset' 文本解决方案相当多,并且发现它的效果要好得多。并且它还有一个额外的好处,即允许内部空心的轮廓文本而无需下载包以获得简单的效果。

它的工作原理是堆叠 .shadow 并保持较低的半径以在对象周围创建一条实线。如果您想要更粗的边框,则需要向扩展添加更多 .shadow 修饰符,但对于我所有的文本需求,这确实做得很好。另外,它也适用于图片。

它并不完美,但我喜欢简单的解决方案,这些解决方案停留在 SwifUI 领域并且可以轻松实施。

最后,outline Bool 参数应用了一个倒置掩码(SwiftUI 缺少​​的其他东西),我也提供了该扩展。

extension View {
@ViewBuilder
func viewBorder(color: Color = .black, radius: CGFloat = 0.4, outline: Bool = false) -> some View {
    if outline {
        self
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .invertedMask(
                self
            )
    } else {
        self
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
            .shadow(color: color, radius: radius)
    }

}

}

extension View {
func invertedMask<Content : View>(_ content: Content) -> some View {
    self
        .mask(
            ZStack {
                self
                    .brightness(1)
                content
                    .brightness(-1)
            }.compositingGroup()
            .luminanceToAlpha()
        )
}

}

同样,这不是 'perfect' 解决方案,但它是一个简单有效的解决方案。

⚠️ 编辑:清理 Xcode 缓存后……它不再工作了,我找不到修复它的方法。

其他答案很好,但也有缺点(对于我试过的那些):

  • 要么他们创建了很多层,需要复杂的计算(堆叠阴影)。
  • 要么他们使用覆盖技术,下面有四个版本的文本(在 +x 形状中以获得更好的外观)。当我尝试更大的笔触时,标签变得可见,而且看起来很糟糕。

在大多数情况下,辅助功能也没有得到妥善处理。

我的香草 SwiftUI 解决方案

这就是为什么我试图想出一个 vanilla SwiftUI,非常简单但有效的解决方案

我的主要想法是使用 .blur(radius: radius, opaque: true) 来进行完美的描边。

在玩弄所有修改器几个小时后,我找到了一个 8 行解决方案,我相信你会喜欢它的。 由于模糊是不透明,它也是像素化,我找不到避免这种情况的方法。还有,第二个drawingGroup加了一个奇怪的圆角方形,我也不知道为什么。

特点

Feature Working?
Vanilla SwiftUI
Custom size stroke
Pixel size stroke ❌ (I don't understand the unit)
Colored stroke
Non-opaque stoke color
Rounded stroke
No stroke clipping
Perfect padding
Original text color conservation
Accessibility
No pixelation
Works with any View
Readable, commented…

代码

extension View {

    /// Adds a stroke around the text. This method uses an opaque blur, hence the `radius` parameter.
    ///
    /// - Parameters:
    ///   - color: The stroke color. Can be non-opaque.
    ///   - radius: The blur radius. The value is not in pixels or points.
    ///             You need to try values by hand.
    /// - Warning:
    ///   - The opaque blur is pixelated, I couldn't find a way to avoid this.
    ///   - The second `drawingGroup` allows stroke opacity, but adds a
    ///     strange rounded square shape.
    ///
    /// # Example
    ///
    /// ```
    /// Text("Lorem ipsum")
    ///     .foregroundColor(.red)
    ///     .font(.system(size: 20, weight: .bold, design: .rounded))
    ///     .stroked(color: Color.blue.opacity(0.5), radius: 0.5)
    /// ```
    ///
    /// # Copyright
    ///
    /// CC BY-SA 4.0 [Rémi BARDON](https://github.com/RemiBardon)
    /// (posted on [Stack Overflow](
    @ViewBuilder
    public func stroked(color: Color, radius: CGFloat) -> some View {
        ZStack {
            self
                // Add padding to avoid clipping
                // (3 is a a number I found when trying values… it comes from nowhere)
                .padding(3*radius)
                // Apply padding
                .drawingGroup()
                // Remove any color from the text
                .colorMultiply(.black)
                // Add an opaque blur around the text
                .blur(radius: radius, opaque: true)
                // Remove black background and allow color with opacity
                .drawingGroup()
                // Invert the black blur to get a white blur
                .colorInvert()
                // Multiply white by whatever color
                .colorMultiply(color)
                // Disable accessibility for background text
                .accessibility(hidden: true)
            self
        }
    }

}

截图

当它还在工作时,行程是这样的:

现在坏了,笔划黑底了: