Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Send tapAction from SwiftUI button action to UIView function

Tags:

ios

swift

swiftui

I'm trying to find a way to trigger an action that will call a function in my UIView when a button gets tapped inside swiftUI.

Here's my setup:

foo()(UIView) needs to run when Button(SwiftUI) gets tapped

My custom UIView class making use of AVFoundation frameworks

class SomeView: UIView {

    func foo() {}
}

To use my UIView inside swiftUI I have to wrap it in UIViewRepresentable

struct SomeViewRepresentable: UIViewRepresentable {

    func makeUIView(context: Context) -> CaptureView {
        SomeView()
    }

    func updateUIView(_ uiView: CaptureView, context: Context) {        
    }
}

SwiftUI View that hosts my UIView()

struct ContentView : View {

    var body: some View {
        VStack(alignment: .center, spacing: 24) {
            SomeViewRepresentable()
                .background(Color.gray)
            HStack {
                Button(action: {
                    print("SwiftUI: Button tapped")
                   // Call func in SomeView()
                }) {
                    Text("Tap Here")
                }
            }
        }
    }
}
like image 366
8HP8 Avatar asked Jun 13 '19 15:06

8HP8


3 Answers

Here's another way to do it using a bridging class.

//SwiftUI
struct SomeView: View{
  var bridge: BridgeStuff?

  var body: some View{
    Button("Click Me"){
      bridge?.yo()
    }
  }
}

//UIKit or AppKit (use NS instead of UI)
class BridgeStuff{
  var yo:() -> Void = {}
}

class YourViewController: UIViewController{

  override func viewDidLoad(){
    let bridge = BridgeStuff()
    let view = UIHostingController(rootView: SomeView(bridge: bridge))
    bridge.yo = { [weak self] in
      print("Yo")
      self?.howdy()
    }
  }

  func howdy(){
    print("Howdy")
  }
}
like image 200
Clifton Labrum Avatar answered Nov 15 '22 17:11

Clifton Labrum


You can store an instance of your custom UIView in your representable struct (SomeViewRepresentable here) and call its methods on tap actions:

struct SomeViewRepresentable: UIViewRepresentable {

  let someView = SomeView() // add this instance

  func makeUIView(context: Context) -> SomeView { // changed your CaptureView to SomeView to make it compile
    someView
  }

  func updateUIView(_ uiView: SomeView, context: Context) {

  }

  func callFoo() {
    someView.foo()
  }
}

And your view body will look like this:

  let someView = SomeViewRepresentable()

  var body: some View {
    VStack(alignment: .center, spacing: 24) {
      someView
        .background(Color.gray)
      HStack {
        Button(action: {
          print("SwiftUI: Button tapped")
          // Call func in SomeView()
          self.someView.callFoo()
        }) {
          Text("Tap Here")
        }
      }
    }
  }

To test it I added a print to the foo() method:

class SomeView: UIView {

  func foo() {
    print("foo called!")
  }
}

Now tapping on your button will trigger foo() and the print statement will be shown.

like image 30
M Reza Avatar answered Nov 15 '22 17:11

M Reza


M Reza's solution works for simple situations, however if your parent SwiftUI view has state changes, every time when it refreshes, it will cause your UIViewRepresentable to create new instance of UIView because of this: let someView = SomeView() // add this instance. Therefore someView.foo() is calling the action on the previous instance of SomeView you created, which is already outdated upon refreshing, so you might not see any updates of your UIViewRepresentable appear on your parent view. See: https://medium.com/zendesk-engineering/swiftui-uiview-a-simple-mistake-b794bd8c5678

A better practice would be to avoid creating and referencing that instance of UIView when calling its function.

My adaption to M Reza's solution would be calling the function indirectly through parent view's state change, which triggers updateUIView :

  var body: some View {
    @State var buttonPressed: Bool = false
    VStack(alignment: .center, spacing: 24) {

      //pass in the @State variable which triggers actions in updateUIVIew
      SomeViewRepresentable(buttonPressed: $buttonPressed)
        .background(Color.gray)
      HStack {
        Button(action: {
          buttonPressed = true
        }) {
          Text("Tap Here")
        }
      }
    }
  }

struct SomeViewRepresentable: UIViewRepresentable {
  @Binding var buttonPressed: Bool 

  func makeUIView(context: Context) -> SomeView {
    return SomeView()
  }

  //called every time buttonPressed is updated
  func updateUIView(_ uiView: SomeView, context: Context) {
    if buttonPressed {
        //called on that instance of SomeView that you see in the parent view
        uiView.foo()
        buttonPressed = false
    }
  }
}
like image 43
ada10086 Avatar answered Nov 15 '22 17:11

ada10086