You'd need to generate a set of Path instances from the characters in your text. This is only really suitable for small amounts, e.g. the "Welcome" animation or clock digits, etc., because it's not the most performant thing. Also, bigger text is better. Lastly, remember to wrap it all in a GlassEffectContainer to avoid each character's glass being rendered in a discrete pass.
Actually lastly: you'll need to adjust the location of the text using .offset(x:y:) or by tweaking the path creation to include better offset translations. This is my quick & dirty attempt.
Since .glassEffect() is designed similarly to a fill API, you need something non-empty to 'fill' with your text. Color.clear gets elided by the rendering engine, but what I've found is that Color.white.opacity(0) works to cause an actual (invisible) view to render and take up space during layout calculations.
And now: the code! This was written for tvOS, so it uses BIG fonts and offsets to write big numbers. I had it draw a counter that increments each second, to see what the animations did with the character paths. Wasn't quite what I was looking for in the end (character paths are complicated and likely need custom inter-character path mutation to get the points moving in the right directions), but you might find this a useful jumping-off point.
import SwiftUI
import CoreText
private func textToPath(_ text: String, font: Font, in context: Font.Context) -> [Path] {
let ctFont = font.resolve(in: context).ctFont
var attrStr = AttributedString(text)
attrStr[AttributeScopes.UIKitAttributes.FontAttribute.self] = ctFont
let nsString = NSAttributedString(attrStr)
let line = CTLineCreateWithAttributedString(nsString as CFAttributedString)
let runs = CTLineGetGlyphRuns(line) as! [CTRun]
var paths = [Path]()
for run in runs {
let attrs = CTRunGetAttributes(run) as! [CFString: Any]
let runFont = attrs[kCTFontAttributeName] as! CTFont
let glyphs = UnsafeBufferPointer(
start: CTRunGetGlyphsPtr(run), count: CTRunGetGlyphCount(run))
let positions = UnsafeBufferPointer(
start: CTRunGetPositionsPtr(run), count: CTRunGetGlyphCount(run))
for (glyph, position) in zip(glyphs, positions) {
if let path = CTFontCreatePathForGlyph(runFont, glyph, nil) {
var transform = CGAffineTransform(translationX: position.x, y: 0)
.scaledBy(x: 1, y: -1)
if let p = path.mutableCopy(using: &transform) {
paths.append(.init(p))
}
}
}
}
return paths
}
struct TextFill: View {
@Environment(\.fontResolutionContext) var fontContext
@Namespace var namespace
@State var counter: Int = 0
var rawPaths: [Path] {
textToPath(
counter.formatted(.number.grouping(.never).precision(.integerAndFractionLength(integer: 4, fraction: 0))),
font: .system(size: 260, weight: .medium, design: .rounded),
in: fontContext)
}
var paths: [(Int, Path)] {
return Array(zip(1..., rawPaths))
}
var path: Path {
var path = Path()
for rawPath in rawPaths {
path.addPath(rawPath)
}
return path
}
var body: some View {
GlassEffectContainer {
ZStack(alignment: .center) {
Image("InsertBackgroundImageHere")
.resizable()
.ignoresSafeArea()
Color.white.opacity(0)
.glassEffect(in: path)
.glassEffectTransition(.matchedGeometry)
.glassEffectID(10, in: namespace)
.offset(x: 340, y: 400)
.task {
self.counter = 0
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(1))
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
self.counter += 1
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
I wrote this during the betas, but I'm at least 85% sure it should still work.