The story of one modal window or moving from UIKit to SwiftUI. Part 2.1. Unexpected Combine bug

In this article I initially planned to write a continuation of the first part of the article. Namely: show the promised ProgressView and SkeletonView. But then an unexpected obstacle appeared on my way.

First things first.

We understand that just leaving the View with all the functionality inside is a shame. Usually I put the quick functionality and UI in one class, and then separate and complicate it. I use MVVM architecture. And the modal window was no exception. After checking that everything worked in the View, I created a ViewModel and made it an ObservableObject

class ReportsViewModel: ObservableObject {
    @Published var ReportData: [Report]?
    @Published var currentItem: Int
...
  }

The View itself, accordingly, is now responsible only for the UI and the only thing it knows about is its model.

struct ReportsView: View {
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var viewModel: ReportsViewModel
  ...
  }

Once again it would seem, what could go wrong? This is where great disappointment awaited me.

When I collected all the functionality in the View, I assigned all the variables as @ObservedObject. Accordingly, inside the View we track the state of the ReportData via @ObservedObject. And also several variables were updated through delegates from API methods (for example, deleting an article or like/dislike).

When we transfer the model to ReportsViewModel, we also monitor the entire model and each variable inside. But, as it turns out, Combine cannot track state changes through model tracking, neither through variables nor through delegates. That is, when we store everything inside the View, we perfectly track changes from the parent controller. If the View is initiated with the ViewModel, then that’s it, we don’t have access to the parent variables.

This was quite a disappointment for me. Despite the fact that Chat GPT stubbornly insisted that I should put @Published everywhere and I would be happy (it really pissed me off!). I had to Google it the old fashioned way. From this article I learned that the problem is not new, but has been going on since 2021. Looking ahead, I will say that the solution proposed in the article did not work. Moreover, I thought that since it doesn’t work at the moment, then perhaps this implementation was simply “fixed”, which means…

I fell asleep with approximately these thoughts that day

I fell asleep with approximately these thoughts that day

Well, we need to somehow solve the problem. Here's how Not good I did it elegantly. When initializing a modal window, we pass a closure there, which is triggered inside the View. We put the update of our data into this closure.

Using ReportsView as an example:

let viewModel = ViewModel( ... здесь обычные переменные)
viewModel.onProgressComplete = {
  viewModel.progress = self.progress // Сюда кладём обновленный прогресс с апи
}
                
let swiftUIView = ReportsView(viewModel: viewModel)
                
let hostingController = UIHostingController(rootView: swiftUIView)
hostingController.modalPresentationStyle = .automatic
hostingController.view.backgroundColor = .clear
hostingController.hidesBottomBarWhenPushed = true
DispatchQueue.main { [weak self] in
  self.present(hostingController, animated: true, completion: nil)
}

Well, in the model it looks like this:

class ReportsViewModel: ObservableObject {
    var onProgressComplete: (() -> Void)? // Вызываем при нажатии на кнопку или где необходимо

  ...
}

The problem is that in the end I had to insert about 5 closures in a circle (in the controller, in the service class that implements this particular Api, etc. in a circle) just to track progress.

Share in the comments, have you encountered this or was there no need to track objects from the parent class? How did you decide?

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *