У меня есть два представления 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
}
}
Не смешивайте 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
}
}
}
Не в этом случае вы можете использовать id: \.0
, который должен быть таким же, как id: \.offset
в данном конкретном случае. Лучшим вариантом будет использование ForEach(categoriesListViewModel.categories)
вы также можете использовать id: \.1.id
, но поскольку массив прост, я не вижу преимущества в этом конкретном случае. В общем, избегайте использования этой enumerated()
настройки и используйте ForEach(categoriesListViewModel.categories){...}
Мне нравится ваше решение не использовать индекс, потому что оно выглядит чистым, но мне интересно, не менее ли оно эффективно, поскольку вы запускаете forEach
внутри forEach
Хорошо, обновил свой ответ, используя более эффективный подход, если вас беспокоит время обработки цикла forEach.
В SwiftUI структуры представления уже являются моделью представления (вам не нужны собственные объекты), а данные динамического представления, такие как выделение, должны быть @State
отделены от модели, например
@State var selectedCategory: Category?
var body: some View {
List(selection: $selectedCategory) {
ForEach(model.categories) {
...
body
вызывается при изменении выбора.
Идентификатор в ForEach должен основываться на идентификаторе элемента, а не на смещении, верно?