I'm trying to make text-stroke in SwiftUI or add a border on my text, in the letters not the Text()
item.
Is it possible?
I want to make this effect with the border:
(source: noelshack.com)
Tip: Use stroke() or strokeBorder() with shapes, and border() with other view types.
Hold the command key and click the text to bring up a pop-over menu. Choose Show SwiftUI Inspector and then you can edit the text/font properties.
Here is a 100% SwiftUI solution. Not perfect, but it works and it gives you full SwiftUI control of the resulting view.
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)
}
}
}
I suggest using bold weight. It works better with reasonably sized fonts and stroke widths. For larger sizes, you may have to add Text offsets in more angles to cover the area.
I don't think there's a way for doing that "out of the box".
So far (beta 5) we can apply strokes to Shapes
only.
For example:
struct SomeView: View {
var body: some View {
Circle().stroke(Color.red)
}
}
But again that isn’t available for Text
.
UIViewRepresentable
Another approach would be to use the good ol' UIKit
\ NSAttributedString
with SwiftUI via UIViewRepresentable
.
Like so:
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
Of course you have to tweak the attributes (size, font, color, etc) of the NSAttributedString
to generate the desired output. For that I would recommend the Visual Attributed String macOS app.
I find another trick to create stroke, but it only works if your desired stroke width is not more than 1
Text("Hello World")
.shadow(color: .black, radius: 1)
I used shadow
, but make sure the radius is just 1, to get the same efffect
You can do this with SwiftFX
import SwiftUI
import SwiftFX
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.fxEdge()
}
}
Here's the Swift Package:
.package(url: "https://github.com/hexagons/SwiftFX.git", from: "0.1.0")
Setup instructions here.
I used the 'offset' text solution quite a bit before changing to using this instead and have found that it works a ton better. AND it has the added benefit of allowing outline text with a hollow inside WITHOUT needing to download a package just to get a simple effect.
It works by stacking .shadow and keeping the radius low to create a solid line around the object. if you want a thicker border, you will need to add more .shadow modifiers to the extension, but for all my text needs, this has done really well. Plus, it works on pictures as well.
It's not perfect, but I am a fan of simple solutions that stay in the realm of SwifUI and can be easily implemented.
Finally, the outline Bool parameter applies an inverted mask(something else SwiftUI lacks) and I have provided that extension as well.
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()
)
}
}
Again, not a 'perfect' solution, but it is a simple and effective one.
⚠️ Edit: After cleaning Xcode cache… it doesn't work anymore 😕 I couldn't find a way to fix it.
+
or x
shape for better look). When I tried having a larger stroke, the labels became visible, and it was looking very bad.In most cases, accessibility is not handled properly either.
That's why I tried to come up with a vanilla SwiftUI, really simple but effective solution.
My main idea was to use .blur(radius: radius, opaque: true)
to have a perfect stroke around.
After hours of playing with all the modifiers, I found a 8-line solution, and I'm sure you'll love it. As the blur is opaque, it's also pixelated, I couldn't find a way to avoid this. Also, the second drawingGroup
adds a strange rounded square shape, I don't know why.
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](https://stackoverflow.com/a/67348676/10967642))
@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
}
}
}
When it was still working, stroke looked like this:
Now it's broken, the stroke has a black background:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With