Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling a local UIViewController function from a SwiftUI View

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.

like image 511
Liv Avatar asked Oct 19 '19 05:10

Liv


People also ask

What is UIViewController in SwiftUI?

UIKit has a class called UIViewController , which is designed to hold all the code to bring views to life. Just like UIView , UIViewController has many subclasses that do different kinds of work. UIKit uses a design pattern called delegation to decide where work happens.

What is the difference between UIView and UIViewController?

They are separate classes: UIView is a class that represents the screen of the device of everything that is visible to the viewer, while UIViewController is a class that controls an instance of UIView, and handles all of the logic and code behind that view.

Is UIViewController a subclass of UIView?

Instead, you subclass UIViewController and add the methods and properties needed to manage the view controller's view hierarchy. A view controller's main responsibilities include the following: Updating the contents of the views, usually in response to changes to the underlying data.


3 Answers

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
        }
    }
}
like image 185
MadeByDouglas Avatar answered Oct 19 '22 22:10

MadeByDouglas


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
    }
}

like image 34
nrj Avatar answered Oct 20 '22 00:10

nrj


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.

like image 20
cbjeukendrup Avatar answered Oct 19 '22 23:10

cbjeukendrup