Created
April 20, 2025 10:06
-
-
Save Koshimizu-Takehito/145e89f7f76bca2f46a81ed9b616d5b2 to your computer and use it in GitHub Desktop.
TextShape
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct ContentView: View { | |
var body: some View { | |
TextShape(sample) | |
.fill(.orange) | |
.stroke(.blue, lineWidth: 2) | |
} | |
var sample: AttributedString { | |
var text = AttributedString("Hello, world!") | |
text.font = UIFont(name: "HiraginoSans-W8", size: 50) | |
text.foregroundColor = .label | |
return text | |
} | |
} | |
struct TextShape: Shape { | |
var text: AttributedString | |
init(_ text: AttributedString) { | |
self.text = text | |
} | |
func path(in rect: CGRect) -> Path { | |
let font: UIFont = text.font ?? .systemFont(ofSize: UIFont.systemFontSize) | |
let ctFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil) | |
let attr = [NSAttributedString.Key.font: ctFont] | |
let line = CTLineCreateWithAttributedString( | |
NSAttributedString(string: String(text.characters), attributes: attr) | |
) | |
let runs = CTLineGetGlyphRuns(line) as! [CTRun] | |
let rawPath = CGMutablePath() | |
for run in runs { | |
let count = CTRunGetGlyphCount(run) | |
var glyphs = [CGGlyph](repeating: 0, count: count) | |
var positions = [CGPoint](repeating: .zero, count: count) | |
CTRunGetGlyphs(run, CFRange(location: 0, length: 0), &glyphs) | |
CTRunGetPositions(run, CFRange(location: 0, length: 0), &positions) | |
for i in 0..<count { | |
if let path = CTFontCreatePathForGlyph(ctFont, glyphs[i], nil) { | |
let t = CGAffineTransform(translationX: positions[i].x, y: positions[i].y) | |
rawPath.addPath(path, transform: t) | |
} | |
} | |
} | |
// CGPath を上下反転( iOS の座標系を考慮 ) | |
var flip = CGAffineTransform(scaleX: 1, y: -1) | |
let flipped = rawPath.copy(using: &flip)! | |
let b = flipped.boundingBox | |
let dx = (rect.width - b.width) / 2 - b.minX | |
let dy = (rect.height - b.height) / 2 - b.minY | |
var center = CGAffineTransform(translationX: dx, y: dy) | |
return flipped.copy(using: ¢er).map(Path.init) ?? Path() | |
} | |
} | |
#Preview { | |
ContentView() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment