How would you make Icons like in the Settings App (SwiftUI)

Viewed 690

How could you achieve Icons like that? Image from the Settings App

I know that the base is this:

Image(systemName: "person.fill")

And than you could give it a background-Color:

Image(systemName: "person.fill")
            .background(Color.blue)

To get rounded corners you could just add cornerRadius:

Image(systemName: "person.fill")
            .background(Color.blue)
            .cornerRadius(5)

But how would you make it that each of the items is in a square box with the same size? Because SF Symbols don't have the same size.

And I don't want to make this:

Image(systemName: "person.fill")
            .frame(width: 20, height: 20)
            .background(Color.blue)
            .cornerRadius(5)

The frame modifier would destroy the ability of SF Symbols to match with the preferred Font Size of the User.

Is there an other solution?

Or do you think the Settings App is done with .frame()?

3 Answers

Okay, I found an answer at Medium. He works with Labels and adds an custom Modifier to them. The Modifier looks like that:

struct ColorfulIconLabelStyle: LabelStyle {
    var color: Color
    var size: CGFloat
    
    func makeBody(configuration: Configuration) -> some View {
        Label {
            configuration.title
        } icon: {
            configuration.icon
                .imageScale(.small)
                .foregroundColor(.white)
                .background(RoundedRectangle(cornerRadius: 7 * size).frame(width: 28 * size, height: 28 * size).foregroundColor(color))
        }
    }
}

I did some changes:

struct ColorfulIconLabelStyle: LabelStyle {
    var color: Color
    
    func makeBody(configuration: Configuration) -> some View {
        Label {
            configuration.title
        } icon: {
            configuration.icon
                .font(.system(size: 17))
                .foregroundColor(.white)
                .background(RoundedRectangle(cornerRadius: 7).frame(width: 28, height: 28).foregroundColor(color))
        }
    }
}

You can use it like that:

NavigationLink {
    //Destination
} label: {
    Label("Your Text", systemImage: "Your Image").labelStyle(ColorfulIconLabelStyle(color: .green))
}

This achieves a very native look :)

As I mentioned, full credits to Luca J.

I recommend this down way for you the update for my answer would be reading the device is zoomed or not! Then we could gave correct size for your UI, you can change your wished size in class.

enter image description here

struct ContentView: View {
    
    var body: some View {
        
        CustomButtonView(string: "gear", action: { print("setting!") })
        CustomButtonView(string: "lasso.sparkles", action: { print("lasso!") })
        CustomButtonView(string: "xmark.bin", action: { print("xmark!") })
        CustomButtonView(string: "command", action: { print("command!") })
        CustomButtonView(string: "infinity", action: { print("infinity!") })
        
    }
    
}



struct CustomButtonView: View {
    
    let string: String
    let action: (() -> Void)?
    
    init(string: String, action: (() -> Void)? = nil) {
        self.string = string
        self.action = action
    }
    
    @State private var tapped: Bool = Bool()
    
    var body: some View {
        
        Image(systemName: string)
            .resizable()
            .scaledToFit()
            .frame(width: DeviceReader.shared.size - 5.0, height: DeviceReader.shared.size - 5.0)
            .foregroundColor(Color.white)
            .padding(5.0)
            .background(tapped ? Color.blue : Color.gray)
            .cornerRadius(10.0)
            .onTapGesture { tapped.toggle(); action?() }
            .animation(.interactiveSpring(), value: tapped)
        
    }
    
}



class DeviceReader: ObservableObject {
    
    let size: CGFloat
    
    init() {
        
        switch UIDevice.current.userInterfaceIdiom {
        case .phone: self.size = 30.0
        case .pad: self.size = 40.0
        case .mac: self.size = 50.0
        default: self.size = 30.0 }

    }
    
    static let shared: DeviceReader = DeviceReader()
    
}

I was looking at this question and your answer to it because this would be a useful thing to have. However, your answer does not allow the SFFont to scale with user preferences, and the answer you found on the Medium post does not scale well, as you can't just scale up and down with theses things. They look weird. If you run it in the simulator and change the Text setting, your will see what I mean.

I would simply use a .frame that changes it's size based off a preference key on the SF Symbol itself, and giving it a bit of padding extra. You could also simply add .padding() before your .background(), but the background would not necessarily be square. This method will set the width and height of the frame to slightly more than the biggest dimension of the SF Symbol, and it will fluidly change its size, not only allowing you to drop a .font() on it, but also handle the dynamic font sizes. This is a pure SwiftUI answer, using no UIKit.

Icons in various sizes

struct ColoredIconView: View {

    let imageName: String
    let foregroundColor: Color
    let backgroundColor: Color
    @State private var frameSize: CGSize = CGSize(width: 30, height: 30)
    @State private var cornerRadius: CGFloat = 5
    
    var body: some View {
        Image(systemName: imageName)
            .overlay(
                GeometryReader { proxy in
                    Color.clear
                        .preference(key: SFSymbolKey.self, value: max(proxy.size.width, proxy.size.height))
                }
            )
            .onPreferenceChange(SFSymbolKey.self) {
                let size = $0 * 1.05
                frameSize = CGSize(width:size, height: size)
                cornerRadius = $0 / 6.4
            }
            .frame(width: frameSize.width, height: frameSize.height)
            .foregroundColor(foregroundColor)
            .background(
                RoundedRectangle(cornerRadius: cornerRadius)
                    .fill(backgroundColor)
            )
    }
}

fileprivate struct SFSymbolKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

Use it like this:

ColoredIconView(imageName: "airplane", foregroundColor: .white, backgroundColor: .orange)
    .font(.body)
Related