Problem using onChange with Picker

I have a Picker that uses an enum for its selection variable. I have designated the selection variable as Equatable, but I get an error when I use the variable with onChange. Here’s the code:

//
//  ItemsView.swift
//  Bill & Task Manager
//
//  Created by Peter Lewis on 1/11/21.
//

import SwiftUI

enum Filter: Equatable { case all, bills, tasks }

struct ItemsView: View
{
  @AppStorage("showFilter") var showFilter = false
  @EnvironmentObject var periodicals: Periodicals
  @State private var showingSearchBar = false
  @State private var today = Date.now
  @State private var totalPastDue = 0.0
  @State private var searchText = ""
  @State private var filter = Filter.all

  let addButtonImage = Image(systemName: "plus")
  let undoButtonImage = Image(systemName: "arrow.uturn.backward")
  let searchButtonImage = Image(systemName: "magnifyingglass")
  let searchButtonImage2 = Image(systemName: "magnifyingglass.circle.fill")
  
  var title: String
  {
    switch(filter)
    {
      case .bills: return "Bills"
      case .tasks: return "Tasks"
      case .all:   return "Bills & Tasks"
    }
  }
  
  var dateLine: some View
  {
    let formattedDate = Text(today, style: .date)
    return Text("Today is \(formattedDate)")
            .padding(7)
            .foregroundColor(colorEmphasis)
            .background(colorContrast)
            .clipShape(RoundedRectangle(cornerRadius: 12))
  }

  var body: some View
  {
    NavigationStack.              // type '()' cannot conform to 'Equatable'
    {
      ZStack
      {
        color1.ignoresSafeArea()

        VStack
        {
          dateLine
          
          if periodicals.items.isEmpty
          {
            Spacer()
            
            HStack
            {
              Text("Tap")
              addButtonImage.font(.largeTitle)
              Text(", above, to add an item.")
            }
            Spacer()
          }
          else
          {
            if showFilter
            {
              Picker("Filter", selection: $filter)
              {
                Text("All").tag(Filter.all)
                Text("Bills").tag(Filter.bills)
                Text("Tasks").tag(Filter.tasks)
              }
              .pickerStyle(SegmentedPickerStyle())
              .fixedSize()
              .background(color2)
              .cornerRadius(5)
            }
            
            List
            {
              ForEach(filteredSearchResults)
              { item in
                NavigationLink(destination: EditView(item: item))
                { ItemRow(item: item) }
                  .listRowInsets(EdgeInsets(top: 0, leading: 10, bottom: 3, trailing: 0))
                  .listRowBackground(color2)
                  .padding([.top, .bottom], 2)
              }
              .onDelete(perform: removeItem)
              .listRowSeparatorTint(.black)
            }
            .listStyle(.plain)
            .cornerRadius(10)
            .if(showingSearchBar)
            { view in view.searchable(text: $searchText, prompt: "Search Name") }
            
            SummaryView(total: periodicals.dueInXDays)
            PastDueView(totalPastDue: periodicals.totalPastDue)
          }
        }
        .navigationTitle(title)
        .toolbar
        {
          ToolbarItemGroup(placement: .navigationBarTrailing)
          {
            if periodicals.previousItem != nil
            {
              Button(action: { periodicals.undo() })
              { undoButtonImage }
            }
            
            NavigationLink { AddView() }
              label: { addButtonImage }
            
            if !periodicals.items.isEmpty
            {
              Button(action: { showingSearchBar.toggle() })
              { showingSearchBar ? searchButtonImage2 : searchButtonImage}
            }
          }
        }
      }
      .navigationViewStyle(.stack)
      .alert(item: $periodicals.appError)
      { appAlert in
        Alert(title: Text(periodicals.errorHeading),
              message: Text(appAlert.errorString))
      }
      .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification))
      { _ in today = Date.now }
      .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification))
      { _ in periodicals.setNotifications() }
    }
    .onChange(of: filter).          // Cannot convert value of type 'Filter' to expected argument type '()'
    {
      let generator = UISelectionFeedbackGenerator()
      generator.selectionChanged()
    }
  }
  
  // The search text is trimmed of leading and trailing spaces
  // unless it consists of only spaces.
  
  var filteredSearchResults: [Periodical]
  {
    var searchText = self.searchText
    var items = periodicals.items
    
    if filter == .bills
    { items = items.filter { $0.price != nil }}
    else if filter == .tasks
    { items = items.filter { $0.price == nil }}
    
    if !searchText.trimmed().isEmpty
    { searchText = searchText.trimmed() }
    
    if searchText.isEmpty
    { return items }
    else
    { return items.filter
      { $0.name.localizedCaseInsensitiveContains(searchText) }
    }
  }

  private func removeItem(at offsets: IndexSet)
  {
    let id = filteredSearchResults[offsets.first!].id
    periodicals.removeItem(id: id)
  }
}

struct ItemsView_Previews: PreviewProvider {
    static var previews: some View {
      ItemsView()
        .environmentObject(Periodicals())
    }
}

The perform closure you pass to onChange takes a parameter (the new value of the property being observed), which you need to account for in your code. Since you aren’t using the value of filter in your closure, you can do this:

.onChange(of: filter)
    { _ in
      let generator = UISelectionFeedbackGenerator()
      generator.selectionChanged()
    }

See if that fixes your error.

It does indeed! Thank you.

I don’t know why I couldn’t figure that out from the error messages.

Because SwiftUI’s error messages are… lacking, let’s say.

To be fair, when you specify the modifier you get code completion like this:

and you do get a closure that gives you newValue in when you hit return on the completion handler (Equatable) -> Void like this:

1 Like

Thus the ‘()’ for the missing (Equatable). I should pay more attention to the completion. I usually just ignore it.

Learned something again. Thanks.