如何在SwiftUI中实现文本描边?

30

我正在尝试在SwiftUI中实现文本描边或向我的文本添加边框,而不是在Text()项目中。

这是否可能?

我想用边框来实现这种效果: effect
(来源:noelshack.com)


2
你尝试过使用阴影吗?虽然不完全是你所要求的,但外观可能会相似。 - Michael Salmon
嗨,我尝试了4个阴影,x位置为1和-1,y位置也是如此(半径为0),这很好用。 - Nicolas M
9个回答

28
这里提供了一个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)
        }
    }
}

我建议使用加粗字重。它在适度大小的字体和笔画宽度下效果更好。对于较大的字号,您可能需要在更多角度上添加文本偏移来覆盖该区域。


1
我建议在所有8个方向上绘制轮廓以获得更平滑的结果。 我会添加另一个答案,但如果您修改您的答案,我也会很高兴。 - Mick
当笔画宽度大于字体宽度的三分之一时,这肯定会出现问题。 - Ky -

21

我发现了另一个创建描边的技巧,但只有在所需描边宽度不超过1时才有效。

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

我使用了shadow,但是请确保半径只有1,以获得相同的效果。


当在图像上叠加一些文本时,这真的很有帮助,可以让文本更加突出。 - Jeff Paquette
3
你还可以在其上堆叠更多的.shadow(),使效果更加强烈。尝试使用半径为0.4的十个阴影。 - Bryce
1
到目前为止,这是最好的解决方案,尤其是来自 @Bryce 的提示将阴影堆叠在彼此之上。 - yawnobleix
我很高兴回到这个。这个加上@Bryce的建议在多个层面上都非常有效。 - Jose Santos

14

我认为目前没有现成的方法来做到那一点。
到目前为止(beta 5),我们只能对形状应用笔画。

例如:

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

但是这种方法对于Text不可用。

UIViewRepresentable

另一种方法是通过UIViewRepresentable,使用经典的UIKit \ NSAttributedString来与SwiftUI配合使用。

操作如下:

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 应用程序。


1
非常感谢!自昨天起我在网上搜索,解决方案是使用UIKit,但我的Swift水平很差。我希望苹果在未来的SwiftUI中提供一种简单的方法来制作文本复杂效果。 - Nicolas M
当我将它更改为UITextField并在键盘上按返回时,这会导致应用程序冻结。我需要更改一些内容才能使其正常工作吗? - sfung3

9
这里有另一种方法,不需要覆盖 Text 对象的副本。适用于任何形状或视图。
extension View {
    func stroke(color: Color, width: CGFloat = 1) -> some View {
        modifier(StrokeModifer(strokeSize: width, strokeColor: color))
    }
}

struct StrokeModifer: ViewModifier {
    private let id = UUID()
    var strokeSize: CGFloat = 1
    var strokeColor: Color = .blue
    
    func body(content: Content) -> some View {
        if strokeSize > 0 {
            appliedStrokeBackground(content: content)
        } else {
            content
        }
    }
    
    private func appliedStrokeBackground(content: Content) -> some View {
        content
            .padding(strokeSize*2)
            .background(
                Rectangle()
                    .foregroundColor(strokeColor)
                    .mask(alignment: .center) {
                        mask(content: content)
                    }
            )
    }
    
    func mask(content: Content) -> some View {
        Canvas { context, size in
            context.addFilter(.alphaThreshold(min: 0.01))
            context.drawLayer { ctx in
                if let resolvedView = context.resolveSymbol(id: id) {
                    ctx.draw(resolvedView, at: .init(x: size.width/2, y: size.height/2))
                }
            }
        } symbols: {
            content
                .tag(id)
                .blur(radius: strokeSize)
        }
    }
}


这看起来是一个相当不错的解决方案,至少在文本上给出了期望的效果(尚未尝试其他形状),并且看起来不像使用重复叠加解决方案时可能会出现任何粗糙的边缘。我注意到的一件事是,如果你的strokeSize为零,你仍然会得到非常少量的描边,所以我在if let resolvedView =语句的开头添加了一个strokeSize > 0检查,这样就可以去除所有描边了。 - CMash
@CMash 谢谢!好建议,我编辑了答案以适应笔画大小为0的情况。还进行了一些重构,使其更清晰,说明了它是如何构建的。 - paescebu
是的,这看起来比我的快速破解要合理一些! - CMash
这个解决方案非常适用于没有背景的文本和PNG图像!这正是我一直在寻找的,非常感谢。 顺便说一下,我注意到如果我将其与矩形图像一起使用,它会有一点圆角,所以边框不是完全的矩形形状。 - Basel

5

您可以使用 SwiftFX 来完成此操作。


注:本文涉及 IT 技术。
import SwiftUI
import SwiftFX

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

这是Swift Package的地址:
.package(url: "https://github.com/hexagons/SwiftFX.git", from: "0.1.0")

设置说明在这里


