swiftswiftuidelegatesautomatic-ref-counting

Why doesn't delegate work in SwiftUI with UIViewControllerRepresentable?


Here's a sample code snippet for a video player in SwiftUI, where the player's core is implemented in UIKit (specifically in PlayerViewController):

struct Page: View {
    let delegate = Delegate()
    @State private var isPlay = true
    var body: some View {
        VStack {
            PlayerView(delegate: delegate)
            Button("Play") { delegate.play() }
        }
    }
}
class Delegate {
    var controller: PlayerViewController? = nil
    func play() { controller?.play() }
    func pause() { controller?.pause() }
}
struct PlayerView: UIViewControllerRepresentable {
    let delegate: Delegate
    func makeUIViewController(context: Context) -> some UIViewController {
        let c =  PlayerViewController()
        delegate.controller = c
        return c
    }
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
}
class PlayerViewController: UIViewController {
    func play() { ... }
    func pause() { ... }
}

In the code above, the page correctly displays the PlayerViewController, and the video plays without issue. However, I have run into a problem when trying to call functions like play or pause on the video.

I created a delegate (the Delegate class) to encapsulate these functions, passing the delegate to PlayerView, and assigned delegate.controller = c in the makeUIViewController method.

Unfortunately, when I tap the play button, I discovered that the controller is nil.

I'm puzzled as to why this delegate isn't functioning as expected. Can anyone help me understand what's going wrong, and guide me on how to fix it? Any assistance would be greatly appreciated!

Additionally, I replicated the same scenario in a playground, and everything appears to be working as expected:

class City {
    let name: String
    init(name: String) { self.name = name }
    func say() { print("hello I am \(name)") }
}
class Delegate {
    var city: City? = nil
    func sayFromDelegate() {
        city?.say()
    }
}
struct Person {
    let name: String = "Person"
    let delegate: Delegate
    func foo() {
        let c = City(name: "QingDao")
        delegate.city = c
    }
}
let d = Delegate()
let p = Person(delegate: d)
p.foo() // force bind delegate
d.sayFromDelegate() // hello I am QingDao

Solution

  • View structs are ephemeral so can't hold onto objects so your let delegate = Delgate() is a memory leak.

    You need to either pass a closure or a binding to your UIViewControllerRepresentable and implement the update func to update the view controller if these values have changed from the last time update was called.

    Eg

    
    @Binding var myBinding : Int
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        uiViewController.someValueChanged = { x in
            myBinding = x
        }
    
    }