11

I'm trying to call a local ViewController function from ContentView. The function uses some local variables and cannot be moved outside the ViewController.

class ViewController: UIViewController {
    func doSomething() {...}
} 

extension ViewController : LinkViewDelegate {...}

located on a different file:

struct ContentView: View {

    init() {
        viewController = .init(nibName:nil, bundle:nil)
    }
    var viewController: viewController

var body: some View {
    Button(action: {self.viewController.doSomething()}) {
            Text("Link Account")
        }
    }
}

UIViewController cannot be changed to something like UIViewRepresentable because LinkViewDelegate can only extend UIViewController.

Liv
  • 235
  • 3
  • 9
  • Solution with a reference to a view controller that allows to call all its functions directly from SwiftUI: https://stackoverflow.com/questions/65923718/calling-functions-from-uiviewcontroller-in-swiftui/73543236#73543236 – iUrii Aug 30 '22 at 13:27

3 Answers3

8

So you need to create a simple bool binding in SwiftUI, flip it to true to trigger the function call in the UIKit viewController, and then set it back to false until the next time the swiftUI button is pressed. (As for LinkViewDelegate preventing something like UIViewControllerRepresentable that shouldn't stop you, use a Coordinator to handle the delegate calls.)

struct ContentView: View {

    @State var willCallFunc = false

    var body: some View {
        ViewControllerView(isCallingFunc: $willCallFunc)

        Button("buttonTitle") {
            self.willCallFunc = true
        }
    }
}

struct ViewControllerView: UIViewControllerRepresentable {

    @Binding var isCallingFunc: Bool

    func makeUIViewController(context: Context) -> YourViewController {
        makeViewController(context: context) //instantiate vc etc.
    }

    func updateUIViewController(_ uiViewController: YourViewController, context: Context) {
        if isCallingFunc {
            uiViewController.doSomething()
            isCallingFunc = false
        }
    }
}
MadeByDouglas
  • 2,509
  • 1
  • 18
  • 22
7

Here is a way that I've come up with which doesn't result in the "Modifying state during view update, this will cause undefined behavior" problem. The trick is to pass a reference of your ViewModel into the ViewController itself and then reset the boolean that calls your function there, not in your UIViewControllerRepresentable.

public class MyViewModel: ObservableObject {
    @Published public var doSomething: Bool = false
}


struct ContentView: View {

    @StateObject var viewModel = MyViewModel()

    var body: some View {
        MyView(viewModel: viewModel)

        Button("Do Something") {
            viewModel.doSomething = true
        }
    }
}


struct MyView: UIViewControllerRepresentable {
    
    @ObservedObject var viewModel: MyViewModel
    
    func makeUIViewController(context: Context) -> MyViewController {
        return MyViewController(viewModel)
    }
    
    func updateUIViewController(_ viewController: MyViewController, context: Context) {
        if viewModel.doSomething {
            viewController.doSomething()
            // boolean will be reset in viewController
        }
    }
}


class MyViewController: UIViewController {
    
    var viewModel: MyViewModel
    
    public init(_ viewModel: MyViewModel) {
        self.viewModel = viewModel
    }
    
    public func doSomething() {
        // do something, then reset the flag
        viewModel.doSomething = false
    }
}

nrj
  • 1,701
  • 2
  • 22
  • 37
3

You could pass the instance of ViewController as a parameter to ContentView:

struct ContentView: View {
    var viewController: ViewController // first v lowercase, second one Uppercase

    var body: some View {
        Button(action: { viewController.doSomething() }) { // Lowercase viewController
            Text("Link Account")
        }
    }

    init() {
        self.viewController = .init(nibName:nil, bundle:nil) // Lowercase viewController
    }
} 

// Use it for the UIHostingController in SceneDelegate.swift
window.rootViewController = UIHostingController(rootView: ContentView()) // Uppercase ContentView

Updated answer to better fit the question.

cbjeukendrup
  • 3,216
  • 2
  • 12
  • 28
  • I get an error in my scene delegate "let contentView = ContentView(viewController: ViewController)" - Use of unresolved identifier 'ViewController' – Liv Oct 19 '19 at 17:02
  • @Liv You will have to init the instance of ViewController before making the ContentView and the UIHostingController. Please make sure that viewController is written with a lowercase _v_, because it’s an instance and not a type in this case. – cbjeukendrup Oct 19 '19 at 17:12
  • i updated the sample code to reflect the changes. The project builds but when ever the button action is performed the project crashes with this error: "libc++abi.dylib: terminating with uncaught exception of type NSException " – Liv Oct 20 '19 at 02:20
  • @Liv Updated the answer, hoping to get a step closer to the solution! I hope it's now more like you need it. – cbjeukendrup Oct 20 '19 at 19:18
  • I still can't get it to work and it still gives me the same error. i've contacted support for the API that im trying to implement. I'll see what i hear back. Will update this post when i get it working :) – Liv Oct 22 '19 at 06:18