从SwiftUI视图如何调用UIKit的ViewController方法

5

我已经在网上寻找了答案,但似乎找不到。如何在SwiftUI中调用viewController方法(例如在按钮点击时)?

我的viewcontroller如下所示:

import Player

class PlayerViewController: UIViewController {
    var player = Player()
    func play() {
        self.player.play()
    }
}

我有一个看起来像这样的包装器:

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
}

我希望能够在SwiftUI中使用按钮动作,并仅调用viewController的play方法一次。我看到有些答案建议在包装器上设置状态/绑定,并在updateUIViewController中调用该方法,但是当我这样做时,它会被调用多次,而不是一次。


1
我停止使用SwiftUI 1.0(并且可能不会将任何东西移植到SwiftUI 3.0之前),但我知道一件肯定有效的事情 - 并且绝对不是SwiftUI或Combine - 那就是通知。去年8月份就像魔法一样运作良好。 - user7014451
这个回答解决了你的问题吗?从SWIFTUI调用UIKIT函数 - lorem ipsum
3个回答

6
这里提供一种基于协议/配置器的方法,使得可以直接使用操作,从而更加简单易读。
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 ?? {})
        }
    }
}


2
我以相同的方式做了,但它引发了SwiftUI问题:在视图更新期间修改状态,这将导致未定义的行为。 - Ptah

3

我曾经也遇到过这个问题,使用了通知中心来解决。但是还有另一种方法。您可以在SwiftUIView中创建一个@StateObject "Coordinator",将其传递给UIViewControllerRepresentable,然后在viewDidLoad中将ViewController传回给Coordinator,您可以通过协调器调用其函数。

// Create a Coordinator
class BridgingCoordinator: ObservableObject {
    var vc: ViewController!
}

// SwiftUI View
struct SwiftUIView: View {
    @StateObject private var coordinator: BridgingCoordinator

    init() {
        let coordinator = BridgingCoordinator()
        self._coordinator = StateObject(wrappedValue: coordinator)
    }

    var body: some View {
        VStack {
            Text("Swift UI View")

            Button(action: buttonTapped) {
                Text("Call function on UIViewControllerRepresentable VC")
            }
            .disabled(coordinator.vc == nil)

            UIViewControllerRepresentation(bridgingCoordinator: coordinator)
        }
    }
    
    private func buttonTapped() {
        coordinator.vc.doSomething()
    }
}

// The UIViewControllerRepresentable of the ViewController
struct UIViewControllerRepresentation: UIViewControllerRepresentable {
    var bridgingCoordinator: BridgingCoordinator

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    func makeUIViewController(context: Context) -> some UIViewController {
        let vc = ViewController()
        vc.bridgingCoordinator = bridgingCoordinator
        return vc
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        //
    }

    class Coordinator: NSObject {
        let parent: UIViewControllerRepresentation
        init(_ view: UIViewControllerRepresentation) {
            self.parent = view
        }
    }
}

// ViewController which contains functions that need to be called from SwiftUI
class ViewController: UIViewController {
    // The BridgingCoordinator received from the SwiftUI View
    var bridgingCoordinator: BridgingCoordinator!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Set self to the BridgingCoordinator
        bridgingCoordinator.vc = self
    }

    func doSomething() {
        print("Received function call from SwiftUI View")
    }
}

1
似乎这仍然是2023年最好的方法。 - David

3
很好的问题。在SwiftUI中似乎缺少某些东西。
如果您的应用程序中只有一个这样的视图控制器,则可以通过拥有全局的PassthroughSubject(或另一种传递事件的方式)来解决此问题。您的UIViewController可以订阅它,而您的SwiftUI代码可以发布点击事件。
如果您不想这样做,这里有另一种使用UUID的解决方法,以消除您提到的多个调用。
也许我们会在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
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接