How one can 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 consequence 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 = Coloration.black
@State non-public var strokeColor = Coloration.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: "Hi there, 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("Heart").tag(NSTextAlignment.middle)
Textual content("Proper").tag(NSTextAlignment.proper)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.high, 5)
// Coloration Pickers
HStack {
VStack {
Textual content("Font Coloration")
ColorPicker("", choice: $fontColor)
.body(width: 50)
}
VStack {
Textual content("Stroke Coloration")
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,
coloration: fontColor,
strokeColor: strokeColor,
strokeWidth: strokeWidth,
alignment: alignment,
letterSpacing: letterSpacing,
isBold: isBold,
isItalic: isItalic,
isUnderlined: isUnderlined,
isCurved: isCurved,
curveRadius: curveRadius
) {
ZStack {
// Fill the whole textual content path with the chosen font coloration.
Path(path)
.fill(Coloration(fontColor))
// Then stroke the trail with the chosen stroke coloration.
Path(path)
.stroke(Coloration(strokeColor), lineWidth: strokeWidth)
}
.body(width: geometry.measurement.width, peak: geometry.measurement.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,
coloration: 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, measurement: 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 one of the best ways to align and remodel the generated textual content path appropriately in SwiftUI?