1
我首先尝试了您的解决方案,因为它适用于Metal,所以我认为它比使用4个“Text”的Jose Santos的解决方案更好。 我有点失望...因为它的工作方式相同 :/ 我尝试使用.fxEdge(strength: .constant(1), distance: .constant(100))(故意极端)并得到了5个文本标签 :/ 很抱歉,由于Metal libs设置要求,我不会使用您的解决方案。 - Rémi B.
是的,设置有点烦人。我会尝试修复它,因为Swift Package管理器具有资源支持。不过说实话,我个人会选择backslash-f的View Representable方法。我希望SwiftUI有一天能够添加stroke修改器对Text进行支持。 - Heestand XYZ
我昨天花了几个小时来解决一个问题,今天早上终于完成了。这是6行纯SwiftUI代码,完美地运行着!我可以做任何事情(非常大的笔画、不透明的颜色...),当我在这里写我的长篇回答时,我清理了XCode的缓存,结果它就停止工作了。我很沮丧,因为有一件小事情不能像我想象的那样工作。(我认为它使用了Core Graphics,这会添加一个默认的黑色背景) - Rémi B.
编辑:如果我没记错的话,它在单色模式下运行得非常完美,并且在彩色笔画周围显示伪影。 - Rémi B.
1
我成功地让我的解决方案再次运行,并分享了它:https://dev59.com/5FMH5IYBdhLWcg3w_Wd_#67348676。我很快会添加图片。 - Rémi B.
显示剩余2条评论

3
我之前经常使用“偏移文本”解决方案,后来改用这种方法,并发现它的效果更好。并且,它还具有在不需要下载包的情况下实现带空心效果的轮廓文本的附加优势。
它的工作原理是通过堆叠.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()
        )
}

再次强调,这并不是一种“完美”的解决方案,但它是简单而有效的。


1
我喜欢它。但是我认为你可以通过只使用4x. shadow(..., x: [0,0,1,-1], y:[1,-1,0,0])来优化它,获得相同的结果和更好的性能(?)。 - thisIsTheFoxe
最好的建议是这个回应。它避免了ZStack(如果没有概述),并且足够快速。 - Vinzius

2

⚠️ 修改:清理了Xcode缓存后...它不再起作用,我找不到修复它的方法。

其他答案很好,但它们都有缺点(就我尝试过的):

  • 要么创建多个图层,需要复杂的计算(堆叠阴影)。
  • 要么使用覆盖技术,在四个版本的文本下面使用+x形状以获得更好的外观。当我尝试使用较大的描边时,标签变得可见,看起来非常糟糕。

在大多数情况下,无障碍性也没有得到适当处理。

我的原生SwiftUI解决方案

这就是我试图提出一个原生SwiftUI,非常简单但有效的解决方案的原因。

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

经过几个小时的尝试,我找到了一个8行代码的解决方案,我相信你会喜欢。 由于模糊是不透明的,所以它也是像素化的,我找不到避免这种情况的方法。另外,第二个drawingGroup添加了一个奇怪的圆形方框形状,我不知道为什么。

特点

特征 工作?
原生SwiftUI
自定义大小描边
像素大小描边 ❌(我不明白单位)
有颜色的描边
非不透明描边颜色
圆形描边
没有描边裁剪
完美的填充
原始文本颜色保留
无障碍性
没有像素化
适用于任何View
可读性强,有注释...

代码

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](https://dev59.com/5FMH5IYBdhLWcg3w_Wd_#67348676))
    @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
        }
    }

}

截图

在正常工作时,stroke的界面如下:

Working stroke

现在出现了故障,stroke的界面背景变成了黑色:

enter image description here


1
我建议在所有8个方向上绘制轮廓文本:
struct OutlinedText: View {
    var text: String
    var width: CGFloat
    var color: Color
    
    var body: some View {
        let diagonal: CGFloat = 1/sqrt(2) * width
        ZStack{
            ZStack{
                // bottom right
                Text(text).offset(x:  diagonal, y:  diagonal)
                // top left
                Text(text).offset(x: -diagonal, y: -diagonal)
                // bottom left
                Text(text).offset(x: -diagonal, y:  diagonal)
                // top right
                Text(text).offset(x:  diagonal, y: -diagonal)
                // left
                Text(text).offset(x:  -width, y: 0)
                // right
                Text(text).offset(x:  width, y: 0)
                // top
                Text(text).offset(x:  0, y: -width)
                // bottom
                Text(text).offset(x:  0, y: width)
            }
            .foregroundColor(color)
            Text(text)
        }.padding()
    }
}

0

.shadow()修饰符可以迭代调用,从而创建描边效果。只需创建此修饰符并将其添加到您的视图中即可。

import SwiftUI

struct StrokeStyle: ViewModifier {

    var color: Color
    var lineWidth: Int

    func body(content: Content) -> some View {
        applyShadow(
            content: AnyView(content),
            lineWidth: lineWidth
        )
    }

    func applyShadow(content: AnyView, lineWidth: Int) -> AnyView {
        if lineWidth == 0 {
            return content
        } else {
            return applyShadow(
                content: AnyView(
                    content.shadow(
                        color: color,
                        radius: 1
                    )
                ),
                lineWidth: lineWidth - 1
            )
        }
    }
}

extension View {
    func strokeBorder(color: Color, lineWidth: Int) -> some View {
        self.modifier(LHSStrokeStyle(color: color, lineWidth: lineWidth))
    }
}

目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接