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 ButtonStyle
s which can add custom views to it.
Let's consider a Copy button with the following icon:
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:
- By default, the button is blue to indicate it can be tapped
- No weird stretching of the image that you currently have
- 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.