История одного модального окна или переходим с UIKit на SwiftUI. Часть 2.1. Неожиданный баг Combine

от автора

В этой статье изначально планировала написать продолжение первой части статьи. А именно: показать обещанные 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 замыканий по кругу (в контроллер, в сервис класс, реализующий этот конкретный Апи и тд по кругу), только чтобы отслеживать прогресс.

Делитесь в комментариях, сталкивались ли с этим или не было необходимости в отслеживании объектов из родительского класса? Как решали?


ссылка на оригинал статьи https://habr.com/ru/articles/851786/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *