Fonts and Bounds
With `NSAttributedString` one can calculate the bounding box of an attributed string when it is rendered. In the example a `UILabel` is used to show the `NSAttributedString` and a `Rectangle` with the size of the string's bounding box is layered under the label.
To use the `UILabel` in SwiftUI, it is wrapped in a `UIViewRepresentable`. A `Coordinator` is used keep track of the label instance for subsequent updates.
import SwiftUI
struct SampleFontView: View {
let attributedText: NSAttributedString = {
let str = NSMutableAttributedString(
string: """
The bounds of a
rendered string can
be determined
using sizeThatFits
""")
str.addAttribute(.font, value: UIFont.italicSystemFont(ofSize: 22), range: NSRange(location: 0, length: str.length))
return str
}()
var body: some View {
VStack(alignment: .center, spacing: 16) {
Text("Fonts & Bounds")
.font(.largeTitle)
GeometryReader { geo in
let width: CGFloat = geo.size.width - 40
let textSize = AttributedLabel.boundingSize(for: attributedText, width: width)
VStack(alignment: .center) {
Spacer()
VStack(spacing: 0) {
ZStack {
Rectangle()
.stroke(Color.red, lineWidth: 1)
.frame(width: textSize.width, height: textSize.height)
AttributedLabel(attributedText: attributedText)
.border(Color.blue, width: 1)
}
.padding()
Text("Box of AttributedLabel")
.foregroundStyle(Color.blue)
.font(.footnote)
Text("Calculated Size of String: \(Int(textSize.width)) × \(Int(textSize.height))")
.font(.footnote)
.foregroundColor(.gray)
}
.padding(4)
.clipShape(RoundedRectangle(cornerRadius: 8))
.background {
Color.white
.clipShape(RoundedRectangle(cornerRadius: 8))
.shadow(radius: 4)
}
.frame(width: 300, height: 300)
Spacer()
}
.frame(maxWidth: .infinity)
}
.background {
Color(white: 0.95)
.ignoresSafeArea(edges: [.leading, .trailing, .bottom])
}
}
}
struct AttributedLabel: UIViewRepresentable {
let attributedText: NSAttributedString
func makeCoordinator() -> C {
return C()
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
let label = UILabel()
context.coordinator.label = label
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
let cons: [NSLayoutConstraint] = [
view.centerXAnchor.constraint(equalTo: label.centerXAnchor),
view.leadingAnchor.constraint(lessThanOrEqualTo: label.leadingAnchor),
view.trailingAnchor.constraint(greaterThanOrEqualTo: label.trailingAnchor),
view.topAnchor.constraint(equalTo: label.topAnchor),
view.bottomAnchor.constraint(equalTo: label.bottomAnchor),
]
NSLayoutConstraint.activate(cons)
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
context.coordinator.label?.attributedText = attributedText
}
static func boundingSize(for attributedText: NSAttributedString, width: CGFloat) -> CGSize {
let label = UILabel()
label.numberOfLines = 0
label.attributedText = attributedText
let size = label.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
return size
}
}
class C {
var label: UILabel?
}
}
#Preview {
SampleFontView()
}