Richard Topchii
Richard Topchii

Reputation: 8215

SwiftUI: Make Circle "fit" the Text element

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):

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
        }
    }

clipped

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?

Upvotes: 2

Views: 2776

Answers (4)

blackjacx
blackjacx

Reputation: 10520

Swift 5.10 tested

Here is another apporach that keeps the padding around the text even when font size increases using Dynamic Type:

// MARK: - Environments

@SwiftUI.Environment(\.dynamicTypeSize) var dynamicTypeSize

// MARK: - States

@State private var circleSize = CGFloat.zero

// MARK: - Stored Properties

var text: String
var textColor: Color
var font: Font
var fillColor: Color = .clear
var strokeColor: Color = .clear
var strokeWidth: CGFloat = 0
var padding: CGFloat

// MARK: - Body

var body: some View {
    ZStack(alignment: .center) {
        Circle()
            .strokeBorder(strokeColor, lineWidth: strokeWidth)
            .background(Circle().foregroundColor(fillColor))
            .frame(width: circleSize, height: circleSize)
        Text(text)
            .font(font)
            .foregroundColor(textColor)
            .padding(padding)
            .background(
                GeometryReader { geometry in
                    Color.clear.onAppear {
                        circleSize = geometry.frame(in: .local).size.max
                    }
                    .onChange(of: dynamicTypeSize) { _ in
                        // Re-render when font size changed.
                        circleSize = geometry.frame(in: .local).size.max
                    }
                }
            )
    }
}

Upvotes: 1

Yrb
Yrb

Reputation: 9755

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.

Upvotes: 0

HunterLion
HunterLion

Reputation: 4016

You need to apply a padding to the text and then show a background in the shape of a circle.

Here's how you can achieve that:

struct Example: View {
    
    var body: some View {
            VStack {
                Text("1")
                    .font(.footnote)
                    .foregroundColor(.white)
                    .padding(5)
                    .background(.red)
                    .clipShape(Circle())
                Text("2")
                    .font(.headline)
                    .foregroundColor(.white)
                    .padding(5)
                    .background(.red)
                    .clipShape(Circle())
                Text("3")
                    .font(.largeTitle)
                    .foregroundColor(.white)
                    .padding(5)
                    .background(.red)
                    .clipShape(Circle())
            }
        
    }
}

enter image description here

Upvotes: 4

David Pasztor
David Pasztor

Reputation: 54785

A ZStack takes up as much space as its child views need. You aren't providing an explicit frame to the Circle, so how could SwiftUI know that the Circle's size should match that of the icon?

Instead, you should add the Circle as a background to your icon, after applying some padding to the icon.

struct EntryHeaderIconView: View {
    private let color: Color
    private let initials: String

    init(color: Color,
         initials: String = "") {
        self.color = color
        self.initials = initials
    }

    var body: some View {
        icon
            .padding(.all, 30)
            .background(background)
    }

    private var background: some View {
        Circle()
            .foregroundColor(color)
    }

    private var icon: some View {
        Text(verbatim: initials)
            .font(.system(size: 48, weight: .bold, design: .rounded))
            .foregroundColor(.white)
            .accessibilityIdentifier("entry_header_initials")
    }
}

struct EntryHeaderIconView_Preview: PreviewProvider {
    static var previews: some View {
        VStack {
            EntryHeaderIconView(color: .red,
                                initials: "RT")

            EntryHeaderIconView(color: .green,
                                initials: "LV")

            EntryHeaderIconView(color: .red,
                                initials: "中国")
        }
    }
}

Upvotes: 2

Related Questions