Вложенные списки SwiftUI. Работа со вложенными ViewModel

swiftui

#1

Коллеги, кто давно балуется swiftUI, подскажите что не так:

модель

struct ListModel: Identifiable {
    var id = UUID()
    var title: String
    var isExpand: Bool
    var isComplete: Bool
    var subLists: [ListModel]
}

С рекурсией для вью вроде решил, но почему-то VStack весь не видно при вложенных данных:

struct ListView: View {
    var listData: [ListModel]
    
    var body: some View {
        
        List(listData) { list in
            return VStack {
                ListRow(listData: list)
                if !list.subLists.isEmpty {
                    ListView(listData: list.subLists)
                }
            }
        }
        .navigationBarTitle("Lists")
    }
}

видно, что ячейка есть, но VStack как-то коряво растягивает не всё…
27

Или какое-то стандартное решение есть? Я что-то не нашёл.


#2

Как обычно, написал вопрос и потом дошло )))

через frame конечно надо просто увеличить минимальную высоту VStack:

.frame(minHeight: calcHeight(subList: list.subLists))

private func calcHeight(subList: [ListModel])->CGFloat {
    return CGFloat(subList.count * 46 + 30)
}

Это решение оказалось в корне неправильное: не нужно самому считать высоту фрейма - это не сработает уже на 3м уровне вложенности.

Правильное решение: вложенный список оборачивать в Section, тогда фрейм автоматом высоту высчитает (пример кода зацикленной ячейки):

Section {
    ForEach (listRowVM.subListRowsVM) { listRowVM in
          ListRowView(listRowVM: listRowVM, complete: listRowVM.listRow.isComplete)
     }
 }.padding(.leading, 20)

41

PS не помешает раздел “SwiftUI” на форуме, чтобы легче было искать.


#3

Коллеги, опять есть недопонимание, теперь в работе MVVM:

Есть 2 вложенных модели данных:

  1. Список, в котором есть строки. Вот модель строки

    class ListRowModel: Identifiable, ObservableObject {
        let id = UUID()
        var title: String
        var isExpand: Bool
        var isComplete: Bool
        var subLists: [ListRowModel]
     
        init(title: String, isExpand: Bool, isComplete: Bool, subLists: [ListRowModel]) {
            self.title = title
            self.isExpand = isExpand
            self.isComplete = isComplete
            self.subLists = subLists
        }
    }
    
  2. Модель списка у котрого есть ещё свои свойства, помимо массива строк:

    class ListModel: Identifiable, ObservableObject {
        let id = UUID()
        var title: String
        var listRows: [ListRowModel]
        var systemImage: String
        var colorSystemImage: Color
     
        init(title: String, listRows: [ListRowModel], systemImage: String, colorSystemImage: Color) {
            self.title = title
            self.listRows = listRows
            self.systemImage = systemImage
            self.colorSystemImage = colorSystemImage
        }
    }
    

Вьюмодели:

  1. вьюмодель массива списков. Данные беруться из тестового массива (глобал) просто для проверки работы.

    class ListsViewModel: ObservableObject, Identifiable {
        @Published var lists: [ListViewModel] = []
     
        init() {
            ListOfLists.forEach { list in
                lists.append(ListViewModel(list: list))
            }
        }
    }
    
  2. вьюмодель одного списка

    class ListViewModel: ObservableObject, Identifiable {
        let id: UUID
        @Published var list: ListModel
        @Published var listRowsVM: [ListRowViewModel] = []
     
        init(list: ListModel) {
            self.list = list
            self.id = list.id
         
            list.listRows.forEach({ row in
                self.listRowsVM.append(ListRowViewModel(listRow: row))
            })
        }
    }
    
  3. вьюмодель строки

    class ListRowViewModel: ObservableObject, Identifiable {
        @Published var listRow: ListRowModel
     
        init(listRow: ListRowModel) {
            self.listRow = listRow
        }
    }
    

Так, теперь вьюшки (которые важны) от родительской до последне дочерней с самой строкой:

    struct ListsView: View {
        
        @ObservedObject var listsVM = ListsViewModel()
        
        var body: some View {
            
            NavigationView {
                List(listsVM.lists) { list in
                    
                    NavigationLink(destination: ListDetailView(listVM: list)) {
                        HStack {
                            IconImageView(image: list.list.systemImage, color: list.list.colorSystemImage, imageScale: 16)
                            Text("\(list.list.title)")
                            Spacer()
                            Text("\(list.listRowsVM.count)")
                        }
                    }
                    
                }
                .navigationBarTitle("Lists")
            }     
        }
    }


struct ListDetailView: View {
    
    @ObservedObject var listVM: ListViewModel
    
    var body: some View {
        
        List(listVM.listRowsVM) { listRowVM in
            ListRowView(listRowVM: listRowVM, complete: listRowVM.listRow.isComplete)
        }
        .navigationBarTitle("\(listVM.list.title)")
    }
    
}

и последнее вью самой строки:

struct ListRowView: View {
    
    @ObservedObject var listRowVM: ListRowViewModel
    //@State var complete: Bool
    
    var body: some View {
        HStack {
            Image(systemName: isCompleteCheck(isComplete: /*complete*/ listRowVM.listRow.isComplete))
                .onTapGesture {
                    self.listRowVM.listRow.isComplete.toggle()
                    //self.complete.toggle()
            }
            Text("\(listRowVM.listRow.title)")
        }
    }
    
    private func isCompleteCheck(isComplete: Bool) -> String {
        return isComplete ? "checkmark.circle.fill" : "circle"
    }
}

Внимание на закомментированный код, но об этом позже.

Везде в моделях данные в @Published и локальные переменные во вью все @ObservedObject - вроде всё как надо.
Проблема в том, что при работе только с вьюмоделью при нажатии на чекбокс интрефейс не обновляется, хотя изменения в данные вносятся. И если вернуться назад в список списков и снова зайти в список, то всё отобразиться как надо с чеками там, куда тапнули.
Заставил работать только через дополнительную переменную @State взамен прямых данных из вьюмодели (закомментированно), в которую отдельно передаю свойство isComplete из вьюмодели, но это же костыль!

Почему не отправляются/принимаются уведомления об изменении состояния переменных во вьюмоделях понять не могу. Всё перерыл - ответа нет. Может что в UI изменилось. И чую собака где-то в Combine зарыта )))


#4

Продолжаю монолог как обычно ))))

Оказалось, что издатель(или подписчик) именно в swiftUI (возможно в Combine так можно) не работает со вложенными свойствами в другие объекты, даже если те обозначены как издатель (@Published).

Решение: все вложенные свойства нужно перезаписывать в локальные переменные во вьюмодель.

final class ListRowViewModel: ObservableObject, Identifiable {
    @Published var title: String
    @Published var isExpand: Bool
    @Published var isComplete: Bool
    @Published var subListRowsVM: [ListRowViewModel] = []
    
    init(listRow: ListRowModel) {
        self.title = listRow.title
        self.isExpand = listRow.isExpand
        self.isComplete = listRow.isComplete
        
        listRow.subLists.forEach { subListRow in
            self.subListRowsVM.append(ListRowViewModel(listRow: subListRow))
        }
    }
}

Пример работы рекурсии с вьюМодель и правильных свойствах:
Видео