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.

l30c0d35
  • 777
  • 1
  • 8
  • 32
  • 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`. –  Jul 31 '19 at 01:35
  • 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) – l30c0d35 Jul 31 '19 at 08:33
  • 1
    `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. –  Jul 31 '19 at 10:32
  • 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)? – l30c0d35 Jul 31 '19 at 14:52
  • 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? –  Jul 31 '19 at 15:59
  • 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. – l30c0d35 Aug 02 '19 at 12:55

1 Answers1

35

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.

RPatel99
  • 7,448
  • 2
  • 37
  • 45
  • 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) – l30c0d35 Aug 01 '19 at 10:41
  • 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? – RPatel99 Aug 01 '19 at 18:37
  • 1
    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 example – l30c0d35 Aug 02 '19 at 12:44
  • Thats means I can't put my delegate method in the Coordinator class – l30c0d35 Aug 02 '19 at 14:50
  • and what does `vc.delegate` stand for because `UIViewController` has no member delegate? – l30c0d35 Aug 02 '19 at 14:54
  • I fixed my problem I had to inherit from the `UIResponder` class instead of `NSObject` – l30c0d35 Aug 02 '19 at 15:27
  • 2
    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. – RPatel99 Aug 02 '19 at 15:38
  • Coordinator(foo: foo) should be Coordinator(vc: vc, foo: foo) – Matt Carroll Jul 15 '20 at 18:55
  • 2
    To better understand `Coordinator`, here's a great tutorial about it: https://www.hackingwithswift.com/books/ios-swiftui/using-coordinators-to-manage-swiftui-view-controllers – Skoua Aug 25 '20 at 09:56
  • @MattCarroll True! Good catch, just fixed it. – RPatel99 Oct 13 '20 at 07:11