BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

SOLVED: Inconsistency of text search

Forums > SwiftUI

Wondering if anyone can spot why the following doesn't correctly identify all positive search results:

import SwiftUI

struct Book: Codable, Identifiable {
    let id: Int
    let title: String
    let author: String
    let tags: Array<String>
}

struct SearchResultPair: Hashable {
    var author: String
    var title: String
}

struct ContentView: View {
    let books: [Book] = Bundle.main.decode("books.json")

    func searcher(searchTerm: String) -> Array<SearchResultPair> {
        var array: Array<SearchResultPair> = []
        var pair = SearchResultPair(author: "", title: "")

        for book in books {
            if book.title.contains(searchTerm) || book.author.contains(searchTerm) || book.tags.contains(searchTerm) {
                pair.author = book.author
                pair.title = book.title
                array.append(pair)
            } else {
                array = []
            }
        }

        return array
    }

    var body: some View {
        List(searcher(searchTerm: "hardcover"), id: \.self) { listItem in
            Text("\(listItem.title) by \(listItem.author)")
        }
    }
}

For example, if searchTerm = ".", the app lists only 1 book (rather than all 4); if searchTerm = "hardcover", 1 of the 3 books appears; if searchTerm = "g", no books are listed even though 2 of the books have a 'g' in a tag ("e" returns all 4 books). I'm aware of issues such as needing to adjust for ignoring capitalization and white space, but I'm not sure why all the 'regular' character values aren't appearing properly. Here's the json code I'm using for testing:

[
    {
        "id": 0,
        "title": "The Book of Books",
        "author": "Mr. Book",
        "tags": ["bibliography", "hardcover"]
    },
    {
        "id": 1,
        "title": "How to Read a Book",
        "author": "I. Glasses",
        "tags": ["how-to", "hardcover", "rare"]
    },
    {
        "id": 2,
        "title": "If I Were a Book",
        "author": "Plato",
        "tags": ["bibliography", "paperback", "rare"]
    },
    {
        "id": 3,
        "title": "Books for Book Readers",
        "author": "I. M. Odd",
        "tags": ["reference", "hardcover"]
    }
]

   

The way you have your check set up, if one of the books does not match, all previous matches get cleared out of your result array:

for book in books {
    if book.title.contains(searchTerm) || book.author.contains(searchTerm) || book.tags.contains(searchTerm) {
        pair.author = book.author
        pair.title = book.title
        array.append(pair)
    } else {
        array = []
    }
}

So let's walk it through...

  1. First book matches ".". array will have 1 member.
  2. Second book matches "." and array now has 2 members.
  3. Third book doesn't match "." so array gets set to [] and has 0 members.
  4. Fourth book matches "." and array now has 1 member.

You don't need the else clause here. You initialize array (really, you should pick a better name) as an empty array [] so all you need to do in the loop is append any matches; you don't need or want to empty the entire array if one book doesn't match.

You can easily see what's going on if you stick some print(array) statements after array.append(pair) and array = [] and watch your output.

1      

@rooster has the right answer!

May I add a comment?

You have the start of a Book structure. But you embed critical Book-related logic in your BookView.

for book in books {
// Why is this logic in a view? Consider moving this to your Book struct!
    if book.title.contains(searchTerm) || book.author.contains(searchTerm) || book.tags.contains(searchTerm) {
        pair.author = book.author
        pair.title = book.title
        array.append(pair)
    } 
}

Consider moving Book related logic to your Book struct.

struct Book: Codable, Identifiable {
    let id: Int
    let title: String
    let author: String
    let tags: Array<String>

    // Put BOOK logic in the BOOK struct
    func titleContains(_ thisString: String) -> Bool {
        title.contains(thisString) // <-- returns true or false
    }

    // Put BOOK logic in the BOOK struct
    func authorContains(_ thisString: String) -> Bool {
        author.contains(thisString) // <-- returns true or false
    }

    func bookMatches(_ thisSearchString) -> Bool {
        titleContains(thisSearchString) || authorContains(thisSearchString)
    }
}

By encapsulating business logic in your Book struct, this makes the view logic a bit more dashing.

for book in books {
    if book.matches(searchTerm) {  // simplify!
        // DELETE pair.author = book.author
        // DELETE pair.title = book.title
        array.append(SearchResultPair(author: book.author, title: book.title))
    } 
}

Homework problem: How might you create a SearchResultPair object inside your Book struct?

I agree with @rooster. Think of better names for both array and ContentView. Time to step up!

Keep coding!

1      

Heck, go even further and pull out the array-searching logic into an extension:

extension Array where Element == Book {
    func findMatches(for searchTerm: String) -> Self {
        filter { $0.bookMatches(searchTerm) }
    }
}

Then you can just do this to get an array of SearchResultPairs from your Books array:

let foundBooks = books.findMatches(for: searchTerm)
                      .map { SearchResultPair(author: $0.author, title: $0.title) }

1      

Thanks for the assists, @roosterboy & @Obelix! I've been able to fix the logic and simplify the code (including moving SearchResultPair inside Book and then calling it using Book.SearchResultPair (as in Array<Book.SearchResultPair>).

I'll hold off applying the extension method until I'm more confident with the basics, but I've added it to my notes!

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

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.