2

I am using a FluentUI#button, which behind uses UIKit

I need to display that button in a SwiftUI View, and I'm trying to toggle an @State property or add a #selector to the button, but I'm not able to do it

I created a generic UIViewRepresentable structure to help me embed any UIView in my SwiftUI Views, following this tutorial:

struct Anything<Wrapper : UIView>: UIViewRepresentable {
    typealias Updater = (Wrapper, Context) -> Void

    var makeView: () -> Wrapper
    var update: (Wrapper, Context) -> Void

    init(_ makeView: @escaping @autoclosure () -> Wrapper,
         updater update: @escaping (Wrapper) -> Void) {
        self.makeView = makeView
        self.update = { view, _ in update(view) }
    }

    func makeUIView(context: Context) -> Wrapper {
        makeView()
    }

    func updateUIView(_ view: Wrapper, context: Context) {
        update(view, context)
    }
}

And I have the following code:

import SwiftUI
import FluentUI

struct MyView: View {
    @State var isGreen = true
    
    var body: some View {
        VStack {
            Text("Hello, World!")
                .background(isGreen ? Color.green : Color.blue)
            Spacer().frame(height: 20)
            Anything(FluentUI.Button(style: .primaryFilled)) {
                $0.setTitle("Try me!", for: .normal)
            }
            .frame(height: 30)
            .padding()
        }
    }
}

struct Anything<Wrapper: UIView>: UIViewRepresentable {
    typealias Updater = (Wrapper, Context) -> Void

    var makeView: () -> Wrapper
    var update: (Wrapper, Context) -> Void
    var action: (() -> Void)?

    init(_ makeView: @escaping @autoclosure () -> Wrapper,
         updater update: @escaping (Wrapper) -> Void) {
        self.makeView = makeView
        self.update = { view, _ in update(view) }
    }

    func makeUIView(context: Context) -> Wrapper {
        makeView()
    }

    func updateUIView(_ view: Wrapper, context: Context) {
        update(view, context)
    }
}

struct SwiftUIView_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
    }
}

And if I try to add this:

$0.addTarget(self, action: #selector(toggleColor), for: .touchUpInside)

With:

func toggleColor() {
    isGreen = !isGreen
}

I get this error:

Argument of '#selector' refers to instance method 'toggleColor()' that is not exposed to Objective-C

And if I add @objc to the method I get this error:

@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes

And as my Anything struct isn't a Button from SwiftUI, I cannot add the action parameter as normally

How can I add a target/action to my button in this way?

Frakcool
  • 10,915
  • 9
  • 50
  • 89
  • I think that this issue is *part of* why there's a Coordinator pattern available. You could make an `@objc`-exposed function on your coordinator and then forward messages from there to your `View` using a closure. But, it'll be (at least) one more step compared to what you have. – jnpdx May 03 '22 at 19:59
  • Let me have a try @jnpdx, I appreciate your insights – Frakcool May 03 '22 at 20:00

1 Answers1

2

Here is a demo of possible solution - we need a wrapper between UIKit objective-c selectors and SwiftUI swift function.

Tested with Xcode 13.3 / iOS 15.4

demo

Here is main part (used UIButton instead of FluentUI.Button for simplicity):

Anything(UIButton(type: .system)) {
    $0.setTitle("Try me!", for: .normal)

    $0.addTarget(toggleColor, action: #selector(Action.perform(sender:)), for: .touchUpInside)
    toggleColor.action = {
        isGreen.toggle()
    }
}

Complete test module is here

Asperi
  • 228,894
  • 20
  • 464
  • 690