В этой статье изначально планировала написать продолжение первой части статьи. А именно: показать обещанные ProgressView и SkeletonView. Но тут на моём пути возникло неожиданное препятствие.
Обо всём по порядку.
Мы же понимаем, что просто так оставить View со всем функционалом внутри - такое себе. Обычно я накидываю быстрый функционал и UI в одном классе, а затем уже разделяю и усложняю. Использую MVVM архитектуру. И модальное окно не стало исключением. Проверив, что всё работает во View, я создала ViewModel, сделала её ObservableObject
class ReportsViewModel: ObservableObject {
@Published var ReportData: [Report]?
@Published var currentItem: Int
...
}
Сама View соответственно теперь отвечает только за UI и единственное о чем она знает - это о своей модели.
struct ReportsView: View {
@Environment(\.presentationMode) var presentationMode
@ObservedObject var viewModel: ReportsViewModel
...
}
В очередной раз казалось бы, что может пойти не так? Вот тут-то меня и ждало великое разочарование.
Когда я собирала весь функционал во View - я назначила все переменные как @ObservedObject. Соответственно, внутри View мы отслеживаем состояние ReportData через @ObservedObject. А так же несколько переменных обновлялись через делегаты от апи методов (например удаление статьи или лайк/дизлайк).
При переносе модели в ReportsViewModel мы так же следим за всей моделью и за каждой переменной внутри. Но, как оказалось, Combine не умеет отслеживать изменение состояния через отслеживание модели, ни через переменные, ни через делегаты. То есть когда мы храним всё внутри View - мы прекрасно отслеживаем изменения из родительского контроллера. Если же View инициирована с ViewModel - то всё, мы не имеем доступа к родительским переменным.
Это было довольно сильным разочарованием для меня. При том что Chat GPT упорно утверждал, что надо поставить везде @Published и будет мне счастье (аж выбесил!). Пришлось гуглить по старинке. Из этой статьи узнала что проблема не нова, а тянется аж с 2021 года. Забегая вперед, скажу, что предложенный в статье вариант решения не сработал. Более того, я подумала, что раз на данный момент это не работает, то возможно такую реализацию просто "починили", а это значит...
Ну что же, надо как-то решать проблему. Вот как не очень элегантно я сделала. При инициализации модального окна мы передаём туда замыкание, которое срабатывает уже внутри View. В это замыкание кладём обновление наших данных.
На примере ReportsView:
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)
}
ну и в модели это выглядит так:
class ReportsViewModel: ObservableObject {
var onProgressComplete: (() -> Void)? // Вызываем при нажатии на кнопку или где необходимо
...
}
Проблема в том, что по итогу пришлось воткнуть штук 5 замыканий по кругу (в контроллер, в сервис класс, реализующий этот конкретный Апи и тд по кругу), только чтобы отслеживать прогресс.
Делитесь в комментариях, сталкивались ли с этим или не было необходимости в отслеживании объектов из родительского класса? Как решали?