I have two Swift UI views, CategoriesListView
and CategoryView.
CategoriesListView
works sort of like a navigation menu and CategoryView
is a view that can be tapped. I’m trying to attempt the following:
- All the categories start off un-highlighted except the first category.
- If user selects one of the categories in the list (
CategoryView
is selected), that category displays a highlighted image - Whatever category was selected before gets un-highlighted and displays an un-highlighted image
Unfortunately once something is tapped, the category views don’t get changed. Do I have to do something in the view model? I am pretty new to SwiftUI and am still learning a lot
my views:
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
}
}
}
my view models:
@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
}
}
2
Answers
Do not mix
ObservableObject
and@Observable
(eg your CategoryViewModel).Also when you use
Identifiable
you need to have alet id...
.Try this approach cleaning up the
ForEach
inCategoriesListView
and removing the@Bindable var categoriesListViewModel = categoriesListViewModel
or without using index, much better and recommended
and
This is assuming you have have declared
@State private var model = CategoriesListViewModel(....)
in your view hierarchyand pass this model down using
.environment(model)
In SwiftUI the View structs are the view model already (you don’t need your own objects) and dynamic view data like selection should be
@State
that is separate from the model, e.g.body
is called when the selection changes.