Skip to content

Instantly share code, notes, and snippets.

@auramagi
Last active June 27, 2022 11:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save auramagi/82a05d81146507180ed1f9111bc2e37a to your computer and use it in GitHub Desktop.
Save auramagi/82a05d81146507180ed1f9111bc2e37a to your computer and use it in GitHub Desktop.
Use Core Text in SwiftUI
import SwiftUI
struct ContentView: View {
var body: some View {
CoreTextShape(
string: "Hello YUMEMI.swift!",
font: .boldSystemFont(ofSize: 42)
)
.border(.purple)
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CoreTextShape: Shape {
let string: String
let font: UIFont
func path(in rect: CGRect) -> Path {
.init(attributedString.path(in: rect))
}
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
let constraints = proposal.replacingUnspecifiedDimensions(by: CGSize(
width: CGFloat.greatestFiniteMagnitude, // greatestFiniteMagnitude means unconstrained size for CTFramesetterSuggestFrameSizeWithConstraints
height: CGFloat.greatestFiniteMagnitude
))
return attributedString.suggestedFrame(constraints: constraints)
}
private var attributedString: NSAttributedString {
.init(
string: string,
attributes: [
.font: font,
.paragraphStyle: paragraphStyle,
]
)
}
private var paragraphStyle: NSParagraphStyle {
let style = NSMutableParagraphStyle()
style.alignment = .center
return style
}
}
extension NSAttributedString {
func suggestedFrame(constraints: CGSize) -> CGSize {
let framesetter = CTFramesetterCreateWithAttributedString(self)
let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, .init(), nil, constraints, nil)
// SwiftUI adheres to the pixel grid, so we round up the suggested size
return .init(
width: ceil(frameSize.width),
height: ceil(frameSize.height)
)
}
func path(in bounds: CGRect) -> CGPath {
let textPath = CGMutablePath()
let framesetter = CTFramesetterCreateWithAttributedString(self)
let frame = CTFramesetterCreateFrame(framesetter, CFRange(), .init(rect: bounds, transform: nil), nil)
let lines = CTFrameGetLines(frame) as? [CTLine] ?? []
let lineOrigins = [CGPoint](unsafeUninitializedCapacity: lines.count) { buffer, initializedCount in
CTFrameGetLineOrigins(frame, .init(), buffer.baseAddress!)
initializedCount = lines.count
}
for (line, origin) in zip(lines, lineOrigins) {
for run in CTLineGetGlyphRuns(line) as? [CTRun] ?? [] {
let attributes = CTRunGetAttributes(run) as NSDictionary
let font = attributes[kCTFontAttributeName as String]
guard CFGetTypeID(font as CFTypeRef) == CTFontGetTypeID() else { continue }
let glyphsCount = CTRunGetGlyphCount(run)
let glyphs = [CGGlyph](unsafeUninitializedCapacity: glyphsCount) { buffer, initializedCount in
CTRunGetGlyphs(run, .init(), buffer.baseAddress!)
initializedCount = glyphsCount
}
let positions = [CGPoint](unsafeUninitializedCapacity: glyphsCount) { buffer, initializedCount in
CTRunGetPositions(run, .init(), buffer.baseAddress!)
initializedCount = glyphsCount
}
zip(glyphs, positions).forEach { (glyph, position) in
guard let glyphPath = CTFontCreatePathForGlyph(font as! CTFont, glyph, nil) else { return }
textPath.addPath(
glyphPath,
transform: .init(
translationX: position.x + origin.x + bounds.origin.x,
y: position.y + origin.y - bounds.origin.y
)
)
}
}
}
var transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -bounds.height)
return textPath.copy(using: &transform)!
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment