I have looked all over for the answer to this question and can't seem to find it. How can I call a viewController method from swiftUI (e.g. on a button click)?
I have a viewcontroller that looks like this:
import Player
class PlayerViewController: UIViewController {
var player = Player()
func play() {
self.player.play()
}
}
And I have a wrapper that looks like this:
import SwiftUI
import AVFoundation
struct ProjectEditorPlayerBridge: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> PlayerViewController {
let player = PlayerViewController()
return player
}
func updateUIViewController(_ uiViewController: PlayerViewController, context: Context) {
}
typealias UIViewControllerType = PlayerViewController
}
I want to be able to use a button action in swiftUI and call the viewController play
method once. I have seen answers that suggest setting state/binding on the wrapper and calling the method in updateUIViewController, but when I do this I see it gets called multiple times, not just once.
Here is possible protocol/configurator based approach, which allows to use actions directly that looks more appropriate from code simplicity and readability.
protocol Player { // use protocol to hide implementation
func play()
}
class PlayerViewController: UIViewController, Player {
var player = Player()
func play() {
self.player.play()
}
}
struct ProjectEditorPlayerBridge: UIViewControllerRepresentable {
var configurator: ((Player) -> Void)? // callback
func makeUIViewController(context: Context) -> PlayerViewController {
let player = PlayerViewController()
// callback to provide active component to caller
configurator?(player)
return player
}
func updateUIViewController(_ uiViewController: PlayerViewController, context: Context) {
}
typealias UIViewControllerType = PlayerViewController
}
struct DemoPlayerView: View {
@State private var player: Player? // always have current player
var body: some View {
VStack {
ProjectEditorPlayerBridge { self.player = $0 } // << here !!
// use player action directly !!
Button("Play", action: player?.play ?? {})
}
}
}
backup
Good question. It seems to me that something is missing from SwiftUI here.
If you only have one of these view controllers in your app, then you could workaround this by having a global PassthroughSubject
(or another way to pass an event). Your UIViewController
could subscribe to it, and your SwiftUI code could publish clicks to it.
If you don't want to do that, here is another workaround that uses UUID
to get rid of those multiple calls you mentioned.
Maybe we'll see new options at WWDC 2020.
struct ContentView: View {
@State var buttonClickID: UUID? = nil
var body: some View {
VStack {
Button(action: self.callPlay) { Text("Play") }
ProjectEditorPlayerBridge(clickID: $buttonClickID)
}
}
func callPlay() {
buttonClickID = UUID()
}
}
struct ProjectEditorPlayerBridge: UIViewControllerRepresentable {
@Binding var clickID: UUID?
func makeUIViewController(context: Context) -> PlayerViewController {
let player = PlayerViewController()
return player
}
class Coordinator {
var previousClickID: UUID? = nil
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func updateUIViewController(_ uiViewController: PlayerViewController, context: Context) {
print("Update")
if clickID != context.coordinator.previousClickID {
uiViewController.play()
context.coordinator.previousClickID = clickID
} else {
print("Not calling play")
}
}
typealias UIViewControllerType = PlayerViewController
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With