BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

SOLVED: Picker not allowing selection

Forums > SwiftUI

This is, in part, a follow-up to my question last week.

I have an enum of options for filtering in my library management app:

enum Filtering: String, CaseIterable {
    case none = "Reset"
    case title = "Title"
    case author = "Author"
    case series = "Series"
    case collection = "Collection"
    case genre = "Genre"
    case keyword = "Keyword"
    case rating = "Rating"
    case language = "Language"

All but the last two options are fairly straightforward text-filtering for which I adapted this HWS+ tutorial.

The last two require picker views, however. But I'm running into a strange problem when I attempt to work those picker views into a view that encompasses all the optios:

This works as intended:

    var ratings = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
    @State private var starRatingSelection: Double = 0.0

    var body: some View  {
        return NavigationView {
            VStack {
                Picker(selection: $starRatingSelection,
                       label: Text("Search by rating"),
                       content: {
                        ForEach(ratings, id: \.self) { rating in
                            viewForRating(rating)
                        }
                       })
                List(library.database.books.filtered(starRating: starRatingSelection)) { book in
                    let bookObject = AudiobookObject(book)
                    BookRowView(audiobook: bookObject)
                }
                .listStyle(InsetListStyle())
            }
            .navigationBarTitle("Rating")
        }
    }

This, however, does not:

    var ratings = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
    @State private var starRatingSelection: Double = 0.0
    @State private var filteredBooks = [Audiobook]()

    var body: some View {
        switch filtering {
            case .title: byTitleView
            case .author: byAuthorView
            case .series: bySeriesView
            case .collection: byCollectionView
            case .genre: byGenreView
            case .rating: starRatingPickerView
            case .language: languagePickerView
            case .keyword: byKeywordView
            default: LibraryView()
        }
    }

    private func applyFilter() {
        switch filtering {
            case .title: filteredBooks = library.database.books.sorted(by: filterString)
            case .author: filteredAuthors = library.database.authors.filtered(by: filterString)
            case .series: filteredSeries = library.database.series.sorted(by: filterString)
            case .collection: filteredCollections = library.database.collections.sorted(by: filterString)
            case .genre: filteredGenres = library.database.genres.sorted(by: filterString)
            case .rating: filteredBooks = library.database.books.filtered(starRating: starRatingSelection)
            case .language: filteredBooks = library.database.books.booksByLanguage(languageSelection)
            case .keyword:
                filteredBooks = library.database.books.booksByKeyword(filterString)
                filteredSeries = library.database.series.filtered(by: filterString)
                filteredCollections = library.database.collections.filtered(by: filterString)
            default: filteredBooks = library.database.books.sorted(by: "")
        }
    }

    private var starRatingPickerView: some View  {
        return NavigationView {
            VStack {
                Picker(selection: $starRatingSelection,
                       label: Text("Search by rating"),
                       content: {
                        ForEach(ratings, id: \.self) { rating in
                            viewForRating(rating)
                        }
                       })
                List(filteredBooks) { book in
                    let bookObject = AudiobookObject(book)
                    BookRowView(audiobook: bookObject)
                }
                .onAppear(perform: applyFilter)
                .listStyle(InsetListStyle())
            }
            .navigationBarTitle("Rating")
        }
    }

Which... doesn't make sense to me. The only difference between the two (at least so far as the picker portion is concerned) is that instead of:

                List(library.database.books.filtered(starRating: starRatingSelection)) { book in
                    let bookObject = AudiobookObject(book)
                    BookRowView(audiobook: bookObject)
                }

I'm using:

                List(filteredBooks) { book in
                    let bookObject = AudiobookObject(book)
                    BookRowView(audiobook: bookObject)
                }

I can get around it by bypassing applyFilter() and just making that one change, and it works:

private func applyFilter() {
        switch filtering {
            case .title: filteredBooks = library.database.books.sorted(by: filterString)
            case .author: filteredAuthors = library.database.authors.filtered(by: filterString)
            case .series: filteredSeries = library.database.series.sorted(by: filterString)
            case .collection: filteredCollections = library.database.collections.sorted(by: filterString)
            case .genre: filteredGenres = library.database.genres.sorted(by: filterString)
            case .keyword:
                filteredBooks = library.database.books.booksByKeyword(filterString)
                filteredSeries = library.database.series.filtered(by: filterString)
                filteredCollections = library.database.collections.filtered(by: filterString)
            default: filteredBooks = library.database.books.sorted(by: "")
        }
    }

    private var starRatingPickerView: some View  {
        return NavigationView {
            VStack {
                Picker(selection: $starRatingSelection,
                       label: Text("Search by rating"),
                       content: {
                        ForEach(ratings, id: \.self) { rating in
                            viewForRating(rating)
                        }
                       })
                List(library.database.books.filtered(starRating: starRatingSelection)) { book in
                    let bookObject = AudiobookObject(book)
                    BookRowView(audiobook: bookObject)
                }
                .listStyle(InsetListStyle())
            }
            .navigationBarTitle("Rating")
        }
    }

But I'd much rather understand WHY it isn't working, rather than just getting around it with guesswork.

3      

Hey, it's me again 😂

TLDR

The only thing you're missing is an

.onChange(of: starRatingSelection) { _ in
    applyFilter()
}

block on your starRatingView.

Disclaimer

I don't have all the code back code you have, so I had to simplify it quite a bit, but it should work nonetheless. Here's the view I am using:

extension String: Identifiable {
    public var id: String { return self }
}

struct TestView: View {
    static let allBooks = (0..<100).map({ "\(Double($0) / 2)" })
    var ratings = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
    @State private var starRatingSelection: Double = 2.5
    @State private var filteredBooks = [String]()

