Implement delegates within SwiftUI Views
Asked Answered
S

1

32

I am trying to implement a functionality that requires a delegate method (like NSUserActivity). Therefore I need a UIViewController that conforms to NSUserActivityDelegate (or similar other delegates), handles and hold all the required information. My problem is that I am using SwiftUI for my interface and therefore I am not using UIViewControllers. So how can I implement this functionality and still use SwiftUI for the UI. What I tried: view1 is just a normal SwiftUI View that can present (via NavigationLink) view2 which is the view where in want to implement this functionality. So I tried instead of linking view1 and view2, linking view1 to a UIViewControllerRepresentable which then handles the implementation of this functionality and adds UIHostingController(rootView: view2) as a child view controller.

struct view1: View {    
    var body: some View {
        NavigationLink(destination: VCRepresentable()) {
            Text("Some Label")
        }
    }
}

struct view2: View {    
    var body: some View {
        Text("Hello World!")
    }
}

struct VCRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        return implementationVC()
    }

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

class implementationVC: UIViewController, SomeDelegate for functionality {
    // does implementation stuff in delegate methods
    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        attachChild(UIHostingController(rootView: view2()))
    }

    private func attachChild(_ viewController: UIViewController) {
        addChild(viewController)

        if let subview = viewController.view {
            subview.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(subview)

            subview.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
            subview.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
            subview.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
            subview.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        }

        viewController.didMove(toParent: self)
    }
}

I am having trouble with transferring the data between my VC and my view2. So I'm wondering if there is a better way to implement such a functionality within a SwiftUI View.

Stooge answered 30/7, 2019 at 23:46 Comment(6)
If your app is a SwiftUI app - and it sounds like it is - then you must use UIViewControllerRepresentable. Without it? You cannot implement delegate methods. Now, you may be confused - UIHostingController is how you do the reverse... add a SwiftUI view to your UIKit project. Here (and elsewhere, including WWDC videos) there are pretty decent examples of using a UIViewController in a SwiftUI project. My suggestion is to (1) get something working in a UIKit project first, then (2) expose it in a SwiftUI app - remember, a UIViewcontrollerRepresentable is just a SwiftUI View`.Andiron
I am sorry, I corrected my code: view1 links to a UIViewControllerRepresentable in order to add an UIViewController to my app that can handle my delegate. Since I want to do my UI in SwiftUI this UIViewController attaches a child vc (see code) which is a UIHostingController in order to present the UI in a SwiftUI View. My problem is that I can not really pass information between my UIViewController (delegate handler) and my SwiftUI View (view2)Stooge
UIHostingController is used only in a UIKit app. It's how you bring a SwiftUI View into UIKit. Basically, you use either a UIViewControllerRepresentable (UIKit >> SwiftUI) or a UIHostingController (SwiftUI >> UIKit. For more details, check out Session 231: Integrating SwiftUI.Andiron
Okay I get that. But how can I handle my delegate methods on a UIViewController without using it as UI and instead using view2 (a swift UI View)?Stooge
I've created three UIViewControllerRepresentables in my SwiftUI project. MTKView, UIImagePickerController, and UIActivityViewController. Two of them use delegates (the first two). I'm really not sure if they can help you, but I'm willing to try. The idea behind an image picker is to (a) present it, (b) use the delegate methods to either get the image picked or pass on that cancel was pressed, and (c) dismiss it. It's tied into my model - as of beta 5 it's now called an ObservableObject and update the various SwiftUI Views when the app state is updated. Can this help you?Andiron
It might. Could you attempt to write an answer and show some code. Even if you aren't sure if it helps because you can still update the answer.Stooge
C
37

You need to create a view that conforms to UIViewControllerRepresentable and has a Coordinator that handles all of the delegate functionality.

For example, with your example view controller and delegates:

struct SomeDelegateObserver: UIViewControllerRepresentable {
    let vc = SomeViewController()
    var foo: (Data) -> Void
    func makeUIViewController(context: Context) -> SomeViewController {
        return vc
    }

    func updateUIViewController(_ uiViewController: SomeViewController, context: Context) { }
    func makeCoordinator() -> Coordinator {
        Coordinator(vc: vc, foo: foo)
    }

    class Coordinator: NSObject, SomeDelegate {
        var foo: (Data) -> Void
        init(vc: SomeViewController, foo: @escaping (Data) -> Void) {
            self.foo = foo
            super.init()
            vc.delegate = self
        }
        func someDelegateFunction(data: Data) {
            foo(data)
        }
    }
}

Usage:

struct ContentView: View {
    var dataModel: DataModel

    var body: some View {
        NavigationLink(destination: CustomView(numberFromPreviousView: 10)) {
            Text("Go to VCRepresentable")
        }
    }
}

struct CustomView: View {
    @State var instanceData1: String = ""
    @State var instanceData2: Data?
    var numberFromPreviousView: Int // example of data passed from the previous view to this view, the one that can react to the delegate's functions
    var body: some View {
        ZStack {
            SomeDelegateObserver { data in
                print("Some delegate function was executed.")
                self.instanceData1 = "Executed!"
                self.instanceData2 = data
            }
            VStack {
                Text("This is the UI")
                Text("That, in UIKit, you would have in the UIViewController")
                Text("That conforms to whatever delegate")
                Text("SomeDelegateObserver is observing.")
                Spacer()
                Text(instanceData1)
            }
        }
    }
}

Note: I renamed VCRepresentable to SomeDelegateObserver to be more indicative of what it does: Its sole purpose is to wait for delegate functions to execute and then run the closures (i.e foo in this example) you provide it. You can use this pattern to create as many functions as you need to "observe" whatever delegate functions you care about, and then execute code that can update the UI, your data model, etc. In my example, when SomeDelegate fires someDelegateFunction(data:), the view will display "Excuted" and update the data instance variable.

Cookhouse answered 31/7, 2019 at 16:25 Comment(10)
The problem with that is: the destination of the NavigationLink is VCRepresentable which returns ImplementationVC in makeUIViewController(context:) for the UI and that would require me to do my UI in my ImplementationVC (so using UIKit)Stooge
No you do not need to do that. You can create a custom View that contains VCRepresentable and the UI you want within a ZStack, and then navigate to that view. I’ll update my code to show you what I mean. I’m guessing ImplementationVC is just a placeholder for your example and not an actual view controller in your project, right?Cookhouse
For SomeDelegate I am using NSUserActivityDelegate but somehow I can't access self.userActivity. When I used class VC: UIViewController, NSUserActivityDelegate { ... } I was able to do access it. And for someDelegateFunction(data:) I also can't access updateUserActivityState(_ activity:) for exampleStooge
Thats means I can't put my delegate method in the Coordinator classStooge
and what does vc.delegate stand for because UIViewController has no member delegate?Stooge
I fixed my problem I had to inherit from the UIResponder class instead of NSObjectStooge
Glad you figured it out! And I'll rename UIViewController to be more generic; the view controller should be whatever type of view controller the delegate belongs to. For example, if you wanted to listen for tableview delegate methods, you would either make the view controller a TableViewController or make the struct a UIViewRepresentable and use a TableView, and then hook up its delegate in the Coordinator.Cookhouse
Coordinator(foo: foo) should be Coordinator(vc: vc, foo: foo)Doubleteam
To better understand Coordinator, here's a great tutorial about it: hackingwithswift.com/books/ios-swiftui/…Illlooking
@MattCarroll True! Good catch, just fixed it.Cookhouse

© 2022 - 2024 — McMap. All rights reserved.