Methods to Convert SwiftUI Textual content to a Path and Apply Transformations?
Physique:
I’m making an attempt to render a Textual content view as a path in SwiftUI in order that I can apply transformations like scaling, skewing, and rotation. I’ve managed to extract the glyphs utilizing Textual content.toPath(), however I’m dealing with points with making use of transformations and rendering the end result correctly.
Right here’s my present strategy:
//
// TextToPathView.swift
import SwiftUI
import CoreText
struct TextToPathView: View {
@State non-public var fontSize: CGFloat = 40
@State non-public var strokeWidth: CGFloat = 2
@State non-public var letterSpacing: CGFloat = 2
@State non-public var curveRadius: CGFloat = 0
@State non-public var isBold = false
@State non-public var isItalic = false
@State non-public var isUnderlined = false
@State non-public var isCurved = false
@State non-public var fontColor = Colour.black
@State non-public var strokeColor = Colour.purple
@State non-public var alignment: NSTextAlignment = .middle
var physique: some View {
VStack {
Textual content("Textual content to Path Editor")
.font(.title)
.fontWeight(.daring)
.padding(.backside, 10)
// Use the unified PathView that creates one CGPath for all results.
PathView(
textual content: "Good day, SwiftUI!",
fontSize: fontSize,
strokeWidth: strokeWidth,
letterSpacing: letterSpacing,
curveRadius: curveRadius,
isBold: isBold,
isItalic: isItalic,
isUnderlined: isUnderlined,
isCurved: isCurved,
fontColor: UIColor(fontColor),
strokeColor: UIColor(strokeColor),
alignment: alignment
)
.body(peak: 200)
.padding()
Divider().padding()
// Sliders
VStack {
SliderView(worth: $fontSize, label: "Font Dimension", vary: 20...100)
SliderView(worth: $strokeWidth, label: "Stroke Width", vary: 0...5)
SliderView(worth: $letterSpacing, label: "Letter Spacing", vary: 0...10)
if isCurved {
SliderView(worth: $curveRadius, label: "Curve Radius", vary: 50...200)
}
}
// Toggles
HStack {
Toggle("Daring", isOn: $isBold)
Toggle("Italic", isOn: $isItalic)
Toggle("Underline", isOn: $isUnderlined)
}
.padding(.high, 10)
Toggle("Curved Textual content", isOn: $isCurved)
.padding(.high, 5)
// Alignment Picker
Picker("Alignment", choice: $alignment) {
Textual content("Left").tag(NSTextAlignment.left)
Textual content("Middle").tag(NSTextAlignment.middle)
Textual content("Proper").tag(NSTextAlignment.proper)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.high, 5)
// Colour Pickers
HStack {
VStack {
Textual content("Font Colour")
ColorPicker("", choice: $fontColor)
.body(width: 50)
}
VStack {
Textual content("Stroke Colour")
ColorPicker("", choice: $strokeColor)
.body(width: 50)
}
}
.padding(.high, 10)
}
.padding()
}
}
// MARK: - PathView (Creates one CGPath reflecting all results)
struct PathView: View {
var textual content: String
var fontSize: CGFloat
var strokeWidth: CGFloat
var letterSpacing: CGFloat
var curveRadius: CGFloat
var isBold: Bool
var isItalic: Bool
var isUnderlined: Bool
var isCurved: Bool
var fontColor: UIColor
var strokeColor: UIColor
var alignment: NSTextAlignment
var physique: some View {
GeometryReader { geometry in
if let path = styledTextToPath(
textual content: textual content,
font: UIFont.systemFont(ofSize: fontSize),
fontSize: fontSize,
colour: fontColor,
strokeColor: strokeColor,
strokeWidth: strokeWidth,
alignment: alignment,
letterSpacing: letterSpacing,
isBold: isBold,
isItalic: isItalic,
isUnderlined: isUnderlined,
isCurved: isCurved,
curveRadius: curveRadius
) {
ZStack {
// Fill the entire textual content path with the chosen font colour.
Path(path)
.fill(Colour(fontColor))
// Then stroke the trail with the chosen stroke colour.
Path(path)
.stroke(Colour(strokeColor), lineWidth: strokeWidth)
}
.body(width: geometry.dimension.width, peak: geometry.dimension.peak)
// Modify vertical offset as wanted.
.offset(y: 50)
}
}
}
}
// MARK: - Create a CGPath that displays all styling results.
func styledTextToPath(
textual content: String,
font: UIFont,
fontSize: CGFloat,
colour: UIColor,
strokeColor: UIColor,
strokeWidth: CGFloat,
alignment: NSTextAlignment,
letterSpacing: CGFloat,
isBold: Bool,
isItalic: Bool,
isUnderlined: Bool,
isCurved: Bool,
curveRadius: CGFloat
) -> CGPath? {
// Decide the font traits.
var traits: UIFontDescriptor.SymbolicTraits = []
if isBold { traits.insert(.traitBold) }
if isItalic { traits.insert(.traitItalic) }
var styledFont = font
if let descriptor = font.fontDescriptor.withSymbolicTraits(traits) {
styledFont = UIFont(descriptor: descriptor, dimension: fontSize)
}
// Construct widespread attributes.
let attributes: [NSAttributedString.Key: Any] = [
.font: styledFont,
.foregroundColor: color,
.kern: letterSpacing,
.strokeColor: strokeColor,
.strokeWidth: strokeWidth
]
// If not curved, use a easy strategy.
if !isCurved {
let attributedString = NSAttributedString(string: textual content, attributes: attributes)
let line = CTLineCreateWithAttributedString(attributedString)
let totalWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
let runArray = CTLineGetGlyphRuns(line) as NSArray
let path = CGMutablePath()
for run in runArray {
let run = run as! CTRun
let rely = CTRunGetGlyphCount(run)
for index in 0..
var physique: some View {
VStack {
Textual content("(label): (Int(worth))")
Slider(worth: $worth, in: vary)
}
.padding(.vertical, 5)
}
}
// MARK: - Preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
TextToPathView()
}
}
Points:
-
The textual content path doesn’t align correctly within the body.
-
Transformations like scaling and skewing don’t appear to work as anticipated.
-
The glyphs seem shifted when utilizing CGAffineTransform.
What’s the easiest way to align and remodel the generated textual content path accurately in SwiftUI?