I have the following code:
import SwiftUI
struct EntryHeaderIconView: View {
private let backgroundSize: Double = 88
private let color: Color
private let initials: String
init(color: Color,
initials: String = "") {
self.color = color
self.initials = initials
}
var body: some View {
ZStack(alignment: .center) {
Circle()
// .frame(width: backgroundSize,
// height: backgroundSize)
// Uncommenting this fixes the issue, but the text now clips
.foregroundColor(color)
icon
}
}
@ViewBuilder
private var icon: some View {
Text(verbatim: initials)
.font(.system(size: 48, weight: .bold, design: .rounded))
.foregroundColor(.white)
.accessibilityIdentifier("entry_header_initials")
}
}
struct ContentView: View {
var body: some View {
VStack {
EntryHeaderIconView(color: .red,
initials: "RT")
EntryHeaderIconView(color: .green,
initials: "LV")
EntryHeaderIconView(color: .red,
initials: "中国")
Spacer()
}
}
}
My goal is to have the Circle
element to fit perfectly around the Text
element. However, it either continues growing in the VStack
, so that it occupies as much space as possible and looks like this (sizing lines commented out):
Or, if I set a fixed size to the Circle
, the content gets clipped:
var body: some View {
ZStack(alignment: .center) {
Circle()
.frame(width: backgroundSize,
height: backgroundSize)
// Uncommenting this fixes the issue, but the text now clips
.foregroundColor(color)
icon
}
}
My goal is to make the Circle
size dependent on the Text
size, so that it grows (or shrinks) together with it.
Pretty easy to make with AutoLayout, how to achieve the same with SwiftUI?
I am going to post this as it is a different take on the other two answers, which are simpler and do work. This is a bit more flexible in handling different sizes, but still making them look consistent. This uses a PreferenceKey to read the size of the initials and restrict the circle to a certain size as a result.
struct EntryHeaderIconView: View {
@State var textViewSize = CGFloat.zero
// This gives a consistent additional size of the circle around the intitials
let circleSizeMultiplier = 1.5
// To give it a minimum size, just have the size introduced here
let minimumSize: CGfloat
var backgroundSize: CGFloat {
min(textViewSize * circleSizeMultiplier, minimumSize)
}
private let color: Color
private let initials: String
init(color: Color,
initials: String = "") {
self.color = color
self.initials = initials
}
var body: some View {
ZStack(alignment: .center) {
Circle()
// The frame is sized at the circleSizeMultiplier times the
// largest dimension of the initials.
.frame(width: backgroundSize
height: backgroundSize)
.foregroundColor(color)
icon
// This reads the size of the initials
.background(GeometryReader { geometry in
Color.clear.preference(
key: SizePreferenceKey.self,
value: geometry.size
)
})
}
// this sets backgroundSize to be the max value of the width or height
.onPreferenceChange(SizePreferenceKey.self) {
textViewSize = max($0.width, $0.height)
}
}
@ViewBuilder
private var icon: some View {
Text(verbatim: initials)
.font(.system(size: 48, weight: .bold, design: .rounded))
.foregroundColor(.white)
.accessibilityIdentifier("entry_header_initials")
}
}
// This is the actual preferenceKey that makes it work.
fileprivate struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
Edit:
Unless the icon is a SF Symbol, it will have to be handled differently. However, I have updated the code to add a minimumSize
constant and changed backgroundSize
to be a computed variable. This view is not set up to handle an image, but you would simply need to determine how you wanted to constrain the image, or do something like this.