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

Can multiple @Queries bind to one single source? #40

Closed
baoshan opened this issue Mar 24, 2023 · 7 comments
Closed

Can multiple @Queries bind to one single source? #40

baoshan opened this issue Mar 24, 2023 · 7 comments

Comments

@baoshan
Copy link

baoshan commented Mar 24, 2023

Say there is a @State private var search: String = "" plus two @Queries in a view:

@Query(RequestA(search: "")) private var resultA: A;
@Query(RequestB(search: "")) private var resultB: B;

These queries need to be synced with the $search binding.

Besides the (imperative) .onChange(search) {...}, is there a better (descriptive) way? Do I need to introduce an extra layer of view to construct the queries?

Thank you ❤️

@groue
Copy link
Owner

groue commented Mar 24, 2023

Hello @baoshan,

In the Adding Parameters to Queryable Types guide, we see that there are three different ways to create a @Query:

  • In the "Modifying the Request from the SwiftUI View" section, the parameter belongs to @Query. I mean that in @Query(PlayersRequest(ordering: .byScore)), there is no external @State var ordering. The "single source of truth" for the ordering is the @Query. So this does not fit your need.

  • In the "Initializing @Query from a Request Binding" section, it is possible to initialize @Query from a request binding. That's better because this kind of @Query no longer owns the parameter. But you won't easily derive a request binding from your @State var search. And you don't need to modify the search via the @Query itself anyway.

  • Remains "Initializing @Query from a Constant Request". That's what you are looking for. Such @Query does not own its parameter, and does not allow to modify it. In the sample code below, the TestView1 is my interpretation of your initial setup. I hope it matches what you intended to say. The TestView2 uses constant requests in order to avoid the onChange technique.

import Combine
import SwiftUI
import GRDBQuery

@main
struct QueryTestsApp: App {
    var body: some Scene {
        WindowGroup {
            VStack {
                TestView1()
                Divider()
                TestView2()
            }
        }
    }
}

struct A { var name: String }
struct B { var name: String }

struct RequestA: Queryable {
    static var defaultValue: A { A(name: "") }
    var search: String
    
    func publisher(in _: EnvironmentValues) -> AnyPublisher<A, Never> {
        // Some publisher that publishes various values over time
        Timer
            .publish(every: 1, on: .main, in: .default)
            .autoconnect()
            .prepend(Date())
            .map { A(name: "\(search) \($0)" )}
            .eraseToAnyPublisher()
    }
}

struct RequestB: Queryable {
    static var defaultValue: B { B(name: "") }
    var search: String
    
    func publisher(in _: EnvironmentValues) -> AnyPublisher<B, Never> {
        // Some publisher that publishes various values over time
        Timer
            .publish(every: 1, on: .main, in: .default)
            .autoconnect()
            .prepend(Date())
            .map { B(name: "\(search) \($0)" )}
            .eraseToAnyPublisher()
    }
}

// MARK: - Version 1

struct TestView1: View {
    @State private var search: String = ""
    @Query(RequestA(search: ""), in: \.self) private var resultA: A
    @Query(RequestB(search: ""), in: \.self) private var resultB: B
    
    var body: some View {
        VStack {
            TextField("Search", text: $search)
            Text("A \(resultA.name)")
            Text("B \(resultB.name)")
        }
        .onChange(of: search) { search in
            $resultA.search.wrappedValue = search
            $resultB.search.wrappedValue = search
        }
    }
}

// MARK: - Version 2

struct TestView2: View {
    @State private var search: String = ""
    
    var body: some View {
        VStack {
            TextField("Search", text: $search)
            InnerView(search: search)
        }
    }
    
    private struct InnerView: View {
        @Query<RequestA> private var resultA: A
        @Query<RequestB> private var resultB: B
        
        init(search: String) {
            _resultA = Query(constant: RequestA(search: search), in: \.self)
            _resultB = Query(constant: RequestB(search: search), in: \.self)
        }
        
        var body: some View {
            Text("A \(resultA.name)")
            Text("B \(resultB.name)")
        }
    }
}

@baoshan
Copy link
Author

baoshan commented Mar 24, 2023

Thanks, Gwendal for the textbook examples! This is too much!

I think both are clear and great. I personally prefer the second one because it looks more direct. I wish Query(constant:) does not hurt performance because many Query instances are created while I type.

PS: Is it possible to write this?

init() {
  _resultA = Query(RequestA(search: $search))
}

@groue
Copy link
Owner

groue commented Mar 24, 2023

I think both are clear and great. I personally prefer the second one because it looks more direct.

👍🙂

I wish Query(constant:) does not hurt performance because many Query instances are created while I type.

It should not. The underlying publishers that perform the heavy work are only recreated if the requests actually change (all Queryable types are Equatable).

You can check if the app behaves as you expect by adding a print to your publishers. You'll see what's happening in your application logs.

PS: Is it possible to write this?

init() {
  _resultA = Query(RequestA(search: $search))
}

You mean, without the in: ... parameter? Yes, there's a tip in Getting Started with @Query (look for "Tip"). In my sample code I did not use it in order to keep the example short.

EDIT: you mean, without the constant argument name? No, this ruins all your efforts 😅

EDIT2: Oh, I see what you mean - I've been both too fast, and slow. You are looking for a shorthand and wondering if some $ magic could help. Well, $ is not magic, and your RequestA type does not accept a binding in its initializer, does it?

@baoshan
Copy link
Author

baoshan commented Mar 24, 2023

I mean a QueryRequest that takes a @Binding var search: String. So we can save an InnerView.

struct TestView1: View {
    @State private var search: String = ""
    @Query(RequestA(search: .constant(""))) private var resultA: A
    @Query(RequestB(search: .constant(""))) private var resultB: B
    
    var body: some View {
        VStack {
            TextField("Search", text: $search)
            Text("A \(resultA.name)")
            Text("B \(resultB.name)")
        }
    }
    
    init() {
        _resultA = Query(RequestA(search: $search))
        _resultB = Query(RequestB(search: $search))
    }
}

@groue
Copy link
Owner

groue commented Mar 24, 2023

I'm not sure this is possible (maybe I'm wrong).

And I'm not sure this is desirable.

With the current APIs, it is possible to define Queryable types without any dependency on the SwiftUI framework. I think this is good design. Those types belong to the "model" / "service" layers, even if they "talk to the view", by outputting types that are directly consumed by views. Yes, even if they are often created in order to feed one particular view, and look very coupled to it. I like that you don't need the SwiftUI engine or framework in order to test requests. If the library would foster Queryable types that accept bindings, I'm afraid we'd foster muddy application layering.

All in all, your idea is "left as an exercise" 😉 Maybe you'll find something that's actually cool.

I agree that the InnerView extra view is an inconvenience and I also wish we could do without it. In your own experiments, you'll see that it's easy to fight the compiler, or the SwiftUI engine, or both. Fighting, in programming, usually has bad consequences.

@baoshan
Copy link
Author

baoshan commented Mar 24, 2023

Thanks for sharing your insights!

Although InnerView looks superfluous at the first glance, it serves its (simple) purpose quite well.

I’m closing this thread and I wish more people could benefit from the discussion. Thanks again, Gwendal!

@baoshan baoshan closed this as completed Mar 24, 2023
@groue
Copy link
Owner

groue commented Mar 24, 2023

Thank you for asking an interesting question 😊 Happy GRDBQuery!

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