Почему мое представление не обновляется с использованием архитектуры MVVM?

У меня есть два представления Swift UI: CategoriesListView и CategoryView.CategoriesListView работают как меню навигации, а CategoryView — это представление, которое можно нажать. Я пытаюсь сделать следующее:

  • Все категории начинаются с невыделения, кроме первой категории.
  • Если пользователь выбирает одну из категорий в списке (выбран CategoryView), для этой категории отображается выделенное изображение.
  • Какая бы категория ни была выбрана ранее, она не выделяется и отображается невыделенное изображение.

К сожалению, если что-то нажать, представления категорий не изменяются. Нужно ли мне что-то делать в модели представления? Я новичок в SwiftUI и все еще многому учусь.

мои взгляды:

struct CategoriesListView: View {
    @Environment(CategoriesListViewModel.self) var categoriesListViewModel
    
    var selectedIndex: Int { categoriesListViewModel.categories.firstIndex(where: {
            $0.isSelected == true
        }) ?? 0
    }
    
    var body: some View {
        @Bindable var categoriesListViewModel = categoriesListViewModel
        ScrollView(.horizontal) {
            HStack(alignment: .top, spacing: 12) {
                Spacer()
                // With each category in the categories list view model, create a category view. If the category view gets tapped, change that view to be the selected image. the previously selected view shows an unselected image.
                ForEach(Array(categoriesListViewModel.categories.enumerated()), id: \.offset) { index, element in
                    CategoryView(categoryViewModel: $categoriesListViewModel.categories[index]).onTapGesture {
                        categoriesListViewModel.categories[selectedIndex].isSelected = false
                        categoriesListViewModel.categories[index].isSelected = true
                        CategoryView(categoryViewModel: $categoriesListViewModel.categories[index])
                    }
                }
            }
        }
    }
}

struct CategoryView: View {
    @Binding var categoryViewModel: CategoryViewModel
    
    var body: some View {
        if categoryViewModel.isSelected {
            categoryViewModel.category.highlightedIconImage
        } else {
            categoryViewModel.category.iconImage
        }
    }
}

мои модели просмотра:


@Observable
class CategoriesListViewModel {
    var categories: [CategoryViewModel] = []
    var currentSelection: Int = 0
    var previousSelection: Int? = nil
    
    init(categories: [CategoryViewModel], currentSelection: Int, previousSelection: Int? = nil) {
        self.categories = setCategoriesToShow()
        self.currentSelection = currentSelection
        self.previousSelection = previousSelection
    }
    
    func setCategoriesToShow() -> [CategoryViewModel] {
        var categoriesToShow = [CategoryViewModel]()
        let resortCategory = Category(
            identifier: "a",
            title: "Resort",
            outfits: [],
            iconImage: Image("ResortUnselected"),
            highlightedIconImage: Image("ResortSelected")
        )
        
        var europeCategory = Category(
            identifier: "b",
            title: "Europe",
            outfits: [],
            iconImage: Image("EuropeUnselected"),
            highlightedIconImage: Image("EuropeSelected")
        )
        
        var brunchCategory = Category(
            identifier: "c",
            title: "Brunch",
            outfits: [],
            iconImage: Image("BrunchUnselected"),
            highlightedIconImage: Image("BrunchSelected")
        )
        
        var athleisureCategory = Category(
            identifier: "d",
            title: "Athleisure",
            outfits: [],
            iconImage: Image("AthleisureUnselected"),
            highlightedIconImage: Image("AthleisureSelected")
        )
        
        var workCategory = Category(
            identifier: "e",
            title: "Work",
            outfits: [],
            iconImage: Image("WorkUnselected"),
            highlightedIconImage: Image("WorkSelected")
        )
        
        categoriesToShow.append(CategoryViewModel(category: resortCategory, isSelected: true))
        categoriesToShow.append(CategoryViewModel(category: europeCategory, isSelected: false))
        categoriesToShow.append(CategoryViewModel(category: brunchCategory, isSelected: false))
        categoriesToShow.append(CategoryViewModel(category: athleisureCategory, isSelected: false))
        categoriesToShow.append(CategoryViewModel(category: workCategory, isSelected: false))
        
        return categoriesToShow
    }
}

class CategoryViewModel: ObservableObject, Identifiable {
    var category: Category
    var isSelected: Bool
    
