Execute the same code for every button

I have a standard ButtonStyle that I use for all the buttons in my app:

import SwiftUI

public struct DefaultButtonStyle: ButtonStyle
{
  let color: Color
  
  init() { color = colorDefaultButton }
  init(color: Color) { self.color = color }
  
  public func makeBody(configuration: Self.Configuration) -> some View
  {
    configuration.label
      .frame(width: 90.0, height: 25.0)
      .background(color)
      .foregroundColor(.white)
      .cornerRadius(8.0)
      .overlay(RoundedRectangle(cornerRadius: 5.0)
        .stroke(lineWidth: 1))
      .scaleEffect(configuration.isPressed ? 0.9 : 1)
      .shadow(color: .gray,
              radius: configuration.isPressed ? 4 : 6,
              x: configuration.isPressed ? 4 : 6,
              y: configuration.isPressed ? 4 : 6)
    
  }
}

I would like to execute code every time I define the button. For illustration, here is the code attached to a single button:

              Button("Export")
              {
                showingExporter = true
                document.message = periodicals.getDataAsString() ?? "No Data"
                
                let generator = UIImpactFeedbackGenerator(style: .soft)
                generator.impactOccurred()
              }
              .padding([.leading, .bottom])
              .buttonStyle(DefaultButtonStyle())
              .fileExporter(isPresented: $showingExporter,
                            document: document,
                            contentType: .plainText)

I want to have the two lines creating and activating the feedback generator executed for every button, but I’m hoping to invoke it from the ButtonStyle so I don’t have to code it for each button.

Is there a way to do this?

Use view composition and create a new View that contains your Button with the style and action you want. Then use that View wherever you would have used the Button.

So basically shove all of this:

Button("Export")
{
    showingExporter = true
    document.message = periodicals.getDataAsString() ?? "No Data"
                
    let generator = UIImpactFeedbackGenerator(style: .soft)
    generator.impactOccurred()
}
.padding([.leading, .bottom])
.buttonStyle(DefaultButtonStyle())

into a new View and use that. You will probably need to pass in some dependencies and bindings.

Thanks for replying. The first two lines of code is unique for each button. The last two lines are common for every button. I could make the last two lines a function, but that wouldn’t save much. I’d still have to call the function for each button.

I would like the last two lines as part of the default button definition, but not the other lines.

You would need to do something like this:

struct ShakyButton: View {
    let title: String
    let action: () -> Void
    
    init(_ title: String, action: @escaping () -> Void) {
        self.title = title
        self.action = action
    }
    
    var body: some View {
        Button(title) {
            action()
            
            //this code is common to all ShakyButtons
            let generator = UIImpactFeedbackGenerator(style: .soft)
            generator.impactOccurred()
        }
        .buttonStyle(DefaultButtonStyle())
    }
}

struct TrialButtonView: View {
    @State private var showingSheet = false
    
    var body: some View {
        VStack {
            ShakyButton("Export") {
                //here's where you would put the custom code for each button
                showingSheet.toggle()
            }
            .padding([.leading, .bottom])
        }
        .sheet(isPresented: $showingSheet) {
            print("Dis-missed!")
        } content: {
            Color.mint
        }

    }
}

Thanks Patrick. I’ll need a day or two to try this out. I’ll post the results here.

Didn’t take as long as I expected. It works perfectly. Thanks for your help.

One question. Can you help me understand why @escaping is necessary? Is it because the function is passed to another function?

It’s because the closure isn’t being called right away, but is being stored and could be called at any later time.

With something like this:

//func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]

[1,2,3,4,5].map { "\($0)" }

the closure you pass in doesn’t have to be marked as @escaping because it’s being called immediately. There is no way for it to “escape” the scope of the map function.

But with the action closure of a Button, you aren’t calling it immediately. You are storing the closure and then calling it when the user triggers the button, which may be now or it may be later in your code. Basically, the action closure gets called some time after the Button itself is constructed so it needs a way to live on after that init runs.

Thanks. That clears it up.