Skip to content

Commit

Permalink
Merge branch 'development'
Browse files Browse the repository at this point in the history
  • Loading branch information
groue committed Apr 5, 2021
2 parents 67892d7 + 53d75c3 commit e017edd
Show file tree
Hide file tree
Showing 56 changed files with 1,195 additions and 984 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:

#### 5.x Releases

- `5.7.x` Releases - [5.7.0](#570) | [5.7.1](#571) | [5.7.2](#572)
- `5.7.x` Releases - [5.7.0](#570) | [5.7.1](#571) | [5.7.2](#572) | [5.7.3](#573)
- `5.6.x` Releases - [5.6.0](#560)
- `5.5.x` Releases - [5.5.0](#550)
- `5.4.x` Releases - [5.4.0](#540)
Expand Down Expand Up @@ -74,6 +74,15 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:

---

## 5.7.3

Released April 5, 2021 • [diff](https://github.com/groue/GRDB.swift/compare/v5.7.2...v5.7.3)

- **Fixed**: [#950](https://github.com/groue/GRDB.swift/pull/950) by [@MartinP7r](https://github.com/MartinP7r): Fix memory consumption when encoding JSON columns
- **Fixed**: [#951](https://github.com/groue/GRDB.swift/pull/951) by [@alexwlchan](https://github.com/alexwlchan): Fix documentation typo
- **Fixed**: [#952](https://github.com/groue/GRDB.swift/pull/952) by [@holsety](https://github.com/holsety): Fix documentation typo
- **Documentation Update**: [#953](https://github.com/groue/GRDB.swift/pull/953): Refactor the SwiftUI demo app

#### 5.7.2

- **Fixed**: Really fix breaking change and restore `SQLLiteral` as a deprecated alias for `SQL`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@
/* Begin PBXBuildFile section */
56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */; };
56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */; };
5671722A261A185300423B6F /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717229261A185300423B6F /* Query.swift */; };
5671723A261B23C800423B6F /* PlayerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717239261B23C800423B6F /* PlayerList.swift */; };
56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717251261B334D00423B6F /* PlayerRequestTests.swift */; };
567C3E1A2520B6DE0011F6E9 /* GRDBCombineDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */; };
567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */; };
567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */; };
567C3E4E2520B70E0011F6E9 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 567C3E3B2520B7000011F6E9 /* GRDB.framework */; };
567C3E4F2520B70E0011F6E9 /* GRDB.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 567C3E3B2520B7000011F6E9 /* GRDB.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E532520B75C0011F6E9 /* Player.swift */; };
567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E542520B75C0011F6E9 /* Persistence.swift */; };
567C3E5F2520B75C0011F6E9 /* PlayerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E562520B75C0011F6E9 /* PlayerListViewModel.swift */; };
567C3E602520B75C0011F6E9 /* PlayerFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E572520B75C0011F6E9 /* PlayerFormViewModel.swift */; };
567C3E612520B75D0011F6E9 /* PlayerForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E592520B75C0011F6E9 /* PlayerForm.swift */; };
567C3E622520B75D0011F6E9 /* PlayerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5A2520B75C0011F6E9 /* PlayerList.swift */; };
567C3E632520B75D0011F6E9 /* PlayerCreationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5B2520B75C0011F6E9 /* PlayerCreationSheet.swift */; };
567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */; };
567C3E622520B75D0011F6E9 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5A2520B75C0011F6E9 /* AppView.swift */; };
567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */; };
567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */; };
567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E652520B7880011F6E9 /* AppDatabase.swift */; };
567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E752520BB650011F6E9 /* Localizable.stringsdict */; };
567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */; };
56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -119,6 +121,9 @@
56026C9C25B8A7D000D1DF3F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = "<group>"; };
56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = "<group>"; };
56717229261A185300423B6F /* Query.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = "<group>"; };
56717239261B23C800423B6F /* PlayerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerList.swift; sourceTree = "<group>"; };
56717251261B334D00423B6F /* PlayerRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequestTests.swift; sourceTree = "<group>"; };
567C3E162520B6DE0011F6E9 /* GRDBCombineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBCombineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBCombineDemoApp.swift; sourceTree = "<group>"; };
567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand All @@ -127,15 +132,14 @@
567C3E292520B7000011F6E9 /* GRDB.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GRDB.xcodeproj; path = ../../../GRDB.xcodeproj; sourceTree = "<group>"; };
567C3E532520B75C0011F6E9 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
567C3E542520B75C0011F6E9 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
567C3E562520B75C0011F6E9 /* PlayerListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerListViewModel.swift; sourceTree = "<group>"; };
567C3E572520B75C0011F6E9 /* PlayerFormViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerFormViewModel.swift; sourceTree = "<group>"; };
567C3E592520B75C0011F6E9 /* PlayerForm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerForm.swift; sourceTree = "<group>"; };
567C3E5A2520B75C0011F6E9 /* PlayerList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerList.swift; sourceTree = "<group>"; };
567C3E5B2520B75C0011F6E9 /* PlayerCreationSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerCreationSheet.swift; sourceTree = "<group>"; };
567C3E592520B75C0011F6E9 /* PlayerFormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerFormView.swift; sourceTree = "<group>"; };
567C3E5A2520B75C0011F6E9 /* AppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerCreationView.swift; sourceTree = "<group>"; };
567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionView.swift; sourceTree = "<group>"; };
567C3E652520B7880011F6E9 /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = "<group>"; };
567C3E762520BB650011F6E9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
567C3E782520BB650011F6E9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
56B6D1082619EC1B003CC455 /* PlayerRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequest.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -162,6 +166,7 @@
children = (
56026C9C25B8A7D000D1DF3F /* Info.plist */,
56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */,
56717251261B334D00423B6F /* PlayerRequestTests.swift */,
56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */,
);
path = GRDBCombineDemoTests;
Expand Down Expand Up @@ -212,10 +217,11 @@
567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */,
567C3E542520B75C0011F6E9 /* Persistence.swift */,
567C3E532520B75C0011F6E9 /* Player.swift */,
56B6D1082619EC1B003CC455 /* PlayerRequest.swift */,
56717229261A185300423B6F /* Query.swift */,
567C3E1F2520B6DF0011F6E9 /* Preview Content */,
56185BC125B8047D00B9C30F /* Resources */,
56185BC225B8048B00B9C30F /* Support */,
567C3E552520B75C0011F6E9 /* ViewModels */,
567C3E582520B75C0011F6E9 /* Views */,
);
path = GRDBCombineDemo;
Expand Down Expand Up @@ -251,22 +257,14 @@
name = Frameworks;
sourceTree = "<group>";
};
567C3E552520B75C0011F6E9 /* ViewModels */ = {
isa = PBXGroup;
children = (
567C3E562520B75C0011F6E9 /* PlayerListViewModel.swift */,
567C3E572520B75C0011F6E9 /* PlayerFormViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
567C3E582520B75C0011F6E9 /* Views */ = {
isa = PBXGroup;
children = (
567C3E592520B75C0011F6E9 /* PlayerForm.swift */,
567C3E5A2520B75C0011F6E9 /* PlayerList.swift */,
567C3E5B2520B75C0011F6E9 /* PlayerCreationSheet.swift */,
567C3E5A2520B75C0011F6E9 /* AppView.swift */,
567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */,
567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */,
567C3E592520B75C0011F6E9 /* PlayerFormView.swift */,
56717239261B23C800423B6F /* PlayerList.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -441,23 +439,25 @@
files = (
56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */,
56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */,
56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
567C3E122520B6DE0011F6E9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5671722A261A185300423B6F /* Query.swift in Sources */,
567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */,
567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */,
567C3E612520B75D0011F6E9 /* PlayerForm.swift in Sources */,
567C3E632520B75D0011F6E9 /* PlayerCreationSheet.swift in Sources */,
56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */,
5671723A261B23C800423B6F /* PlayerList.swift in Sources */,
567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */,
567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */,
567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */,
567C3E622520B75D0011F6E9 /* PlayerList.swift in Sources */,
567C3E622520B75D0011F6E9 /* AppView.swift in Sources */,
567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */,
567C3E1A2520B6DE0011F6E9 /* GRDBCombineDemoApp.swift in Sources */,
567C3E5F2520B75C0011F6E9 /* PlayerListViewModel.swift in Sources */,
567C3E602520B75C0011F6E9 /* PlayerFormViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,25 @@ struct AppDatabase {
// MARK: - Database Access: Writes

extension AppDatabase {
/// A validation error that prevents some players from being saved into
/// the database.
enum ValidationError: LocalizedError {
case missingName

var errorDescription: String? {
switch self {
case .missingName:
return "Please provide a name"
}
}
}

/// Saves (inserts or updates) a player. When the method returns, the
/// player is present in the database, and its id is not nil.
func savePlayer(_ player: inout Player) throws {
if player.name.isEmpty {
throw ValidationError.missingName
}
try dbWriter.write { db in
try player.save(db)
}
Expand Down Expand Up @@ -125,19 +141,8 @@ extension AppDatabase {
// MARK: - Database Access: Reads

extension AppDatabase {
/// Returns a publisher that tracks changes in players ordered by name
func playersOrderedByNamePublisher() -> AnyPublisher<[Player], Error> {
ValueObservation
.tracking(Player.all().orderedByName().fetchAll)
.publisher(in: dbWriter, scheduling: .immediate)
.eraseToAnyPublisher()
}

/// Returns a publisher that tracks changes in players ordered by score
func playersOrderedByScorePublisher() -> AnyPublisher<[Player], Error> {
ValueObservation
.tracking(Player.all().orderedByScore().fetchAll)
.publisher(in: dbWriter, scheduling: .immediate)
.eraseToAnyPublisher()
/// Provides a read-only access to the database
var databaseReader: DatabaseReader {
dbWriter
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import GRDB
import SwiftUI

@main
struct GRDBCombineDemoApp: App {
let appDatabase = AppDatabase.shared

var body: some Scene {
WindowGroup {
PlayerList(viewModel: PlayerListViewModel(database: appDatabase))
AppView().environment(\.appDatabase, AppDatabase.shared)
}
}
}

// Let SwiftUI views access the database through the SwiftUI environment
private struct AppDatabaseKey: EnvironmentKey {
static let defaultValue: AppDatabase? = nil
}

extension EnvironmentValues {
var appDatabase: AppDatabase? {
get { self[AppDatabaseKey.self] }
set { self[AppDatabaseKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import GRDB

/// A player request defines how to feed the player list
struct PlayerRequest {
enum Ordering {
case byScore
case byName
}

var ordering: Ordering
}

/// Make `PlayerRequest` able to be used with the `@Query` property wrapper.
extension PlayerRequest: Queryable {
static var defaultValue: [Player] { [] }

func fetchValue(_ db: Database) throws -> [Player] {
switch ordering {
case .byScore: return try Player.all().orderedByScore().fetchAll(db)
case .byName: return try Player.all().orderedByName().fetchAll(db)
}
}
}
109 changes: 109 additions & 0 deletions Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Query.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// Query.swift
//
// A property wrapper inspired from
// https://davedelong.com/blog/2021/04/03/core-data-and-swiftui/
//

import Combine
import GRDB
import SwiftUI

/// The protocol that feeds the `@Query` property wrapper.
protocol Queryable: Equatable {
/// The type of the fetched value
associatedtype Value

/// The default value, used whenever the database is not available
static var defaultValue: Value { get }

/// Fetches the database value
func fetchValue(_ db: Database) throws -> Value
}

/// The property wrapper that observes a database query
@propertyWrapper
struct Query<Query: Queryable>: DynamicProperty {
/// The database reader that makes it possible to observe the database
@Environment(\.appDatabase?.databaseReader) private var databaseReader: DatabaseReader?
@StateObject private var core = Core()
private var baseQuery: Query

/// The fetched value
var wrappedValue: Query.Value {
core.value ?? Query.defaultValue
}

/// A binding to the query, that lets your views modify it.
///
/// This is how the demo app changes the player ordering.
var projectedValue: Binding<Query> {
Binding(
get: { core.query ?? baseQuery },
set: { core.query = $0 })
}

init(_ query: Query) {
baseQuery = query
}

func update() {
guard let databaseReader = databaseReader else {
fatalError("Attempting to use @Query without any database in the environment")
}
// Feed core with necessary information, and make sure tracking has started
if core.query == nil { core.query = baseQuery }
core.startTrackingIfNecessary(in: databaseReader)
}

private class Core: ObservableObject {
private(set) var value: Query.Value?
var databaseReader: DatabaseReader?
var query: Query? {
willSet {
if query != newValue {
// Stop tracking, and tell SwiftUI about the update
objectWillChange.send()
cancellable = nil
}
}
}
private var cancellable: AnyCancellable?

init() { }

func startTrackingIfNecessary(in databaseReader: DatabaseReader) {
if databaseReader !== self.databaseReader {
// Database has changed. Stop tracking.
self.databaseReader = databaseReader
cancellable = nil
}

guard let query = query else {
// No query set
return
}

guard cancellable == nil else {
// Already tracking
return
}

cancellable = ValueObservation
.tracking(query.fetchValue)
.publisher(
in: databaseReader,
scheduling: .immediate)
.sink(
receiveCompletion: { _ in
// Ignore errors
},
receiveValue: { [weak self] value in
guard let self = self else { return }
// Tell SwiftUI about the new value
self.objectWillChange.send()
self.value = value
})
}
}
}
Loading

0 comments on commit e017edd

Please sign in to comment.