    init(category: Category, isSelected: Bool) {
        self.category = category
        self.isSelected = isSelected
    }
}
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
74
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Не смешивайте ObservableObject и @Observable (например, вашу CategoryViewModel). Также, когда вы используете Identifiable, вам необходимо иметь let id....

Попробуйте этот подход: очистите ForEach в CategoriesListView и удалите @Bindable var categoriesListViewModel = categoriesListViewModel.

 ForEach(Array(categoriesListViewModel.categories.enumerated()), id: \.offset) { index, element in
     CategoryView(categoryViewModel: categoriesListViewModel.categories[index])
         .onTapGesture {
         categoriesListViewModel.categories[selectedIndex].isSelected = false
         categoriesListViewModel.categories[index].isSelected = true
     }
 }

или без использования индекса, намного лучше и рекомендуется

        ForEach(categoriesListViewModel.categories) { category in
            CategoryView(categoryViewModel: category)
                .onTapGesture {
                    // turn off all selections
                    categoriesListViewModel.categories.forEach{
                        $0.isSelected = false
                    }
                    // turn on only this one
                    category.isSelected = true
                }
        }

и

@Observable
 class CategoryViewModel: Identifiable {
     let id = UUID()
     var category: Category
     var isSelected: Bool
     
     init(category: Category, isSelected: Bool) {
         self.category = category
         self.isSelected = isSelected
     }
 }

struct CategoryView: View {
    var categoryViewModel: CategoryViewModel
    
    var body: some View {
        if categoryViewModel.isSelected {
            categoryViewModel.category.highlightedIconImage
        } else {
            categoryViewModel.category.iconImage
        }
    }
}

Это предполагает, что вы заявили @State private var model = CategoriesListViewModel(....) в иерархии представлений и передайте эту модель, используя .environment(model)

РЕДАКТИРОВАТЬ-1

Хорошо, если у вас десятки тысяч категорий, то обработка может занять несколько миллисекунд. в цикле forEach. Следующие более эффективные код использует дополнительную переменную, чтобы отслеживать, какую категорию отключить, вместо использования уродливого цикла forEach.

struct CategoriesListView: View {
    @Environment(CategoriesListViewModel.self) var categoriesListViewModel
    @State private var prev: CategoryViewModel? // <--- here
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack(alignment: .top, spacing: 12) {
                Spacer()
                ForEach(categoriesListViewModel.categories) { category in
                    CategoryView(categoryViewModel: category)
                        .onTapGesture {
                            // turn on the category
                            category.isSelected = true
                            // turn off prev category
                            prev?.isSelected = false
                            // register the new prev
                            prev = category
                        }
                }
            }
        }
        .onAppear {
            // since you set the currentSelection=0
            prev = categoriesListViewModel.categories.first  // <--- here
        }
    }
}

Идентификатор в ForEach должен основываться на идентификаторе элемента, а не на смещении, верно?

jnpdx 09.08.2024 05:35

Не в этом случае вы можете использовать id: \.0, который должен быть таким же, как id: \.offset в данном конкретном случае. Лучшим вариантом будет использование ForEach(categoriesListViewModel.categories)

workingdog support Ukraine 09.08.2024 06:11

вы также можете использовать id: \.1.id, но поскольку массив прост, я не вижу преимущества в этом конкретном случае. В общем, избегайте использования этой enumerated() настройки и используйте ForEach(categoriesListViewModel.categories){...}

workingdog support Ukraine 09.08.2024 06:27

Мне нравится ваше решение не использовать индекс, потому что оно выглядит чистым, но мне интересно, не менее ли оно эффективно, поскольку вы запускаете forEach внутри forEach

jane-bun 09.08.2024 18:30

Хорошо, обновил свой ответ, используя более эффективный подход, если вас беспокоит время обработки цикла forEach.

workingdog support Ukraine 10.08.2024 01:34

В SwiftUI структуры представления уже являются моделью представления (вам не нужны собственные объекты), а данные динамического представления, такие как выделение, должны быть @State отделены от модели, например

@State var selectedCategory: Category?

var body: some View {
    List(selection: $selectedCategory) {
        ForEach(model.categories) {
    ...

body вызывается при изменении выбора.

Другие вопросы по теме