How come this SwiftUI Button extension initializer doesn't work?

Viewed 687

I am trying to create an extension on Button that reduces the boilerplate when creating them with a system image.

extension Button {
  init(_ systemName: String, action: @escaping () -> Void) {
    self.init {
      action()
    } label: {
      Image(systemName: systemName) // <-- Error
    }
  }
}

This results in the following compiler error:

Cannot convert value of type 'Image' to closure result type 'Label'

Looking at Button's initializer, I see it declared like this:

struct Button<Label> : View where Label : View {
  init(action: @escaping () -> Void, @ViewBuilder label: () -> Label) { ... }
}

But specifying my extension similarly also doesn't work. Where am I going wrong?

extension Button where Label : View {

2 Answers

There are two immediate issues with your approach:

  1. You need to specify that your Label is of type Image
  2. The signature you're using will collide with an existing constructor for Button (Button(_ title: StringProtocol, action: () -> Void))

You could address both with your extension:

extension Button where Label == Image {
  init(systemName: String, action: @escaping () -> Void) { 
    self.init(action: action, label: { Image(systemName: systemName) })
  }
}

But it feels more in line with SwiftUI's API to make your own Button wrapper:

struct SystemImageButton: View {
  let action: () -> Void
  let systemName: String

  init(systemName: String,
       action: @escaping () -> Void) {
    self.action = action
    self.systemName = systemName
  }

  var body: some View {
    Button(action: action,
           label: { Image(systemName: systemName) })
  }
}

You need to specify generics type, like

extension Button where Label == Image {   // << here !!
  init(_ systemName: String, action: @escaping () -> Void) {
    self.init {
      action()
    } label: {
      Image(systemName: systemName) // << No Error !!
    }
  }
}

Tested with Xcode 12.5 / iOS 14.5

Note: see for details how system extensions for Text label declared

extension Button where Label == Text {
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Button where Label == Text {
    public init(_ titleKey: LocalizedStringKey, action: @escaping () -> Void)
Related