Skip to content

Commit

Permalink
Merge pull request #17 from groue/dev/query-initializers
Browse files Browse the repository at this point in the history
Fine-Grained Query initializers
  • Loading branch information
groue authored Apr 17, 2022
2 parents b3c91ac + 651f47e commit d4542be
Show file tree
Hide file tree
Showing 5 changed files with 353 additions and 88 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SwiftUI

/// A view that displays an always up-to-date list of players in the database.
struct PlayerList: View {
@Query(AllPlayers())
@Query(PlayerRequest())
var players: [Player]

var body: some View {
Expand Down
2 changes: 1 addition & 1 deletion Sources/GRDBQuery/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SwiftUI

/// A view that displays an always up-to-date list of players in the database.
struct PlayerList: View {
@Query(AllPlayers())
@Query(PlayerRequest())
var players: [Player]

var body: some View {
Expand Down
29 changes: 24 additions & 5 deletions Sources/GRDBQuery/Documentation.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import GRDB
import GRDBQuery

/// Tracks the full list of players
struct AllPlayers: Queryable {
struct PlayerRequest: Queryable {
static var defaultValue: [Player] { [] }

func publisher(in dbQueue: DatabaseQueue) -> AnyPublisher<[Player], Error> {
Expand Down Expand Up @@ -103,7 +103,7 @@ import GRDBQuery
import SwiftUI

struct PlayerList: View {
@Query(AllPlayers(), in: \.dbQueue)
@Query(PlayerRequest(), in: \.dbQueue)
var players: [Player]

var body: some View {
Expand All @@ -120,12 +120,31 @@ struct PlayerList: View {

> Tip: Some applications want to use `@Query` without specifying the key path to the database in each and every view.
>
> See how to make the environment key implicit in ``Query/init(_:in:)``.
> To do so, add somewhere in your application those convenience `Query` initializers:
>
> ```swift
> // Convenience Query initializers for requests
> // that feed from `DatabaseQueue`.
> extension Query where Request.DatabaseContext == DatabaseQueue {
> init(_ request: Request) {
> self.init(request, in: \.dbQueue)
> }
> init(_ request: Binding<Request>) {
> self.init(request, in: \.dbQueue)
> }
> init(constant request: Request) {
> self.init(constant:request, in: \.dbQueue)
> }
> }
> ```
>
> These initializers will streamline your SwiftUI views:
>
> ```swift
> struct PlayerList: View {
> @Query(AllPlayers()) // Implicit key path to the database
> @Query(PlayerRequest()) // Implicit key path to the database
> var players: [Player]
>
> ...
> }
> ```
Expand All @@ -141,7 +160,7 @@ import Combine
import GRDB
import GRDBQuery
struct AllPlayers: Queryable {
struct PlayerRequest: Queryable {
typealias Value = Result<[Player], Error>
static var defaultValue: Value { .success([]) }
Expand Down
169 changes: 155 additions & 14 deletions Sources/GRDBQuery/Documentation.docc/QueryableParameters.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# Adding Parameters to Queryable Types

Learn how a SwiftUI view can configure the database content it displays.
Learn how SwiftUI views can configure the database content displayed on screen.

## Overview

When a SwiftUI view needs to configure the database values it displays, it will modify the ``Queryable`` request that feeds the `@Query` property wrapper.
When a SwiftUI view needs to configure the database values displayed on screen, it will modify the ``Queryable`` request that feeds the `@Query` property wrapper.

Such configuration can be performed by the view that declares a `@Query` property. It can also be performed by the enclosing view. This article explores all your available options.

## A Configurable Queryable Type

As an example, let's extend the `AllPlayers` request type we have seen in <doc:GettingStarted>. It can now sort players by score, or by name, depending on its `ordering` property.
As an example, let's extend the `PlayerRequest` request type we have seen in <doc:GettingStarted>. It can now sort players by score, or by name, depending on its `ordering` property.

```swift
struct AllPlayers: Queryable {
struct PlayerRequest: Queryable {
enum Ordering {
case byScore
case byName
Expand Down Expand Up @@ -46,19 +48,19 @@ struct AllPlayers: Queryable {

The `@Query` property wrapper will detect changes in the `ordering` property, and update SwiftUI views accordingly.

> Experiment: You can adapt this example for your own needs. As you can see, you can modify the order to database values, but you can also change how they are filtered. All [ValueObservation] features are available.
> Experiment: You can adapt this example for your own needs. As you can see, you can modify the order to database values, but you can also change how they are filtered. All [GRDB] features are available.
## Modifying the Request from the SwiftUI View

SwiftUI views can change the properties of the Queryable request with the SwiftUI Binding provided by the `@Query` property wrapper:
SwiftUI views can change the properties of the Queryable request with the SwiftUI bindings provided by the `@Query` property wrapper:

```swift
import GRDBQuery
import SwiftUI

struct PlayerList: View {
// Ordering can change through the $players.ordering binding.
@Query(AllPlayers(ordering: .byScore))
@Query(PlayerRequest(ordering: .byScore))
var players: [Player]

var body: some View {
Expand All @@ -78,7 +80,7 @@ struct PlayerList: View {
}

struct ToggleOrderingButton: View {
@Binding var ordering: AllPlayers.Ordering
@Binding var ordering: PlayerRequest.Ordering

var body: some View {
switch ordering {
Expand All @@ -91,21 +93,160 @@ struct ToggleOrderingButton: View {
}
```

In the above example, `$players.ordering` is a SwiftUI binding to the `ordering` property of the `PlayerRequest` request.

This binding feeds `ToggleOrderingButton`, which lets the user change the ordering of the request. `@Query` then redraws the view with an updated the database content.

When appropriate, you can also use `$players.request`, a SwiftUI binding to the `PlayerRequest` request itself.


## Configuring the Initial Request

The above example has the `PlayerList` view always start with the `.byScore` ordering. When you want to provide the initial ordering as a parameter to your view, modify the sample code as below:
The above example has the `PlayerList` view always start with the `.byScore` ordering.

When you want to provide the initial request as a parameter to your view, provide a dedicated initializer:

```swift
struct PlayerList: View {
/// No default request
@Query<PlayerRequest>
var players: [Player]

/// Explicit initial request
init(initialOrdering: PlayerRequest.Ordering) {
_players = Query(PlayerRequest(ordering: initialOrdering))
}

var body: some View { ... }
}
```

Defining a default ordering is still possible:

```swift
struct PlayerList: View {
/// Defines the default initial request (ordered by score)
@Query(PlayerRequest(ordering: .byScore))
var players: [Player]

/// Default initial request (by score)
init() { }

/// Explicit initial request
init(initialOrdering ordering: PlayerRequest.Ordering) {
_players = Query(PlayerRequest(ordering: ordering))
}

var body: some View { ... }
}
```

> IMPORTANT: The initial request is only used when `PlayerList` appears on screen. After that, and until `PlayerList` disappears, the request is only controlled by the `$players` bindings described above.
>
> This means that calling the `PlayerList(initialOrdering:)` with a different ordering will have no effect:
>
> ```swift
> struct Container {
> @State var ordering = PlayerRequest.Ordering.byScore
>
> var body: some View {
> // No effect when the ordering State changes after the `PlayerList`
> // has appeared on screen:
> PlayerList(initialOrdering: ordering)
> }
> }
> ```
>
> To let the enclosing view control the request after `PlayerList` has appeared on screen, you'll need one of the techniques described below.
## Initializing @Query from a Binding
The `@Query` property wrapper can be controlled with a SwiftUI binding, as in the example below:
```swift
struct Container {
@State var ordering = PlayerRequest.Ordering.byScore
var body: some View {
PlayerList(ordering: $ordering) // Note the `$ordering` binding here
}
}
struct PlayerList: View {
@Query<PlayerRequest>
var players: [Player]
init(ordering: Binding<PlayerRequest.Ordering>) {
_players = Query(Binding(
get: { PlayerRequest(ordering: ordering.wrappedValue) },
set: { request in ordering.wrappedValue = request.ordering }))
}
var body: some View { ... }
}
```
With such a setup, `@Query` updates the database content whenever a change is performed by the `$ordering` Container binding, or the `$players` PlayerList bindings.

This is the classic two-way connection enabled by SwiftUI `Binding`.

## Initializing @Query from a Constant Request

Finally, the ``Query/init(constant:in:)`` initializer allows the enclosing Container view to control the request without restriction, and without any SwiftUI Binding. However, `$players` binding have no effect:

```swift
struct Container {
var ordering: PlayerRequest.Ordering

var body: some View {
PlayerList(constantOrdering: ordering)
}
}

struct PlayerList: View {
@Query<PlayerRequest>
var players: [Player]

init(constantOrdering ordering: PlayerRequest.Ordering) {
_players = Query(constant: PlayerRequest(ordering: ordering))
}

var body: some View { ... }
}
```

## Summary

**All the ``Query`` initializers we have seen above can be used in any given SwiftUI view.**

```swift
struct PlayerList: View {
@Query<AllPlayers>
/// Defines the default initial request (ordered by score)
@Query(PlayerRequest(ordering: .byScore))
var players: [Player]

init(initialOrdering: AllPlayers.Ordering) {
_players = Query(AllPlayers(ordering: initialOrdering))
/// Default initial request (by score)
init() { }

/// Initial request
init(initialOrdering ordering: PlayerRequest.Ordering) {
_players = Query(PlayerRequest(ordering: ordering))
}

/// Request binding
init(ordering: Binding<PlayerRequest.Ordering>) {
_players = Query(Binding(
get: { PlayerRequest(ordering: ordering.wrappedValue) },
set: { request in ordering.wrappedValue = request.ordering }))
}

/// Constant request
init(constantOrdering ordering: PlayerRequest.Ordering) {
_players = Query(constant: PlayerRequest(ordering: ordering))
}

...
var body: some View { ... }
}
```

[ValueObservation]: https://github.com/groue/GRDB.swift/blob/master/README.md#valueobservation
[GRDB]: https://github.com/groue/GRDB.swift
Loading

0 comments on commit d4542be

Please sign in to comment.