skip to Main Content

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


  1. Do not mix ObservableObject and @Observable (eg your CategoryViewModel).
    Also when you use Identifiable you need to have a let id....

    Try this approach cleaning up the ForEach in CategoriesListView and removing the @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
         }
     }
    

    or without using index, much better and recommended

            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
                    }
            }
    

    and

    @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
            }
        }
    }
    

    This is assuming you have have declared
    @State private var model = CategoriesListViewModel(....) in your view hierarchy
    and pass this model down using .environment(model)

    Login or Signup to reply.
  2. 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.

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

    body is called when the selection changes.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search