如何在 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
}
}
}
截图
当它还在工作时,行程是这样的:
现在坏了,笔划黑底了:
我正在尝试在 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
}
}
}
截图
当它还在工作时,行程是这样的:
现在坏了,笔划黑底了: