Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storing previous results of a query in SwiftUI view #10

Open
mwildehahn opened this issue Dec 31, 2020 · 3 comments
Open

Storing previous results of a query in SwiftUI view #10

mwildehahn opened this issue Dec 31, 2020 · 3 comments

Comments

@mwildehahn
Copy link

I have a view with a search bar and a query to fetch the results. It all works great, but I'd like to store the previous results so that when you type, you don't jump into the "loading" state as the query executes.

I was trying something like this:

struct HomeView: View {
    @Query<HomeViewQuery>(fetchPolicy: .storeOrNetwork) private var query
    let searchQuery: String
    @State private var previousHits: [HomeViewQuery.Data.SearchResponse_search.SearchHit_hits]?
    
    func getResults() -> Query<HomeViewQuery>.Result {
        switch query.get(.init(query: searchQuery)) {
        case .loading:
            return .loading
        case .failure(let error):
            return .failure(error)
        case .success(let data):
            if let hits = data?.search?.hits {
                print("Setting previous hits: \(hits)")
                previousHits = hits
            }
            
            return .success(data)
        }
    }

    var body: some View {
        Group {
            if !searchQuery.isEmpty {
                switch getResults() {
                case .loading:
                    Group {
                        if previousHits != nil {
                            SearchResults(hits: previousHits!)
                        } else {
                            Text("Loading...")
                        }
                    }
                case .failure(let error):
                    Text("Error: \(error.localizedDescription)")
                case .success(let data):
                    if data?.search?.hits != nil && (data?.search?.nbHits ?? 0) > 0 {
                        SearchResults(hits: data!.search!.hits!)
                    } else {
                        Text("No results")
                    }
                }
            } else {
                Text("Search for stuff")
            }
        }
    }

But then get an error about modifying state during view update and the UI hangs when typing. Is it possible to subscribe to the query results in some way to do this or given this custom behavior, should I just directly call fetchQuery?

@mjm
Copy link
Member

mjm commented Dec 31, 2020

I think you can do what you want here by using onChange and/or onAppear to do the assignment to the @State variable. That's the way they want you to change state like that.

A different pattern I've used for searching is to use separate state for the text the user is entering and the text being used to query, only update the latter once the user has stopped typing for a short amount of time. Here's an example of a view where I did that. That may not be the pattern you want to use for your app, but it does have the benefit of not having to hang on to a previous list of search results.

@mwildehahn
Copy link
Author

Thanks for the pointers! onChange was what I was looking for. A coupe of follow up questions:

  • It doesn't look like you can subscribe to onChange of a query. I think that could be useful?
  • Another approach I was trying was this:
import SwiftUI
import RelaySwiftUI

private let query = graphql("""
query HomeViewQuery($query: String!, $options: SearchOptions) {
    search(query: $query, options: $options) {
        ...SearchResults_search
    }
}
""")


struct HomeView: View {
    @Query<HomeViewQuery>(fetchPolicy: .storeOrNetwork) private var query
    @ObservedObject var searchBar: SearchBar = SearchBar()
    @RelayEnvironment var relay
    
    @State private var data: HomeViewQuery.Data?
    @State private var isLoading: Bool = false
    
    var body: some View {
        NavigationView {
            Group {
                if isLoading && data == nil {
                    Text("Loading...")
                } else if let search = data?.search {
                    SearchResults(search: search.asFragment())
                } else {
                    Text("No results")
                }
            }
            .navigationBarTitle("Cubby", displayMode: .large)
            .add(searchBar)
            .onChange(of: searchBar.text, perform: { value in
                fetchResults()
            })
        }
    }
    
    func fetchResults() {
        let result = query.get(.init(query: searchBar.text, options: .init(snippetCharacters: 8)))
        switch result {
        case .loading:
            self.isLoading = true
        case .success(let data):
            self.data = data
        case .failure(let error):
            print("Error fetching query: \(error.localizedDescription)")
        }
    }
}

so using onChange to trigger the query. The view never updates in this case though even though I see the querying being executed. Is there something with how the query is using combine for results that they aren't being assigned to self.data for some reason?

More than just the search UI, I also wanted to pre-fetch some thumbnails based on the search results. I can do that in the init of another view, but am curious if there are ways to make processing the data from a query easier before using it in the view.

@mjm
Copy link
Member

mjm commented Jan 2, 2021

I'll have to give this some thought. If you really want to manage the lifecycle of the query results yourself, you may be better off using fetchQuery manually. By doing so, you wouldn't automatically update your view when the records changed (like in mutations), but maybe that's okay for something like search results. I've never implemented this pattern, so I'm not sure what gotchas you might hit.

As far as fetching thumbnails, I think I've generally opted to delegate that to a child view like you mention. There's features in Relay for JS I think for extending a schema with client-side-only fields, which could maybe help with this, but I haven't implemented that in Relay.swift yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants