Home > OS >  SwiftUI: Button without Label/String in the initializer but with ButtonStyle
SwiftUI: Button without Label/String in the initializer but with ButtonStyle

Time:12-07

SwiftUI has a few Button initializers, but all of them require either a String or some View as the parameter alongside with the action.

However, the button's appearance can also be customized with the help of ButtonStyles which can add custom views to it.

Let's consider a Copy button with the following icon:

SF Symbols Copy

The style I've made for the button looks as follows:

struct CopyButtonStyle: ButtonStyle {

    init() {}

    func makeBody(configuration: Configuration) -> some View {
        let copyIconSize: CGFloat = 24
        return Image(systemName: "doc.on.doc")
            .renderingMode(.template)
            .resizable()
            .frame(width: copyIconSize, height: copyIconSize)
            .accessibilityIdentifier("copy_button")
            .opacity(configuration.isPressed ? 0.5 : 1)
    }
}

It works perfectly, however, I have to initialize the Button with an empty string at call site:

Button("") {
    print("copy")
}
.buttonStyle(CopyButtonStyle())

So, the question is how can I get rid of the empty string in the button's initialization parameter?

Potential Solution

I was able to create a simple extension that accomplishes the job I need:

import SwiftUI

extension Button where Label == Text {
    init(_ action: @escaping () -> Void) {
        self.init("", action: action)
    }
}

Call site:

Button() { // Note: no initializer parameter
    print("copy")
}
.buttonStyle(CopyButtonStyle())

But curious, whether I'm using the Button struct incorrectly and there is already a use-case for that, so that I can get rid of this extension.

CodePudding user response:

An easier way than making a ButtonStyle configuration is to pass in the label directly:

Button {
    print("copy")
} label: {
    Label("Copy", systemImage: "doc.on.doc")
        .labelStyle(.iconOnly)
}

This also comes with some benefits:

  1. By default, the button is blue to indicate it can be tapped
  2. No weird stretching of the image that you currently have
  3. No need to implement how the opacity changes when pressed

You could also refactor this into its own view:

struct CopyButton: View {
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Label("Copy", systemImage: "doc.on.doc")
                .labelStyle(.iconOnly)
        }
    }
}

Called like so:

CopyButton {
    print("copy")
}

Which looks much cleaner overall.

CodePudding user response:

Here is a right way for what you are trying to do, you do not need make a new ButtonStyle for each kind of Button, you can create just one and reuse it for any other Buttons you want. Also I solved your Image stretching issue with .scaledToFit().

struct CustomButtonView: View {
    
    let imageString: String
    let size: CGFloat
    let identifier: String
    let action: (() -> Void)?
    
    init(imageString: String, size: CGFloat = 24.0, identifier: String = String(), action: (() -> Void)? = nil) {
        self.imageString = imageString
        self.size = size
        self.identifier = identifier
        self.action = action
    }

    var body: some View {
        
        return Button(action: { action?() } , label: {
            
            Image(systemName: imageString)
               .renderingMode(.template)
               .resizable()
               .scaledToFit()
               .frame(width: size, height: size)
               .accessibilityIdentifier(identifier)
            
        })
        .buttonStyle(CustomButtonStyle())

    }
    
}

struct CustomButtonStyle: ButtonStyle {

    func makeBody(configuration: Configuration) -> some View {
        return configuration.label
            .opacity(configuration.isPressed ? 0.5 : 1.0)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
    }
}

use case:

struct ContentView: View {
    var body: some View {
        
        CustomButtonView(imageString: "doc.on.doc", identifier: "copy_button", action: { print("copy") })

    }
}

CodePudding user response:

You can use EmptyView for label, like

    Button(action: { // Note: no initializer parameter
         print("copy")
    }, label: { EmptyView() })
    .buttonStyle(CopyButtonStyle())

but wrapping it in custom button type (like shown in other answer) is more preferable from re-use and code readability point of view.

  • Related