5

Is there a way to make all symbols in the same size have the same width?

As you can see from the screenshot below, person.2.fill has the longest width and square.and.arrow.up is the nearest. Setting the font to a mono-spaced font does not seem to change anything in this case.

Image(systemName: "square.and.arrow.up")
    .font(Font.system(size: 18, weight: .medium, design: .monospaced))

A workaround would be not "forcing" them to have the same width but horizontally centered inside the same width container.

enter image description here

XY L
  • 25,431
  • 14
  • 84
  • 143

3 Answers3

3

I fixed this using PreferenceKey, also work well with dynamic font sizing.

Test in Xcode13.2 Swift5.5

struct ContentView: View {

    @State private var iconWidth: Double = 0

    private var icons: [String] = ["person.crop.circle", "tag", "text.book.closed", "icloud.and.arrow.up"]

    var body: some View {
        List(icons, id: \.self) { iconName in
            HStack {
                Image(systemName: iconName)
                    .sync(with: $iconWidth)
                    .frame(width: iconWidth)

                Text(iconName)
            }
            .onPreferenceChange(SymbolWidthPreferenceKey.self) { iconWidth = $0 }
        }
    }
}

struct SymbolWidthPreferenceKey: PreferenceKey {

    static var defaultValue: Double = 0

    static func reduce(value: inout Double, nextValue: () -> Double) {
        value = max(value, nextValue())
    }
}

struct SymbolWidthModifier: ViewModifier {

    @Binding var width: Double

    func body(content: Content) -> some View {
        content
            .background(GeometryReader { geo in
                Color
                    .clear
                    .preference(key: SymbolWidthPreferenceKey.self, value: geo.size.width)
            })
    }
}

extension Image {

    func sync(with width: Binding<Double>) -> some View {
         modifier(SymbolWidthModifier(width: width))
    }
}

enter image description here

XY L
  • 25,431
  • 14
  • 84
  • 143
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Feb 14 '22 at 10:52
2

You can set the scalable width/height with the @ScaledMetric property wrapper that is available since iOS 14 or macOS 11.

For example, define this view first:

struct ScaledFrame<Content>: View where Content: View {
    @ScaledMetric private var width: Double
    @ScaledMetric private var height: Double
    private var alignment: Alignment
    private var content: () -> Content
    
    init(
        width: ScaledMetric<Double>? = nil,
        height: ScaledMetric<Double>? = nil,
        alignment: Alignment = .center,
        content: @escaping () -> Content
    ) {
        _width = width ?? ScaledMetric(wrappedValue: -1)
        _height = height ?? ScaledMetric(wrappedValue: -1)
        self.alignment = alignment
        self.content = content
    }
    
    var body: some View {
        content().frame(
            width: width > 0 ? width : nil,
            height: height > 0 ? height : nil,
            alignment: alignment)
    }
}

// For convenience :)
extension View {
    func scaledFrame(
        width: Double?,
        height: Double?,
        relativeTo textStyle: Font.TextStyle,
        alignment: Alignment = .center
    ) -> some View {
        ScaledFrame(
            width: width.flatMap { ScaledMetric(wrappedValue: $0, relativeTo: textStyle) },
            height: height.flatMap { ScaledMetric(wrappedValue: $0, relativeTo: textStyle) }) {
                self
            }
    }
}

Then you can use it like this:

struct Previews_ScaledFrame_Previews: PreviewProvider {
    static var previews: some View {
        let size = 30.0
        let textStyle = Font.TextStyle.body
        let scaledSize = ScaledMetric(wrappedValue: size, relativeTo: textStyle)
        return VStack(alignment: .leading, spacing: 0) {
            HStack {
                // Use it directly like this
                ScaledFrame(width: scaledSize, height: scaledSize) {
                    Image(systemName: "folder")
                }
                Text("Hello")
            }

            HStack {
                // Or use it via the method like this
                Image(systemName: "mappin")
                    .scaledFrame(
                        width: size,
                        height: size,
                        relativeTo: textStyle)
                Text("World")
            }
        }
    }
}

These are how it looks like when using the Dynamic Type:

xSmall

enter image description here

Large

enter image description here

AX5

enter image description here

Manabu Nakazawa
  • 1,983
  • 22
  • 23
1

A workaround for the issue. However, the problem is that it does not work well with dynamic font sizing.

HStack {
    Image(systemName: "square.and.arrow.up")
        .font(Font.system(size: 18, weight: .medium, design: .monospaced))
        .foregroundColor(Color("secondary"))
}
.frame(minWidth: 30)

enter image description here

XY L
  • 25,431
  • 14
  • 84
  • 143