    var body: some View {
        starRatingPickerView
    }

    private func applyFilter() {
        filteredBooks = Self.allBooks.filter { (Double($0) ?? 0).truncatingRemainder(dividingBy: 5) == starRatingSelection }
    }

    private var starRatingPickerView: some View  {
        return NavigationView {
            VStack {
                Picker(selection: $starRatingSelection, label: Text("Search by rating")) {
                    ForEach(ratings, id: \.self) { rating in
                        Text("\(rating, specifier: "%.1f")")
                    }
                }
                List(filteredBooks) { book in
                    Text("Book: \(book)")
                }
                .onAppear(perform: applyFilter)
                .listStyle(InsetListStyle())
            }
            .navigationBarTitle("Rating")
        }
    }
}

Notable changes I made:

  • I'm just using the numbers 0-50 in increments of 0.5 for my book names, and name % 5 for the ratings of the books just to avoid having to write up a book struct
  • My default books list isn't in a data store but a static constant on my view
  • I'm only showing the rating filter, not the others
  • I'm only using the string rep of the rating as the ratingView
  • I'm only using Text("Book \(bookname)") as my list row view

None of those changes should break the answer.

Now the actual answer

The first step I take when debugging something like this is to check whether your @State variable actually gets changed when it's supposed to. Recall that @State basically acts as a class wrapping your state variable, which means that attaching a didSet observer to it (@State private var starRatingSelection: Double = 2.5 { didSet { print("change") } }) wouldn't work. You need to use the dedicated swiftUI onChange(of:perform:) modifier to listen to the change of the state variable.

private func applyFilter(newRating: Double) {
    filteredBooks = Self.allBooks.filter { (Double($0) ?? 0).truncatingRemainder(dividingBy: 5) == newRating }
}

private var starRatingPickerView: some View  {
    return NavigationView {
        VStack {
            ...
        }
        .navigationBarTitle("Rating")
        .onChange(of: starRatingSelection) { rating in
            print("Rating Change: \(rating)")
        }
    }
}

When you run it you see your rating variable is changing as expected, so the issue isn't with the picker but with the list showing the filtered information. Which brings me to the issue you have: you're never updating your filteredBooks array. You run the applyFilter() function once in your onAppear(perform:), but never again. So you add applyFilter to the onChange block, and now it updates:

private var starRatingPickerView: some View  {
    return NavigationView {
        VStack {
            ...
        }
        .navigationBarTitle("Rating")
        .onChange(of: starRatingSelection) { _ in
            applyFilter()
        }
    }
}

Full view:

extension String: Identifiable {
    public var id: String { return self }
}

struct TestView: View {
    static let allBooks = (0..<100).map({ "\(Double($0) / 2)" })
    var ratings = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
    @State private var starRatingSelection: Double = 2.5
    @State private var filteredBooks = [String]()

    var body: some View {
        starRatingPickerView
    }

    private func applyFilter() {
        filteredBooks = Self.allBooks.filter { (Double($0) ?? 0).truncatingRemainder(dividingBy: 5) == starRatingSelection }
    }

    private var starRatingPickerView: some View  {
        return NavigationView {
            VStack {
                Picker(selection: $starRatingSelection, label: Text("Search by rating")) {
                    ForEach(ratings, id: \.self) { rating in
                        Text("\(rating, specifier: "%.1f")")
                    }
                }
                List(filteredBooks) { book in
                    Text("Book: \(book)")
                }
                .onAppear(perform: applyFilter)
                .listStyle(InsetListStyle())
            }
            .navigationBarTitle("Rating")
            .onChange(of: starRatingSelection) { _ in
                applyFilter()
            }
        }
    }
}

3      

@twostraws  Site AdminHWS+

@jakcharvat: What an amazing, detailed answer - thank you! 🙌

4      

Thank you again, @jakcharvat. I very much appreciate you taking the time to help me with this.

I actually did discover .onChange the morning after I posted this, so the problem I'm having now is getting the view to invalidate and redraw when there's a change in the book record (i.e. when the list is books with 2.5 stars, and someone changes the selected book to 3.0 stars, the book should disappear from the 2.5 star list in real time.) But that's not the question I asked here and you did answer the question I asked here, so I'll mark this as solved and after I incorporate your recommended changes, if I'm still having that problem, I will make another post.

Thank you again.

4      

Thanks so much for the compliment Paul, really means a lot to me.

@NCrusher74, happy to help, please let me know if it worked, if not I'd love to dig in a bit deeper and try to debug it with you :)

3      

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.