1

I have a SwiftUI app that uses UIViewControllerRepresentable to show a UIViewController. And the view controller uses a hosting controller to show a SwiftUI view inside it.

RoomView (SwiftUI file)

struct RoomView: View {
    
    var body: some View {
        NavigationView {
            VStack {
                RoomUIView()
            }
            .ignoresSafeArea()
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}


// UIViewControllerRepresentable to show a UIView in SwiftUI
struct RoomUIView: UIViewControllerRepresentable {
    typealias UIViewControllerType = TestVC
    
    func makeUIViewController(context: Context) -> TestVC {
        let vc = TestVC()
        return vc
    }
    
    func updateUIViewController(_ uiViewController: TestVC, context: Context) {
        // Updates the state of the specified view controller with new information from SwiftUI.
    }
}

TestVC (UIKit file)

class TestVC: UIViewController {
    
    var testTimerFile = TestTimerFile()
    lazy var hostingController = UIHostingController(rootView: testTimerFile)
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        hostingController = UIHostingController(rootView: testTimerFile)
        
        // Add the hosting controller's view as a child view
        addChild(hostingController)
        view.addSubview(hostingController.view)
        
        // Set up constraints
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        hostingController.didMove(toParent: self)
        
        
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            self.testTimerFile.startTimer()
        }
    }
}

TestTimerFile (SwiftUI file)

struct TestTimerFile: View {
    @State var timeRemaining = 30
    
    var body: some View {
        Text("\(timeRemaining)")
            .onAppear {
                startTimer()
            }
    }
    
    func startTimer() {
        print(timeRemaining)
        if timeRemaining > 0 {
            timeRemaining -= 1
            
        } else {
            timeRemaining = 30
        }
    }
    
}

TestVC has a timer that calls a function inside the TestTimerFile every second. That function is supposed to update the time remaining in the SwiftUI View but for some reason it doesn't update.

Note: I encountered this error when building an actual app, I cannot put the actual code here as it might be confusing and too long. The code above is just some sample code to replicate this error I was facing.

Abdullah Ajmal
  • 431
  • 5
  • 17
  • 1
    You can’t hold on to SwiftUI views in properties. There is an underlying storage that isn’t accessible from the outside. You can use a common source of truth that is a class – lorem ipsum Jul 12 '23 at 13:57
  • Does this answer your question? [Call UIKit function from SwiftUI](https://stackoverflow.com/questions/70435695/call-uikit-function-from-swiftui) – lorem ipsum Jul 12 '23 at 13:58
  • I don't quite understand the question in the link you have provided. Could you please explain how I could do the same with my case? @loremipsum – Abdullah Ajmal Jul 12 '23 at 14:55

1 Answers1

1

When using a UIHostingController, you can change the underlying SwiftUI View by setting the property rootView.

A SwiftUI View is used to a) create an underlying view (rendering pixel) and b) mutate the state of that underlying view, so that it renders differently.

Note also, that the lifetime of a SwiftUI view value is just as long as it takes to make the creation or mutation, while the underlying view will exist for longer.

A @State property declares, that the underlying view should allocate storage for itself. The storage's lifetime is bound to the underlying view. A @State property (very likely) has only an effect on the underlying view, when it is created – beyond the fact, that the view uses it for managing private state while it executes it's body. If you keep a SwiftUI view value elsewhere which has no associated underlying view, it's very likely a @State property has no effect at all (which is good, since if it had, it would be a flaw).

So, when you set the property rootView you basically tell the hosting controller to use this SwiftUI View value to mutate the underlying view which renders the pixels.

You should set the rootView every time when you want your view to change.

Your TestVC may act as the "Model" which is responsible to calculate the new data (aka "view state") which should be rendered.

Now, it should become simple:

Make a SwiftUI View:

struct TestTimerFile: View {
    let timeRemaining: Int
    
    var body: some View {
        Text("\(timeRemaining)")
    }
}

Note that the SwiftUI view is used to tell how the underlying view should be created – respectively modified.

In your TestVC:

func update(_ viewState: Int) {
   self.hostingController.rootView = .init(timeRemaining: viewState) 
}

Call update(_:) whenever the timer fires.

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67