diff --git a/.swiftlint.yml b/.swiftlint.yml
index 4a1fccef96..d179972e8a 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -25,7 +25,6 @@ disabled_rules:
opt_in_rules:
- anyobject_protocol
- array_init
- - attributes
- closure_spacing
- collection_alignment
- contains_over_filter_count
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d07e9c9875..cd5ee286dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
#### 5.x Releases
-- `5.0.0` Betas - [5.0.0-beta](#500-beta) | [5.0.0-beta.2](#500-beta2) | [5.0.0-beta.3](#500-beta3) | [5.0.0-beta.4](#500-beta4) | [5.0.0-beta.5](#500-beta5)
+- `5.0.0` Betas - [5.0.0-beta](#500-beta) | [5.0.0-beta.2](#500-beta2) | [5.0.0-beta.3](#500-beta3) | [5.0.0-beta.4](#500-beta4) | [5.0.0-beta.5](#500-beta5) | [5.0.0-beta.6](#500-beta6)
#### 4.x Releases
@@ -74,6 +74,25 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
## Next Release
-->
+
+## 5.0.0-beta.6
+
+Released June 29, 2020 • [diff](https://github.com/groue/GRDB.swift/compare/v5.0.0-beta.5...v5.0.0-beta.6)
+
+- **New**: [#798](https://github.com/groue/GRDB.swift/pull/798): Debugging operators for ValueObservation
+- **New**: [#800](https://github.com/groue/GRDB.swift/pull/800): Xcode 12 support
+- **New**: [#801](https://github.com/groue/GRDB.swift/pull/801): Combine support
+- **New**: Faster decoding of Date and DateComponents
+
+### Documentation Diff
+
+- The [ValueObservation Operators](README.md#valueobservation-operators) chapter was extended with a description of the two new debugging operators `handleEvents` and `print`.
+
+- A new [GRDB ❤️ Combine](Documentation/Combine.md) guide describes the various Combine publishers that read, write, and observe the database.
+
+- [GRDBCombineDemo](Documentation/DemoApps/GRDBCombineDemo/README.md) is a new Combine + SwiftUI demo application.
+
+
## 5.0.0-beta.5
Released June 15, 2020 • [diff](https://github.com/groue/GRDB.swift/compare/v5.0.0-beta.4...v5.0.0-beta.5)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 58b6b9a16d..4b6c01ccbf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -82,7 +82,6 @@ The ideas, in alphabetical order:
- [JSON]
- [Linux]
- [More SQL Generation]
-- [Reactive Database Observation]
- [SQL Console in the Debugger]
- [SQLCipher in a Shared App Container]
- [Typed Expressions]
@@ -247,15 +246,6 @@ There are several SQLite features that GRDB could natively support:
See [issue #575](https://github.com/groue/GRDB.swift/issues/575) for more information and guidance about the implementation of extra table alterations.
-### Reactive Database Observation
-
-:baby: Starter Task
-
-We already have the [GRDBCombine](http://github.com/groue/GRDBCombine) and [RxGRDB] companion libraries.
-
-More choices of reactive engines would help more developers enjoy GRDB.
-
-
### SQL Console in the Debugger
:question: Unknown Difficulty :hammer: Tooling
@@ -319,7 +309,6 @@ Features that blur this focus are non-goals:
[JSON]: #json
[Linux]: #linux
[More SQL Generation]: #more-sql-generation
-[Reactive Database Observation]: #reactive-database-observation
[Records: Splitting Database Encoding from Ability to Write in the Database]: #records-splitting-database-encoding-from-ability-to-write-in-the-database
[Non-Goals]: #non-goals
[Report Bugs]: #report-bugs
diff --git a/Documentation/Combine.md b/Documentation/Combine.md
new file mode 100644
index 0000000000..fd1609dc8d
--- /dev/null
+++ b/Documentation/Combine.md
@@ -0,0 +1,328 @@
+GRDB ❤️ Combine
+===============
+
+**On systems supporting the Combine framework, GRDB offers the ability to publish database values and events using Combine's publishers.**
+
+- [Usage]
+- [Demo Application]
+- [Asynchronous Database Access]
+- [Database Observation]
+
+## Usage
+
+To connect to the database, please refer to [Database Connections].
+
+
+ Asynchronously read from the database
+
+This publisher reads a single value and delivers it.
+
+```swift
+// DatabasePublishers.Read<[Player]>
+let players = dbQueue.readPublisher { db in
+ try Player.fetchAll(db)
+}
+```
+
+
+
+
+ Asynchronously write in the database
+
+This publisher updates the database and delivers a single value.
+
+```swift
+// DatabasePublishers.Write
+let write = dbQueue.writePublisher { db in
+ try Player(...).insert(db)
+}
+
+// DatabasePublishers.Write
+let newPlayerCount = dbQueue.writePublisher { db -> Int in
+ try Player(...).insert(db)
+ return try Player.fetchCount(db)
+}
+```
+
+
+
+
+ Observe changes in database values
+
+This publisher delivers fresh values whenever the database changes:
+
+```swift
+// A publisher with output [Player] and failure Error
+let publisher = ValueObservation
+ .tracking { db in try Player.fetchAll(db) }
+ .publisher(in: dbQueue)
+
+// A publisher with output Int? and failure Error
+let publisher = ValueObservation
+ .tracking { db in try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player") }
+ .publisher(in: dbQueue)
+```
+
+
+
+
+ Observe database transactions
+
+This publisher delivers database connections whenever a database transaction has impacted an observed region:
+
+```swift
+// A publisher with output Database and failure Error
+let publisher = DatabaseRegionObservation
+ .tracking(Player.all())
+ .publisher(in: dbQueue)
+
+let cancellable = publisher.sink(
+ receiveCompletion: { completion in ... },
+ receiveValue: { (db: Database) in
+ print("Exclusive write access to the database after players have been impacted")
+ })
+
+// A publisher with output Database and failure Error
+let publisher = DatabaseRegionObservation
+ .tracking(SQLRequest(sql: "SELECT MAX(score) FROM player"))
+ .publisher(in: dbQueue)
+
+let cancellable = publisher.sink(
+ receiveCompletion: { completion in ... },
+ receiveValue: { (db: Database) in
+ print("Exclusive write access to the database after maximum score has been impacted")
+ })
+```
+
+
+
+
+# Asynchronous Database Access
+
+GRDB provide publishers that perform asynchronous database accesses.
+
+- [`readPublisher(receiveOn:value:)`]
+- [`writePublisher(receiveOn:updates:)`]
+- [`writePublisher(receiveOn:updates:thenRead:)`]
+
+
+#### `DatabaseReader.readPublisher(receiveOn:value:)`
+
+This methods returns a publisher that completes after database values have been asynchronously fetched.
+
+```swift
+// DatabasePublishers.Read<[Player]>
+let players = dbQueue.readPublisher { db in
+ try Player.fetchAll(db)
+}
+```
+
+Any attempt at modifying the database completes subscriptions with an error.
+
+When you use a [database queue] or a [database snapshot], the read has to wait for any eventual concurrent database access performed by this queue or snapshot to complete.
+
+When you use a [database pool], reads are generally non-blocking, unless the maximum number of concurrent reads has been reached. In this case, a read has to wait for another read to complete. That maximum number can be [configured].
+
+This publisher can be subscribed from any thread. A new database access starts on every subscription.
+
+The fetched value is published on the main queue, unless you provide a specific scheduler to the `receiveOn` argument.
+
+
+#### `DatabaseWriter.writePublisher(receiveOn:updates:)`
+
+This method returns a publisher that completes after database updates have been successfully executed inside a database transaction.
+
+```swift
+// DatabasePublishers.Write
+let write = dbQueue.writePublisher { db in
+ try Player(...).insert(db)
+}
+
+// DatabasePublishers.Write
+let newPlayerCount = dbQueue.writePublisher { db -> Int in
+ try Player(...).insert(db)
+ return try Player.fetchCount(db)
+}
+```
+
+This publisher can be subscribed from any thread. A new database access starts on every subscription.
+
+It completes on the main queue, unless you provide a specific [scheduler] to the `receiveOn` argument.
+
+When you use a [database pool], and your app executes some database updates followed by some slow fetches, you may profit from optimized scheduling with [`writePublisher(receiveOn:updates:thenRead:)`]. See below.
+
+
+#### `DatabaseWriter.writePublisher(receiveOn:updates:thenRead:)`
+
+This method returns a publisher that completes after database updates have been successfully executed inside a database transaction, and values have been subsequently fetched:
+
+```swift
+// DatabasePublishers.Write
+let newPlayerCount = dbQueue.writePublisher(
+ updates: { db in try Player(...).insert(db) }
+ thenRead: { db, _ in try Player.fetchCount(db) })
+}
+```
+
+It publishes exactly the same values as [`writePublisher(receiveOn:updates:)`]:
+
+```swift
+// DatabasePublishers.Write
+let newPlayerCount = dbQueue.writePublisher { db -> Int in
+ try Player(...).insert(db)
+ return try Player.fetchCount(db)
+}
+```
+
+The difference is that the last fetches are performed in the `thenRead` function. This function accepts two arguments: a readonly database connection, and the result of the `updates` function. This allows you to pass information from a function to the other (it is ignored in the sample code above).
+
+When you use a [database pool], this method applies a scheduling optimization: the `thenRead` function sees the database in the state left by the `updates` function, and yet does not block any concurrent writes. This can reduce database write contention. See [Advanced DatabasePool](../README.md#advanced-databasepool) for more information.
+
+When you use a [database queue], the results are guaranteed to be identical, but no scheduling optimization is applied.
+
+This publisher can be subscribed from any thread. A new database access starts on every subscription.
+
+It completes on the main queue, unless you provide a specific [scheduler] to the `receiveOn` argument.
+
+
+# Database Observation
+
+Database Observation publishers are based on [ValueObservation] and [DatabaseRegionObservation]. Please refer to their documentation for more information. If your application needs change notifications that are not built as Combine publishers, check the general [Database Changes Observation] chapter.
+
+- [`ValueObservation.publisher(in:scheduling:)`]
+- [`DatabaseRegionObservation.publisher(in:)`]
+
+
+#### `ValueObservation.publisher(in:scheduling:)`
+
+[ValueObservation] tracks changes in database values. You can turn it into a Combine publisher:
+
+```swift
+let observation = ValueObservation.tracking { db in
+ try Player.fetchAll(db)
+}
+
+// A publisher with output [Player] and failure Error
+let publisher = observation.publisher(in: dbQueue)
+```
+
+This publisher has the same behavior as ValueObservation:
+
+- It notifies an initial value before the eventual changes.
+- It may coalesce subsequent changes into a single notification.
+- It may notify consecutive identical values. You can filter out the undesired duplicates with the `removeDuplicates()` Combine operator, but we suggest you have a look at the [removeDuplicates()](../README.md#valueobservationremoveduplicates) GRDB operator also.
+- It stops emitting any value after the database connection is closed. But it never completes.
+- By default, it notifies the initial value, as well as eventual changes and errors, on the main thread, asynchronously.
+
+ This can be configured with the `scheduling` argument. It does not accept a Combine scheduler, but a [ValueObservation scheduler](../README.md#valueobservation-scheduling).
+
+ For example, the `.immediate` scheduler makes sure the initial value is notified immediately when the publisher is subscribed. It can help your application update the user interface without having to wait for any asynchronous notifications:
+
+ ```swift
+ // Immediate notification of the initial value
+ let cancellable = observation
+ .publisher(
+ in: dbQueue,
+ scheduling: .immediate) // <-
+ .sink(
+ receiveCompletion: { completion in ... },
+ receiveValue: { (players: [Player]) in print("Fresh players: \(players)") })
+ // <- here "fresh players" is already printed.
+ ```
+
+ Note that the `.immediate` scheduler requires that the publisher is subscribed from the main thread. It raises a fatal error otherwise.
+
+See [ValueObservation Scheduling](../README.md#valueobservation-scheduling) for more information.
+
+:warning: **ValueObservation and Data Consistency**
+
+When you compose ValueObservation publishers together with the [combineLatest](https://developer.apple.com/documentation/combine/publisher/3333677-combinelatest) operator, you lose all guarantees of [data consistency](https://en.wikipedia.org/wiki/Consistency_(database_systems)).
+
+Instead, compose requests together into **one single** ValueObservation, as below:
+
+```swift
+struct HallOfFame {
+ var totalPlayerCount: Int
+ var bestPlayers: [Player]
+}
+
+// DATA CONSISTENCY GUARANTEED
+let hallOfFamePublisher = ValueObservation
+ .tracking { db -> HallOfFame in
+ let totalPlayerCount = try Player.fetchCount(db)
+
+ let bestPlayers = try Player
+ .order(Column("score").desc)
+ .limit(10)
+ .fetchAll(db)
+
+ return HallOfFame(
+ totalPlayerCount: totalPlayerCount,
+ bestPlayers: bestPlayers)
+ }
+ .publisher(in: dbQueue)
+```
+
+See [ValueObservation] for more information.
+
+
+#### `DatabaseRegionObservation.publisher(in:)`
+
+[DatabaseRegionObservation] notifies all transactions that impact a tracked database region. You can turn it into a Combine publisher:
+
+```swift
+let request = Player.all()
+let observation = DatabaseRegionObservation.tracking(request)
+
+// A publisher with output Database and failure Error
+let publisher = observation.publisher(in: dbQueue)
+```
+
+This publisher can be created and subscribed from any thread. It delivers database connections in a "protected dispatch queue", serialized with all database updates. It only completes when a database error happens.
+
+```swift
+let request = Player.all()
+let cancellable = DatabaseRegionObservation
+ .tracking(request)
+ .publisher(in: dbQueue)
+ .sink(
+ receiveCompletion: { completion in ... },
+ receiveValue: { (db: Database) in
+ print("Players have changed.")
+ })
+
+try dbQueue.write { db in
+ try Player(name: "Arthur").insert(db)
+ try Player(name: "Barbara").insert(db)
+}
+// Prints "Players have changed."
+
+try dbQueue.write { db in
+ try Player.deleteAll(db)
+}
+// Prints "Players have changed."
+```
+
+See [DatabaseRegionObservation] for more information.
+
+
+[Database Connections]: ../README.md#database-connections
+[Usage]: #usage
+[Asynchronous Database Access]: #asynchronous-database-access
+[Combine]: https://developer.apple.com/documentation/combine
+[Database Changes Observation]: ../README.md#database-changes-observation
+[Database Observation]: #database-observation
+[DatabaseRegionObservation]: ../README.md#databaseregionobservation
+[Demo Application]: DemoApps/GRDBCombineDemo/README.md
+[SQLite]: http://sqlite.org
+[ValueObservation]: ../README.md#valueobservation
+[`DatabaseRegionObservation.publisher(in:)`]: #databaseregionobservationpublisherin
+[`ValueObservation.publisher(in:scheduling:)`]: #valueobservationpublisherinscheduling
+[`readPublisher(receiveOn:value:)`]: #databasereaderreadpublisherreceiveonvalue
+[`writePublisher(receiveOn:updates:)`]: #databasewriterwritepublisherreceiveonupdates
+[`writePublisher(receiveOn:updates:thenRead:)`]: #databasewriterwritepublisherreceiveonupdatesthenread
+[configured]: ../README.md#databasepool-configuration
+[database pool]: ../README.md#database-pools
+[database queue]: ../README.md#database-queues
+[database snapshot]: ../README.md#database-snapshots
+[scheduler]: https://developer.apple.com/documentation/combine/scheduler
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.pbxproj b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..4e1ffc304a
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.pbxproj
@@ -0,0 +1,590 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 563F3E7124A78BD400982CF8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563F3E7024A78BD400982CF8 /* AppDelegate.swift */; };
+ 563F3E7324A78BD400982CF8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563F3E7224A78BD400982CF8 /* SceneDelegate.swift */; };
+ 563F3E7724A78BD700982CF8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 563F3E7624A78BD700982CF8 /* Assets.xcassets */; };
+ 563F3E7A24A78BD700982CF8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 563F3E7924A78BD700982CF8 /* Preview Assets.xcassets */; };
+ 563F3E7D24A78BD700982CF8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 563F3E7B24A78BD700982CF8 /* LaunchScreen.storyboard */; };
+ 5651F47D24A8837800727C9D /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5651F47124A8835B00727C9D /* GRDB.framework */; };
+ 5651F47F24A884B200727C9D /* World.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5651F47E24A884B200727C9D /* World.swift */; };
+ 5651F48124A8856300727C9D /* PlayerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5651F48024A8856300727C9D /* PlayerList.swift */; };
+ 5651F48324A885AF00727C9D /* PlayerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5651F48224A885AF00727C9D /* PlayerListViewModel.swift */; };
+ 5651F48524A8905100727C9D /* PlayerEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5651F48424A8905100727C9D /* PlayerEditor.swift */; };
+ 5651F48724A8909F00727C9D /* PlayerFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5651F48624A8909F00727C9D /* PlayerFormViewModel.swift */; };
+ 5678066124A8A4E400606BC6 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 5678066324A8A4E400606BC6 /* Localizable.stringsdict */; };
+ 5678066524A8B7DD00606BC6 /* PlayerCreationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5678066424A8B7DD00606BC6 /* PlayerCreationSheet.swift */; };
+ 56AAFD4724A78F9A0077EADB /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AAFD4524A78F9A0077EADB /* AppDatabase.swift */; };
+ 56AAFD4824A78F9A0077EADB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AAFD4624A78F9A0077EADB /* Player.swift */; };
+ 56B1B13024A8E9A700D7FB31 /* PlayerForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B1B12F24A8E9A700D7FB31 /* PlayerForm.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 5651F46A24A8835B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = DC3773F319C8CBB3004FCF85;
+ remoteInfo = GRDBOSX;
+ };
+ 5651F46C24A8835B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 56E5D7F91B4D422D00430942;
+ remoteInfo = GRDBOSXTests;
+ };
+ 5651F46E24A8835B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 56553C181C3E906C00522B5C;
+ remoteInfo = GRDBOSXCrashTests;
+ };
+ 5651F47024A8835B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 56E5D7CA1B4D3FED00430942;
+ remoteInfo = GRDBiOS;
+ };
+ 5651F47224A8835B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 56E5D7D31B4D3FEE00430942;
+ remoteInfo = GRDBiOSTests;
+ };
+ 5651F47424A8835B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 565490A01D5A4798005622CB;
+ remoteInfo = GRDBWatchOS;
+ };
+ 5651F47624A8835B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = AAA4DCFE230F1E0600C74B15;
+ remoteInfo = GRDBtvOS;
+ };
+ 5651F47824A8835B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = AAA4DDD4230F262000C74B15;
+ remoteInfo = GRDBtvOSTests;
+ };
+ 5651F47A24A8836B00727C9D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ proxyType = 1;
+ remoteGlobalIDString = 56E5D7C91B4D3FED00430942;
+ remoteInfo = GRDBiOS;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 563F3E6D24A78BD400982CF8 /* GRDBCombineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBCombineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 563F3E7024A78BD400982CF8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 563F3E7224A78BD400982CF8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ 563F3E7624A78BD700982CF8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 563F3E7924A78BD700982CF8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 563F3E7C24A78BD700982CF8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 563F3E7E24A78BD700982CF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GRDB.xcodeproj; path = ../../../GRDB.xcodeproj; sourceTree = ""; };
+ 5651F47E24A884B200727C9D /* World.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = World.swift; sourceTree = ""; };
+ 5651F48024A8856300727C9D /* PlayerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerList.swift; sourceTree = ""; };
+ 5651F48224A885AF00727C9D /* PlayerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListViewModel.swift; sourceTree = ""; };
+ 5651F48424A8905100727C9D /* PlayerEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerEditor.swift; sourceTree = ""; };
+ 5651F48624A8909F00727C9D /* PlayerFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerFormViewModel.swift; sourceTree = ""; };
+ 5678066224A8A4E400606BC6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; };
+ 5678066424A8B7DD00606BC6 /* PlayerCreationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCreationSheet.swift; sourceTree = ""; };
+ 56AAFD4524A78F9A0077EADB /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = ""; };
+ 56AAFD4624A78F9A0077EADB /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; };
+ 56B1B12F24A8E9A700D7FB31 /* PlayerForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerForm.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 563F3E6A24A78BD400982CF8 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5651F47D24A8837800727C9D /* GRDB.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 56248FEF24A8F3DD009C144A /* ViewModels */ = {
+ isa = PBXGroup;
+ children = (
+ 5651F48624A8909F00727C9D /* PlayerFormViewModel.swift */,
+ 5651F48224A885AF00727C9D /* PlayerListViewModel.swift */,
+ );
+ path = ViewModels;
+ sourceTree = "";
+ };
+ 563F3E6424A78BD400982CF8 = {
+ isa = PBXGroup;
+ children = (
+ 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */,
+ 563F3E6F24A78BD400982CF8 /* GRDBCombineDemo */,
+ 563F3E6E24A78BD400982CF8 /* Products */,
+ 5651F47C24A8837800727C9D /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 563F3E6E24A78BD400982CF8 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 563F3E6D24A78BD400982CF8 /* GRDBCombineDemo.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 563F3E6F24A78BD400982CF8 /* GRDBCombineDemo */ = {
+ isa = PBXGroup;
+ children = (
+ 563F3E7024A78BD400982CF8 /* AppDelegate.swift */,
+ 563F3E7224A78BD400982CF8 /* SceneDelegate.swift */,
+ 56AAFD4524A78F9A0077EADB /* AppDatabase.swift */,
+ 56AAFD4624A78F9A0077EADB /* Player.swift */,
+ 5651F47E24A884B200727C9D /* World.swift */,
+ 56248FEF24A8F3DD009C144A /* ViewModels */,
+ 56B1B12524A8DA1000D7FB31 /* Views */,
+ 563F3E7824A78BD700982CF8 /* Preview Content */,
+ 56B1B12D24A8DA3300D7FB31 /* Resources */,
+ 56B1B12E24A8DA4F00D7FB31 /* Support */,
+ );
+ path = GRDBCombineDemo;
+ sourceTree = "";
+ };
+ 563F3E7824A78BD700982CF8 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 563F3E7924A78BD700982CF8 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 5651F46024A8835B00727C9D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 5651F46B24A8835B00727C9D /* GRDB.framework */,
+ 5651F46D24A8835B00727C9D /* GRDBOSXTests.xctest */,
+ 5651F46F24A8835B00727C9D /* GRDBOSXCrashTests.xctest */,
+ 5651F47124A8835B00727C9D /* GRDB.framework */,
+ 5651F47324A8835B00727C9D /* GRDBiOSTests.xctest */,
+ 5651F47524A8835B00727C9D /* GRDB.framework */,
+ 5651F47724A8835B00727C9D /* GRDB.framework */,
+ 5651F47924A8835B00727C9D /* GRDBtvOSTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 5651F47C24A8837800727C9D /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 56B1B12524A8DA1000D7FB31 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 5678066424A8B7DD00606BC6 /* PlayerCreationSheet.swift */,
+ 5651F48424A8905100727C9D /* PlayerEditor.swift */,
+ 56B1B12F24A8E9A700D7FB31 /* PlayerForm.swift */,
+ 5651F48024A8856300727C9D /* PlayerList.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ 56B1B12D24A8DA3300D7FB31 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 563F3E7624A78BD700982CF8 /* Assets.xcassets */,
+ 563F3E7B24A78BD700982CF8 /* LaunchScreen.storyboard */,
+ 5678066324A8A4E400606BC6 /* Localizable.stringsdict */,
+ );
+ name = Resources;
+ sourceTree = "";
+ };
+ 56B1B12E24A8DA4F00D7FB31 /* Support */ = {
+ isa = PBXGroup;
+ children = (
+ 563F3E7E24A78BD700982CF8 /* Info.plist */,
+ );
+ name = Support;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 563F3E6C24A78BD400982CF8 /* GRDBCombineDemo */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 563F3E8124A78BD700982CF8 /* Build configuration list for PBXNativeTarget "GRDBCombineDemo" */;
+ buildPhases = (
+ 563F3E6924A78BD400982CF8 /* Sources */,
+ 563F3E6A24A78BD400982CF8 /* Frameworks */,
+ 563F3E6B24A78BD400982CF8 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 5651F47B24A8836B00727C9D /* PBXTargetDependency */,
+ );
+ name = GRDBCombineDemo;
+ productName = GRDBCombineDemo;
+ productReference = 563F3E6D24A78BD400982CF8 /* GRDBCombineDemo.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 563F3E6524A78BD400982CF8 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 1150;
+ LastUpgradeCheck = 1150;
+ ORGANIZATIONNAME = "Gwendal Roué";
+ TargetAttributes = {
+ 563F3E6C24A78BD400982CF8 = {
+ CreatedOnToolsVersion = 11.5;
+ };
+ };
+ };
+ buildConfigurationList = 563F3E6824A78BD400982CF8 /* Build configuration list for PBXProject "GRDBCombineDemo" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 563F3E6424A78BD400982CF8;
+ productRefGroup = 563F3E6E24A78BD400982CF8 /* Products */;
+ projectDirPath = "";
+ projectReferences = (
+ {
+ ProductGroup = 5651F46024A8835B00727C9D /* Products */;
+ ProjectRef = 5651F45F24A8835B00727C9D /* GRDB.xcodeproj */;
+ },
+ );
+ projectRoot = "";
+ targets = (
+ 563F3E6C24A78BD400982CF8 /* GRDBCombineDemo */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXReferenceProxy section */
+ 5651F46B24A8835B00727C9D /* GRDB.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = GRDB.framework;
+ remoteRef = 5651F46A24A8835B00727C9D /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 5651F46D24A8835B00727C9D /* GRDBOSXTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = GRDBOSXTests.xctest;
+ remoteRef = 5651F46C24A8835B00727C9D /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 5651F46F24A8835B00727C9D /* GRDBOSXCrashTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = GRDBOSXCrashTests.xctest;
+ remoteRef = 5651F46E24A8835B00727C9D /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 5651F47124A8835B00727C9D /* GRDB.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = GRDB.framework;
+ remoteRef = 5651F47024A8835B00727C9D /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 5651F47324A8835B00727C9D /* GRDBiOSTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = GRDBiOSTests.xctest;
+ remoteRef = 5651F47224A8835B00727C9D /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 5651F47524A8835B00727C9D /* GRDB.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = GRDB.framework;
+ remoteRef = 5651F47424A8835B00727C9D /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 5651F47724A8835B00727C9D /* GRDB.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = GRDB.framework;
+ remoteRef = 5651F47624A8835B00727C9D /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 5651F47924A8835B00727C9D /* GRDBtvOSTests.xctest */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.cfbundle;
+ path = GRDBtvOSTests.xctest;
+ remoteRef = 5651F47824A8835B00727C9D /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 563F3E6B24A78BD400982CF8 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 563F3E7D24A78BD700982CF8 /* LaunchScreen.storyboard in Resources */,
+ 5678066124A8A4E400606BC6 /* Localizable.stringsdict in Resources */,
+ 563F3E7A24A78BD700982CF8 /* Preview Assets.xcassets in Resources */,
+ 563F3E7724A78BD700982CF8 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 563F3E6924A78BD400982CF8 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5651F47F24A884B200727C9D /* World.swift in Sources */,
+ 5651F48324A885AF00727C9D /* PlayerListViewModel.swift in Sources */,
+ 56B1B13024A8E9A700D7FB31 /* PlayerForm.swift in Sources */,
+ 5678066524A8B7DD00606BC6 /* PlayerCreationSheet.swift in Sources */,
+ 563F3E7124A78BD400982CF8 /* AppDelegate.swift in Sources */,
+ 563F3E7324A78BD400982CF8 /* SceneDelegate.swift in Sources */,
+ 5651F48724A8909F00727C9D /* PlayerFormViewModel.swift in Sources */,
+ 5651F48124A8856300727C9D /* PlayerList.swift in Sources */,
+ 56AAFD4724A78F9A0077EADB /* AppDatabase.swift in Sources */,
+ 5651F48524A8905100727C9D /* PlayerEditor.swift in Sources */,
+ 56AAFD4824A78F9A0077EADB /* Player.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 5651F47B24A8836B00727C9D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = GRDBiOS;
+ targetProxy = 5651F47A24A8836B00727C9D /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 563F3E7B24A78BD700982CF8 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 563F3E7C24A78BD700982CF8 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+ 5678066324A8A4E400606BC6 /* Localizable.stringsdict */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 5678066224A8A4E400606BC6 /* en */,
+ );
+ name = Localizable.stringsdict;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 563F3E7F24A78BD700982CF8 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.5;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 563F3E8024A78BD700982CF8 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.5;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 563F3E8224A78BD700982CF8 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"GRDBCombineDemo/Preview Content\"";
+ DEVELOPMENT_TEAM = AMD8W895CT;
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = GRDBCombineDemo/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemo;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
+ };
+ name = Debug;
+ };
+ 563F3E8324A78BD700982CF8 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"GRDBCombineDemo/Preview Content\"";
+ DEVELOPMENT_TEAM = AMD8W895CT;
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = GRDBCombineDemo/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemo;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 563F3E6824A78BD400982CF8 /* Build configuration list for PBXProject "GRDBCombineDemo" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 563F3E7F24A78BD700982CF8 /* Debug */,
+ 563F3E8024A78BD700982CF8 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 563F3E8124A78BD700982CF8 /* Build configuration list for PBXNativeTarget "GRDBCombineDemo" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 563F3E8224A78BD700982CF8 /* Debug */,
+ 563F3E8324A78BD700982CF8 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 563F3E6524A78BD400982CF8 /* Project object */;
+}
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..919434a625
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDatabase.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDatabase.swift
new file mode 100644
index 0000000000..ce1cd4ddc3
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDatabase.swift
@@ -0,0 +1,163 @@
+import Combine
+import GRDB
+
+/// AppDatabase lets the application access the database.
+///
+/// It applies the pratices recommended at
+/// https://github.com/groue/GRDB.swift/blob/master/Documentation/GoodPracticesForDesigningRecordTypes.md
+final class AppDatabase {
+ private let dbQueue: DatabaseQueue
+
+ /// Creates an AppDatabase and make sure the database schema is ready.
+ init(_ dbQueue: DatabaseQueue) throws {
+ self.dbQueue = dbQueue
+ try migrator.migrate(dbQueue)
+ }
+
+ /// The DatabaseMigrator that defines the database schema.
+ ///
+ /// See https://github.com/groue/GRDB.swift/blob/master/Documentation/Migrations.md
+ private var migrator: DatabaseMigrator {
+ var migrator = DatabaseMigrator()
+
+ #if DEBUG
+ // Speed up development by nuking the database when migrations change
+ // See https://github.com/groue/GRDB.swift/blob/master/Documentation/Migrations.md#the-erasedatabaseonschemachange-option
+ migrator.eraseDatabaseOnSchemaChange = true
+ #endif
+
+ migrator.registerMigration("createPlayer") { db in
+ // Create a table
+ // See https://github.com/groue/GRDB.swift#create-tables
+ try db.create(table: "player") { t in
+ t.autoIncrementedPrimaryKey("id")
+ t.column("name", .text).notNull()
+ // Sort player names in a localized case insensitive fashion by default
+ // See https://github.com/groue/GRDB.swift/blob/master/README.md#unicode
+ .collate(.localizedCaseInsensitiveCompare)
+ t.column("score", .integer).notNull()
+ }
+ }
+
+ // Migrations for future application versions will be inserted here:
+ // migrator.registerMigration(...) { db in
+ // ...
+ // }
+
+ return migrator
+ }
+}
+
+// MARK: - Database Access
+//
+// This extension defines methods that fulfill application needs, both in terms
+// of writes and reads.
+extension AppDatabase {
+ // MARK: Writes
+
+ /// Save (insert or update) a player.
+ func savePlayer(_ player: inout Player) throws {
+ try dbQueue.write { db in
+ try player.save(db)
+ }
+ }
+
+ /// Delete the specified players
+ func deletePlayers(ids: [Int64]) throws {
+ try dbQueue.write { db in
+ _ = try Player.deleteAll(db, keys: ids)
+ }
+ }
+
+ /// Delete all players
+ func deleteAllPlayers() throws {
+ try dbQueue.write { db in
+ _ = try Player.deleteAll(db)
+ }
+ }
+
+ /// Refresh all players (by performing some random changes, for demo purpose).
+ func refreshPlayers() throws {
+ try dbQueue.write { db in
+ if try Player.fetchCount(db) == 0 {
+ // Insert new random players
+ try createRandomPlayers(db)
+ } else {
+ // Insert a player
+ if Bool.random() {
+ var player = Player.newRandom()
+ try player.insert(db)
+ }
+ // Delete a random player
+ if Bool.random() {
+ try Player.order(sql: "RANDOM()").limit(1).deleteAll(db)
+ }
+ // Update some players
+ for var player in try Player.fetchAll(db) where Bool.random() {
+ try player.updateChanges(db) {
+ $0.score = Player.randomScore()
+ }
+ }
+ }
+ }
+ }
+
+ /// Create random players if the database is empty.
+ func createRandomPlayersIfEmpty() throws {
+ try dbQueue.write { db in
+ if try Player.fetchCount(db) == 0 {
+ try createRandomPlayers(db)
+ }
+ }
+ }
+
+ /// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`.
+ private func createRandomPlayers(_ db: Database) throws {
+ for _ in 0..<8 {
+ var player = Player.newRandom()
+ try player.insert(db)
+ }
+ }
+
+ // MARK: Reads
+
+ /// Returns a publisher that tracks changes in players ordered by name
+ func playersOrderedByNamePublisher() -> AnyPublisher<[Player], Error> {
+ ValueObservation
+ .tracking(Player.all().orderedByName().fetchAll)
+ // Use the .immediate scheduling so that views do not have to wait
+ // until the players are loaded.
+ .publisher(in: dbQueue, 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)
+ // Use the .immediate scheduling so that views do not have to wait
+ // until the players are loaded.
+ .publisher(in: dbQueue, scheduling: .immediate)
+ .eraseToAnyPublisher()
+ }
+}
+
+// MARK: - Support for Tests and Previews
+
+#if DEBUG
+extension AppDatabase {
+ /// Returns an empty, in-memory database, suitable for testing and previews.
+ static func empty() throws -> AppDatabase {
+ try AppDatabase(DatabaseQueue())
+ }
+
+ /// Returns an in-memory database populated with a few random
+ /// players, suitable for previews.
+ static func random() throws -> AppDatabase {
+ let database = try AppDatabase.empty()
+ try database.createRandomPlayersIfEmpty()
+ return database
+ }
+}
+#endif
+
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDelegate.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDelegate.swift
new file mode 100644
index 0000000000..c84522617c
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDelegate.swift
@@ -0,0 +1,45 @@
+import UIKit
+import GRDB
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ // Setup database. Error handling left as an exercise for the reader.
+ try! setupDatabase()
+ return true
+ }
+
+ // MARK: UISceneSession Lifecycle
+
+ func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
+ // Called when a new scene session is being created.
+ // Use this method to select a configuration to create the new scene with.
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+ }
+
+ func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
+ // Called when the user discards a scene session.
+ // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
+ // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
+ }
+
+ // MARK: - Database Setup
+
+ private func setupDatabase() throws {
+ // AppDelegate chooses the location of the database file.
+ // See https://github.com/groue/GRDB.swift/blob/master/README.md#database-connections
+ let databaseURL = try FileManager.default
+ .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
+ .appendingPathComponent("db.sqlite")
+ let dbQueue = try DatabaseQueue(path: databaseURL.path)
+
+ // Create the shared application database
+ let database = try AppDatabase(dbQueue)
+
+ // Populate the database if it is empty, for better demo purpose.
+ try database.createRandomPlayersIfEmpty()
+
+ // Expose it to the rest of the application
+ Current.database = { database }
+ }
+}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/Contents.json
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/Contents.json
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/Icon.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/Icon.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/Icon.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/Icon.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_20pt.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_20pt.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_20pt.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_20pt.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_29pt.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_29pt.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_29pt.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_29pt.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_40pt.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_40pt.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_40pt.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_40pt.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_76pt.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_76pt.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_76pt.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_76pt.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
rename to Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/Contents.json b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/LaunchIcon.imageset/Contents.json b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/LaunchIcon.imageset/Contents.json
new file mode 100644
index 0000000000..2cbe59d5ec
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/LaunchIcon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "LaunchIcon.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf
new file mode 100644
index 0000000000..2660891492
Binary files /dev/null and b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf differ
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Base.lproj/LaunchScreen.storyboard b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000000..246de79c45
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Info.plist b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Info.plist
new file mode 100644
index 0000000000..cb967d41b6
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Info.plist
@@ -0,0 +1,58 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+
+
+
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Player.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Player.swift
new file mode 100644
index 0000000000..0b3f867b4c
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Player.swift
@@ -0,0 +1,94 @@
+import GRDB
+
+/// The Player struct.
+///
+/// Identifiable conformance supports SwiftUI list animations
+struct Player: Identifiable {
+ /// The player id.
+ ///
+ /// Int64 is the recommended type for auto-incremented database ids.
+ /// Use nil for players that are not inserted yet in the database.
+ var id: Int64?
+ var name: String
+ var score: Int
+}
+
+extension Player {
+ private static let names = [
+ "Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David",
+ "Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette",
+ "Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl",
+ "Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole",
+ "Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul",
+ "Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain",
+ "Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann",
+ "Zazie", "Zoé"]
+
+ /// Creates a new player with empty name and zero score
+ static func new() -> Player {
+ Player(id: nil, name: "", score: 0)
+ }
+
+ /// Creates a new player with random name and random score
+ static func newRandom() -> Player {
+ Player(id: nil, name: randomName(), score: randomScore())
+ }
+
+ /// Returns a random name
+ static func randomName() -> String {
+ names.randomElement()!
+ }
+
+ /// Returns a random score
+ static func randomScore() -> Int {
+ 10 * Int.random(in: 0...100)
+ }
+}
+
+// MARK: - Persistence
+
+/// Make Player a Codable Record.
+///
+/// See https://github.com/groue/GRDB.swift/blob/master/README.md#records
+extension Player: Codable, FetchableRecord, MutablePersistableRecord {
+ // Define database columns from CodingKeys
+ fileprivate enum Columns {
+ static let name = Column(CodingKeys.name)
+ static let score = Column(CodingKeys.score)
+ }
+
+ /// Updates a player id after it has been inserted in the database.
+ mutating func didInsert(with rowID: Int64, for column: String?) {
+ id = rowID
+ }
+}
+
+// MARK: - Player Database Requests
+
+/// Define some player requests used by the application.
+///
+/// See https://github.com/groue/GRDB.swift/blob/master/README.md#requests
+/// See https://github.com/groue/GRDB.swift/blob/master/Documentation/GoodPracticesForDesigningRecordTypes.md
+extension DerivableRequest where RowDecoder == Player {
+ /// A request of players ordered by name
+ ///
+ /// For example:
+ ///
+ /// let players = try dbQueue.read { db in
+ /// try Player.all().orderedByName().fetchAll(db)
+ /// }
+ func orderedByName() -> Self {
+ order(Player.Columns.name)
+ }
+
+ /// A request of players ordered by score
+ ///
+ /// For example:
+ ///
+ /// let players = try dbQueue.read { db in
+ /// try Player.all().orderedByScore().fetchAll(db)
+ /// }
+ func orderedByScore() -> Self {
+ order(Player.Columns.score.desc, Player.Columns.name)
+ }
+}
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Preview Content/Preview Assets.xcassets/Contents.json b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/SceneDelegate.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/SceneDelegate.swift
new file mode 100644
index 0000000000..5b7a7b6938
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/SceneDelegate.swift
@@ -0,0 +1,52 @@
+import UIKit
+import SwiftUI
+
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+ var window: UIWindow?
+
+ func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+ // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
+ // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
+ // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
+
+ // Create the SwiftUI view that provides the window contents.
+ let viewModel = PlayerListViewModel(database: Current.database())
+ let rootView = PlayerList(viewModel: viewModel)
+
+ // Use a UIHostingController as window root view controller.
+ if let windowScene = scene as? UIWindowScene {
+ let window = UIWindow(windowScene: windowScene)
+ window.rootViewController = UIHostingController(rootView: rootView)
+ self.window = window
+ window.makeKeyAndVisible()
+ }
+ }
+
+ func sceneDidDisconnect(_ scene: UIScene) {
+ // Called as the scene is being released by the system.
+ // This occurs shortly after the scene enters the background, or when its session is discarded.
+ // Release any resources associated with this scene that can be re-created the next time the scene connects.
+ // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
+ }
+
+ func sceneDidBecomeActive(_ scene: UIScene) {
+ // Called when the scene has moved from an inactive state to an active state.
+ // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
+ }
+
+ func sceneWillResignActive(_ scene: UIScene) {
+ // Called when the scene will move from an active state to an inactive state.
+ // This may occur due to temporary interruptions (ex. an incoming phone call).
+ }
+
+ func sceneWillEnterForeground(_ scene: UIScene) {
+ // Called as the scene transitions from the background to the foreground.
+ // Use this method to undo the changes made on entering the background.
+ }
+
+ func sceneDidEnterBackground(_ scene: UIScene) {
+ // Called as the scene transitions from the foreground to the background.
+ // Use this method to save data, release shared resources, and store enough scene-specific state information
+ // to restore the scene back to its current state.
+ }
+}
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/ViewModels/PlayerFormViewModel.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/ViewModels/PlayerFormViewModel.swift
new file mode 100644
index 0000000000..6d38d9eac1
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/ViewModels/PlayerFormViewModel.swift
@@ -0,0 +1,67 @@
+import Combine
+import Foundation
+
+/// The view model that validates and saves an edited player into the database.
+///
+/// It feeds `PlayerForm`, `PlayerCreationSheet` and `PlayerEditor`.
+final class PlayerFormViewModel: ObservableObject {
+ /// A validation error that prevents the player from being saved into
+ /// the database.
+ enum ValidationError: LocalizedError {
+ case missingName
+
+ var errorDescription: String? {
+ switch self {
+ case .missingName:
+ return "Please give a name to this player."
+ }
+ }
+ }
+
+ @Published var name: String = ""
+ @Published var score: String = ""
+
+ private let database: AppDatabase
+ private var player: Player
+
+ init(database: AppDatabase, player: Player) {
+ self.database = database
+ self.player = player
+ updateViewFromPlayer()
+ }
+
+ // MARK: - Manage the Player Form
+
+ /// Validates and saves the player into the database.
+ func savePlayer() throws {
+ if name.isEmpty {
+ throw ValidationError.missingName
+ }
+ player.name = name
+ player.score = Int(score) ?? 0
+ try database.savePlayer(&player)
+ }
+
+ /// Resets form values to the original player values.
+ func reset() {
+ updateViewFromPlayer()
+ }
+
+ /// Edits a new player
+ func editNewPlayer() {
+ player = .new()
+ updateViewFromPlayer()
+ }
+
+ // MARK: - Private
+
+ private func updateViewFromPlayer() {
+ self.name = player.name
+ if player.score == 0 && player.id == nil {
+ // Avoid displaying "0" for a new player: it does not look good.
+ self.score = ""
+ } else {
+ self.score = "\(player.score)"
+ }
+ }
+}
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/ViewModels/PlayerListViewModel.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/ViewModels/PlayerListViewModel.swift
new file mode 100644
index 0000000000..bc48a8ca8e
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/ViewModels/PlayerListViewModel.swift
@@ -0,0 +1,103 @@
+import Combine
+import Foundation
+
+/// The view model that feeds `PlayerList`, and performs list modifications
+/// in the database.
+final class PlayerListViewModel: ObservableObject {
+ enum Ordering {
+ case byScore
+ case byName
+ }
+
+ /// The list ordering
+ @Published var ordering: Ordering = .byScore
+
+ /// The players in the list
+ @Published var players: [Player] = []
+
+ /// The view model that edits a new player
+ let newPlayerViewModel: PlayerFormViewModel
+
+ private let database: AppDatabase
+ private var playersCancellable: AnyCancellable?
+
+ init(database: AppDatabase) {
+ self.database = database
+ newPlayerViewModel = PlayerFormViewModel(database: database, player: .new())
+ playersCancellable = playersPublisher(in: database).sink { [weak self] players in
+ self?.players = players
+ }
+ }
+
+ // MARK: - Players List Management
+
+ /// Deletes all players
+ func deleteAllPlayers() {
+ // Eventual error presentation is left as an exercise for the reader.
+ try! database.deleteAllPlayers()
+ }
+
+ func deletePlayers(atOffsets offsets: IndexSet) {
+ // Eventual error presentation is left as an exercise for the reader.
+ let playerIDs = offsets.compactMap { players[$0].id }
+ try! database.deletePlayers(ids: playerIDs)
+ }
+
+ /// Refreshes the list of players
+ func refreshPlayers() {
+ // Eventual error presentation is left as an exercise for the reader.
+ try! database.refreshPlayers()
+ }
+
+ /// Spawns many concurrent database updates, for demo purpose
+ func stressTest() {
+ for _ in 0..<50 {
+ DispatchQueue.global().async {
+ self.refreshPlayers()
+ }
+ }
+ }
+
+ // MARK: - Change Player Ordering
+
+ /// Toggles between the available orderings
+ func toggleOrdering() {
+ switch ordering {
+ case .byName:
+ ordering = .byScore
+ case .byScore:
+ ordering = .byName
+ }
+ }
+
+ // MARK: - Player Edition
+
+ /// Returns a view model suitable for editing a player.
+ func editionViewModel(for player: Player) -> PlayerFormViewModel {
+ PlayerFormViewModel(database: database, player: player)
+ }
+
+ // MARK: - Private
+
+ /// Returns a publisher of the players in the list
+ private func playersPublisher(in database: AppDatabase) -> AnyPublisher<[Player], Never> {
+ // Players depend on the current ordering
+ $ordering.map { ordering -> AnyPublisher<[Player], Error> in
+ switch ordering {
+ case .byScore:
+ return database.playersOrderedByScorePublisher()
+ case .byName:
+ return database.playersOrderedByNamePublisher()
+ }
+ }
+ .map { playersPublisher in
+ // Turn database errors into an empty players list.
+ // Eventual error presentation is left as an exercise for the reader.
+ playersPublisher.catch { error in
+ Just<[Player]>([])
+ }
+ }
+ .switchToLatest()
+ .eraseToAnyPublisher()
+ }
+}
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerCreationSheet.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerCreationSheet.swift
new file mode 100644
index 0000000000..cdedb43f7c
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerCreationSheet.swift
@@ -0,0 +1,55 @@
+import SwiftUI
+
+/// The Player creation sheet
+struct PlayerCreationSheet: View {
+ /// Manages edition of the new player
+ let viewModel: PlayerFormViewModel
+
+ /// Executed when user cancels or saves the new user.
+ let dismissAction: () -> Void
+
+ @State private var errorAlertIsPresented = false
+ @State private var errorAlertTitle = ""
+
+ var body: some View {
+ NavigationView {
+ PlayerForm(viewModel: viewModel)
+ .alert(
+ isPresented: $errorAlertIsPresented,
+ content: { Alert(title: Text(errorAlertTitle)) })
+ .navigationBarTitle("New Player")
+ .navigationBarItems(
+ leading: Button(
+ action: self.dismissAction,
+ label: { Text("Cancel") }),
+ trailing: Button(
+ action: self.save,
+ label: { Text("Save") }))
+ }
+ }
+
+ private func save() {
+ do {
+ try viewModel.savePlayer()
+ dismissAction()
+ } catch {
+ errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
+ errorAlertIsPresented = true
+ }
+ }
+}
+
+#if DEBUG
+struct PlayerCreationSheet_Previews: PreviewProvider {
+ static var previews: some View {
+ let viewModel = try! PlayerFormViewModel(
+ database: .empty(),
+ player: .new())
+
+ return PlayerCreationSheet(
+ viewModel: viewModel,
+ dismissAction: { })
+ }
+}
+#endif
+
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerEditor.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerEditor.swift
new file mode 100644
index 0000000000..0a404967ab
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerEditor.swift
@@ -0,0 +1,31 @@
+import SwiftUI
+
+/// The Player edition view, designed to be the destination of
+/// a NavigationLink.
+struct PlayerEditor: View {
+ /// Manages edition of the player
+ let viewModel: PlayerFormViewModel
+
+ var body: some View {
+ PlayerForm(viewModel: viewModel)
+ .onDisappear(perform: {
+ // Ignore validation errors
+ try? self.viewModel.savePlayer()
+ })
+ }
+}
+
+#if DEBUG
+struct PlayerEditionView_Previews: PreviewProvider {
+ static var previews: some View {
+ let viewModel = try! PlayerFormViewModel(
+ database: .empty(),
+ player: .newRandom())
+
+ return NavigationView {
+ PlayerEditor(viewModel: viewModel)
+ .navigationBarTitle("Player Edition")
+ }
+ }
+}
+#endif
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerForm.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerForm.swift
new file mode 100644
index 0000000000..e94d769263
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerForm.swift
@@ -0,0 +1,41 @@
+import SwiftUI
+
+/// The Player edition form, embedded in both
+/// `PlayerCreationSheet` and `PlayerEditor`.
+struct PlayerForm: View {
+ /// Manages the player form
+ @ObservedObject var viewModel: PlayerFormViewModel
+
+ var body: some View {
+ List {
+ TextField("Name", text: $viewModel.name)
+ TextField("Score", text: $viewModel.score)
+ .keyboardType(.numberPad)
+ }
+ .listStyle(GroupedListStyle())
+ // Make sure the form is reset, in case a previous edition ended
+ // with a validation error.
+ //
+ // The bug we want to prevent is the following:
+ //
+ // 1. Launch the app
+ // 2. Tap a player
+ // 3. Erase the name so that validation fails
+ // 4. Hit the back button
+ // 5. Tap the same player
+ // 6. Bug: the form displays an empty name.
+ .onAppear(perform: viewModel.reset)
+ }
+}
+
+#if DEBUG
+struct PlayerFormView_Previews: PreviewProvider {
+ static var previews: some View {
+ let viewModel = try! PlayerFormViewModel(
+ database: .empty(),
+ player: .newRandom())
+
+ return PlayerForm(viewModel: viewModel)
+ }
+}
+#endif
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerList.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerList.swift
new file mode 100644
index 0000000000..eea7b466cc
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerList.swift
@@ -0,0 +1,131 @@
+import SwiftUI
+
+/// The list of players
+struct PlayerList: View {
+ /// Manages the list of players
+ @ObservedObject var viewModel: PlayerListViewModel
+
+ /// Controls the presentation of the player creation sheet.
+ @State private var newPlayerIsPresented = false
+
+ var body: some View {
+ NavigationView {
+ VStack {
+ playerList
+ toolbar
+ }
+ .navigationBarTitle(Text("\(viewModel.players.count) Players"))
+ .navigationBarItems(
+ leading: HStack {
+ EditButton()
+ newPlayerButton
+ },
+ trailing: toggleOrderingButton)
+ }
+ }
+
+ private var playerList: some View {
+ List {
+ ForEach(viewModel.players) { player in
+ NavigationLink(destination: self.editionView(for: player)) {
+ PlayerRow(player: player)
+ }
+ }
+ .onDelete(perform: { offsets in
+ self.viewModel.deletePlayers(atOffsets: offsets)
+ })
+ }
+ .listStyle(PlainListStyle())
+ }
+
+ private var toolbar: some View {
+ HStack {
+ Button(
+ action: viewModel.deleteAllPlayers,
+ label: { Image(systemName: "trash").imageScale(.large) })
+ Spacer()
+ Button(
+ action: viewModel.refreshPlayers,
+ label: { Image(systemName: "arrow.clockwise").imageScale(.large) })
+ Spacer()
+ Button(
+ action: viewModel.stressTest,
+ label: { Image(systemName: "tornado").imageScale(.large) })
+ }
+
+ .padding()
+ }
+
+ /// The button that toggles between name/score ordering.
+ private var toggleOrderingButton: some View {
+ switch viewModel.ordering {
+ case .byName:
+ return Button(action: viewModel.toggleOrdering, label: {
+ HStack {
+ Text("Name")
+ Image(systemName: "arrowtriangle.up.fill")
+ .imageScale(.small)
+ }
+ })
+ case .byScore:
+ return Button(action: viewModel.toggleOrdering, label: {
+ HStack {
+ Text("Score")
+ Image(systemName: "arrowtriangle.down.fill")
+ .imageScale(.small)
+ }
+ })
+ }
+ }
+
+ /// The view that edits a player in the list.
+ private func editionView(for player: Player) -> some View {
+ PlayerEditor(
+ viewModel: viewModel.editionViewModel(for: player))
+ .navigationBarTitle(player.name)
+ }
+
+ /// The button that presents the player creation sheet.
+ private var newPlayerButton: some View {
+ Button(
+ action: {
+ // Make sure we do not edit a previously created player.
+ self.viewModel.newPlayerViewModel.editNewPlayer()
+ self.newPlayerIsPresented = true
+ },
+ label: { Image(systemName: "plus").imageScale(.large) })
+ .sheet(
+ isPresented: $newPlayerIsPresented,
+ content: { self.newPlayerCreationSheet })
+ }
+
+ /// The player creation sheet.
+ private var newPlayerCreationSheet: some View {
+ PlayerCreationSheet(
+ viewModel: self.viewModel.newPlayerViewModel,
+ dismissAction: {
+ self.newPlayerIsPresented = false
+ })
+ }
+}
+
+struct PlayerRow: View {
+ var player: Player
+
+ var body: some View {
+ HStack {
+ Text(player.name)
+ Spacer()
+ Text("\(player.score) points").foregroundColor(.gray)
+ }
+ }
+}
+
+#if DEBUG
+struct PlayerListView_Previews: PreviewProvider {
+ static var previews: some View {
+ let viewModel = try! PlayerListViewModel(database: .random())
+ return PlayerList(viewModel: viewModel)
+ }
+}
+#endif
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/World.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/World.swift
new file mode 100644
index 0000000000..05f47b9bda
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/World.swift
@@ -0,0 +1,11 @@
+/// Dependency Injection based on the "How to Control the World" article:
+/// https://www.pointfree.co/blog/posts/21-how-to-control-the-world
+struct World {
+ /// The application database
+ var database: () -> AppDatabase
+}
+
+/// The current world.
+///
+/// Its setup is done by `AppDelegate`, or tests.
+var Current = World(database: { fatalError("Database is uninitialized") })
diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/en.lproj/Localizable.stringsdict b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/en.lproj/Localizable.stringsdict
new file mode 100644
index 0000000000..2d9aa217c4
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/en.lproj/Localizable.stringsdict
@@ -0,0 +1,42 @@
+
+
+
+
+ %lld Players
+
+ NSStringLocalizedFormatKey
+ %#@VARIABLE@
+ VARIABLE
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ lld
+ zero
+ No Player
+ one
+ 1 Player
+ other
+ %lld Players
+
+
+ %lld points
+
+ NSStringLocalizedFormatKey
+ %#@VARIABLE@
+ VARIABLE
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ lld
+ zero
+ 0 point
+ one
+ 1 point
+ other
+ %lld points
+
+
+
+
diff --git a/Documentation/DemoApps/GRDBCombineDemo/README.md b/Documentation/DemoApps/GRDBCombineDemo/README.md
new file mode 100644
index 0000000000..44cb25e214
--- /dev/null
+++ b/Documentation/DemoApps/GRDBCombineDemo/README.md
@@ -0,0 +1,36 @@
+Combine + SwiftUI Demo Application
+==================================
+
+
+
+**This demo application is a Combine + SwiftUI application, based on the MVVM design pattern.** For a demo application that uses UIKit, see [GRDBDemoiOS](../GRDBDemoiOS/README.md).
+
+The topics covered in this demo are:
+
+- How to setup a database in an iOS app.
+- How to define a simple [Codable Record](../../../README.md#codable-records).
+- How to track database changes and animate a SwiftUI List with [ValueObservation](../../../README.md#valueobservation) Combine publishers.
+- How to apply the recommendations of [Good Practices for Designing Record Types](../../GoodPracticesForDesigningRecordTypes.md).
+- How to feed SwiftUI previews with a transient database.
+
+**Files of interest:**
+
+- [AppDelegate.swift](GRDBCombineDemo/AppDelegate.swift)
+
+ `AppDelegate` creates, on application startup, a unique instance of [DatabaseQueue](../../../README.md#database-queues) available for the whole application.
+
+- [AppDatabase.swift](GRDBCombineDemo/AppDatabase.swift)
+
+ `AppDatabase` grants database access for the whole application. It uses [DatabaseMigrator](../../Migrations.md) in order to setup the database schema, and [ValueObservation](../../../README.md#valueobservation) in order to let the application observe database changes.
+
+- [Player.swift](GRDBCombineDemo/Player.swift)
+
+ `Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records). It defines the database requests used by the application.
+
+- [PlayerList.swift](GRDBCombineDemo/Views/PlayerList.swift) and [PlayerListViewModel.swift](GRDBCombineDemo/ViewModels/PlayerListViewModel.swift)
+
+ `PlayerList` is the SwiftUI view that displays the list of players, fed by `PlayerListViewModel`.
+
+- [PlayerForm.swift](GRDBCombineDemo/Views/PlayerForm.swift), [PlayerEditor.swift](GRDBCombineDemo/Views/PlayerEditor.swift), [PlayerCreationSheet.swift](GRDBCombineDemo/Views/PlayerCreationSheet.swift) and [PlayerFormViewModel.swift](GRDBCombineDemo/ViewModels/PlayerFormViewModel.swift).
+
+ `PlayerForm` is the SwiftUI view that displays a Player edition form. It is embedded in `PlayerEditor` and `PlayerCreationSheet`, two SwiftUI views that edit or create a player. All those views are fed by `PlayerFormViewModel`.
diff --git a/Documentation/DemoApps/GRDBCombineDemo/Screenshot.png b/Documentation/DemoApps/GRDBCombineDemo/Screenshot.png
new file mode 100644
index 0000000000..0536821854
Binary files /dev/null and b/Documentation/DemoApps/GRDBCombineDemo/Screenshot.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.pbxproj b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.pbxproj
index 5e30755888..b44cfca5f7 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.pbxproj
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.pbxproj
@@ -26,6 +26,8 @@
56B0361C1E8D9F38003B6DA4 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B0361A1E8D9F38003B6DA4 /* Player.swift */; };
56B036231E8D9F4C003B6DA4 /* PlayerEditionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */; };
56B036241E8D9F4C003B6DA4 /* PlayerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */; };
+ 56FE6F1E24A90CC500711EDF /* World.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FE6F1D24A90CC500711EDF /* World.swift */; };
+ 56FE6F2624A90CE400711EDF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FE6F2524A90CE400711EDF /* SceneDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -183,6 +185,8 @@
56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionViewController.swift; sourceTree = ""; };
56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerListViewController.swift; sourceTree = ""; };
56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GRDB.xcodeproj; path = ../../../GRDB.xcodeproj; sourceTree = ""; };
+ 56FE6F1D24A90CC500711EDF /* World.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = World.swift; sourceTree = ""; };
+ 56FE6F2524A90CE400711EDF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -273,17 +277,43 @@
56B036051E8D9EBE003B6DA4 /* GRDBDemoiOS */ = {
isa = PBXGroup;
children = (
- 56B036121E8D9EBE003B6DA4 /* Info.plist */,
- 56B036191E8D9F38003B6DA4 /* AppDatabase.swift */,
56B036061E8D9EBE003B6DA4 /* AppDelegate.swift */,
+ 56FE6F2524A90CE400711EDF /* SceneDelegate.swift */,
+ 56B036191E8D9F38003B6DA4 /* AppDatabase.swift */,
56B0361A1E8D9F38003B6DA4 /* Player.swift */,
- 56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */,
- 56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */,
+ 56FE6F1D24A90CC500711EDF /* World.swift */,
+ 56FE6F4F24A9114200711EDF /* ViewControllers */,
+ 56FE6F4E24A9112900711EDF /* Resources */,
+ 56FE6F5124A9118000711EDF /* Support */,
+ );
+ path = GRDBDemoiOS;
+ sourceTree = "";
+ };
+ 56FE6F4E24A9112900711EDF /* Resources */ = {
+ isa = PBXGroup;
+ children = (
56B0360D1E8D9EBE003B6DA4 /* Assets.xcassets */,
56B0360F1E8D9EBE003B6DA4 /* LaunchScreen.storyboard */,
56B0360A1E8D9EBE003B6DA4 /* Main.storyboard */,
);
- path = GRDBDemoiOS;
+ path = Resources;
+ sourceTree = "";
+ };
+ 56FE6F4F24A9114200711EDF /* ViewControllers */ = {
+ isa = PBXGroup;
+ children = (
+ 56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */,
+ 56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */,
+ );
+ path = ViewControllers;
+ sourceTree = "";
+ };
+ 56FE6F5124A9118000711EDF /* Support */ = {
+ isa = PBXGroup;
+ children = (
+ 56B036121E8D9EBE003B6DA4 /* Info.plist */,
+ );
+ name = Support;
sourceTree = "";
};
/* End PBXGroup section */
@@ -353,7 +383,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
- LastUpgradeCheck = 1020;
+ LastUpgradeCheck = 1200;
ORGANIZATIONNAME = "Gwendal Roué";
TargetAttributes = {
568E5FBE1E926430002582E0 = {
@@ -504,6 +534,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 56FE6F1E24A90CC500711EDF /* World.swift in Sources */,
+ 56FE6F2624A90CE400711EDF /* SceneDelegate.swift in Sources */,
56B0361C1E8D9F38003B6DA4 /* Player.swift in Sources */,
56B0361B1E8D9F38003B6DA4 /* AppDatabase.swift in Sources */,
56B036071E8D9EBE003B6DA4 /* AppDelegate.swift in Sources */,
@@ -657,6 +689,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -718,6 +751,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoWatchOS.xcscheme b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoWatchOS.xcscheme
index 2cd5c0f552..2a87216708 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoWatchOS.xcscheme
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoWatchOS.xcscheme
@@ -1,6 +1,6 @@
-
-
-
-
@@ -79,7 +70,7 @@
allowLocationSimulation = "YES">
-
-
-
-
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift
index 8e3e4d8e57..686defde7f 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift
@@ -7,7 +7,7 @@ import GRDB
final class AppDatabase {
private let dbQueue: DatabaseQueue
- /// Creates an AppDatabase and updates its schema if necessary.
+ /// Creates an AppDatabase and make sure the database schema is ready.
init(_ dbQueue: DatabaseQueue) throws {
self.dbQueue = dbQueue
try migrator.migrate(dbQueue)
@@ -19,32 +19,29 @@ final class AppDatabase {
private var migrator: DatabaseMigrator {
var migrator = DatabaseMigrator()
+ #if DEBUG
+ // Speed up development by nuking the database when migrations change
+ // See https://github.com/groue/GRDB.swift/blob/master/Documentation/Migrations.md#the-erasedatabaseonschemachange-option
+ migrator.eraseDatabaseOnSchemaChange = true
+ #endif
+
migrator.registerMigration("createPlayer") { db in
// Create a table
// See https://github.com/groue/GRDB.swift#create-tables
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
-
- // Sort player names in a localized case insensitive fashion by default
- // See https://github.com/groue/GRDB.swift/blob/master/README.md#unicode
- t.column("name", .text).notNull().collate(.localizedCaseInsensitiveCompare)
-
+ t.column("name", .text).notNull()
+ // Sort player names in a localized case insensitive fashion by default
+ // See https://github.com/groue/GRDB.swift/blob/master/README.md#unicode
+ .collate(.localizedCaseInsensitiveCompare)
t.column("score", .integer).notNull()
}
}
- migrator.registerMigration("fixtures") { db in
- // Populate the players table with random data
- for _ in 0..<8 {
- var player = Player(id: nil, name: Player.randomName(), score: Player.randomScore())
- try player.insert(db)
- }
- }
-
-// // Migrations for future application versions will be inserted here:
-// migrator.registerMigration(...) { db in
-// ...
-// }
+ // Migrations for future application versions will be inserted here:
+ // migrator.registerMigration(...) { db in
+ // ...
+ // }
return migrator
}
@@ -54,9 +51,7 @@ final class AppDatabase {
//
// This extension defines methods that fulfill application needs, both in terms
// of writes and reads.
-
extension AppDatabase {
-
// MARK: Writes
/// Save (insert or update) a player.
@@ -66,10 +61,10 @@ extension AppDatabase {
}
}
- /// Delete one player
- func deletePlayer(_ player: Player) throws {
+ /// Delete the specified players
+ func deletePlayers(ids: [Int64]) throws {
try dbQueue.write { db in
- _ = try player.delete(db)
+ _ = try Player.deleteAll(db, keys: ids)
}
}
@@ -85,14 +80,11 @@ extension AppDatabase {
try dbQueue.write { db in
if try Player.fetchCount(db) == 0 {
// Insert new random players
- for _ in 0..<8 {
- var player = Player(id: nil, name: Player.randomName(), score: Player.randomScore())
- try player.insert(db)
- }
+ try createRandomPlayers(db)
} else {
// Insert a player
if Bool.random() {
- var player = Player(id: nil, name: Player.randomName(), score: Player.randomScore())
+ var player = Player.newRandom()
try player.insert(db)
}
// Delete a random player
@@ -109,6 +101,23 @@ extension AppDatabase {
}
}
+ /// Create random players if the database is empty.
+ func createRandomPlayersIfEmpty() throws {
+ try dbQueue.write { db in
+ if try Player.fetchCount(db) == 0 {
+ try createRandomPlayers(db)
+ }
+ }
+ }
+
+ /// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`.
+ private func createRandomPlayers(_ db: Database) throws {
+ for _ in 0..<8 {
+ var player = Player.newRandom()
+ try player.insert(db)
+ }
+ }
+
// MARK: Reads
/// Tracks changes in the number of players
@@ -154,3 +163,13 @@ extension AppDatabase {
}
}
+// MARK: - Support for Tests
+
+#if DEBUG
+extension AppDatabase {
+ /// Returns an empty, in-memory database, suitable for testing.
+ static func empty() throws -> AppDatabase {
+ try AppDatabase(DatabaseQueue())
+ }
+}
+#endif
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDelegate.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDelegate.swift
index 834856c3ed..c84522617c 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDelegate.swift
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDelegate.swift
@@ -1,20 +1,32 @@
import UIKit
import GRDB
-// The shared application database
-var appDatabase: AppDatabase!
-
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
- var window: UIWindow?
-
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
- try! setupDatabase(application)
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ // Setup database. Error handling left as an exercise for the reader.
+ try! setupDatabase()
return true
}
- private func setupDatabase(_ application: UIApplication) throws {
- // AppDelegate is responsible for choosing the location of the database file.
+ // MARK: UISceneSession Lifecycle
+
+ func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
+ // Called when a new scene session is being created.
+ // Use this method to select a configuration to create the new scene with.
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+ }
+
+ func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
+ // Called when the user discards a scene session.
+ // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
+ // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
+ }
+
+ // MARK: - Database Setup
+
+ private func setupDatabase() throws {
+ // AppDelegate chooses the location of the database file.
// See https://github.com/groue/GRDB.swift/blob/master/README.md#database-connections
let databaseURL = try FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
@@ -22,6 +34,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let dbQueue = try DatabaseQueue(path: databaseURL.path)
// Create the shared application database
- appDatabase = try AppDatabase(dbQueue)
+ let database = try AppDatabase(dbQueue)
+
+ // Populate the database if it is empty, for better demo purpose.
+ try database.createRandomPlayersIfEmpty()
+
+ // Expose it to the rest of the application
+ Current.database = { database }
}
}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Base.lproj/LaunchScreen.storyboard b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index 2e721e1833..0000000000
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Info.plist b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Info.plist
index 6905cc67bb..b5f9c0796b 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Info.plist
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Info.plist
@@ -3,7 +3,7 @@
CFBundleDevelopmentRegion
- en
+ $(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -13,15 +13,32 @@
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
- APPL
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
- CFBundleSignature
- ????
CFBundleVersion
1
LSRequiresIPhoneOS
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -33,6 +50,11 @@
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift
index f786ff0679..8e7c114b6c 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift
@@ -1,14 +1,49 @@
import GRDB
-/// The Player struct
-struct Player {
- var id: Int64? // Use Int64 for auto-incremented database ids
+/// The Player struct.
+///
+/// Hashable conformance supports table view updates
+struct Player: Hashable {
+ /// The player id.
+ ///
+ /// Int64 is the recommended type for auto-incremented database ids.
+ /// Use nil for players that are not inserted yet in the database.
+ var id: Int64?
var name: String
var score: Int
}
-/// Hashable conformance supports tableView diffing
-extension Player: Hashable { }
+extension Player {
+ private static let names = [
+ "Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David",
+ "Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette",
+ "Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl",
+ "Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole",
+ "Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul",
+ "Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain",
+ "Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann",
+ "Zazie", "Zoé"]
+
+ /// Creates a new player with empty name and zero score
+ static func new() -> Player {
+ Player(id: nil, name: "", score: 0)
+ }
+
+ /// Creates a new player with random name and random score
+ static func newRandom() -> Player {
+ Player(id: nil, name: randomName(), score: randomScore())
+ }
+
+ /// Returns a random name
+ static func randomName() -> String {
+ names.randomElement()!
+ }
+
+ /// Returns a random score
+ static func randomScore() -> Int {
+ 10 * Int.random(in: 0...100)
+ }
+}
// MARK: - Persistence
@@ -18,18 +53,17 @@ extension Player: Hashable { }
extension Player: Codable, FetchableRecord, MutablePersistableRecord {
// Define database columns from CodingKeys
fileprivate enum Columns {
- static let id = Column(CodingKeys.id)
static let name = Column(CodingKeys.name)
static let score = Column(CodingKeys.score)
}
- // Update a player id after it has been inserted in the database.
+ /// Updates a player id after it has been inserted in the database.
mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
}
}
-// MARK: - Player Requests
+// MARK: - Player Database Requests
/// Define some player requests used by the application.
///
@@ -58,25 +92,3 @@ extension DerivableRequest where RowDecoder == Player {
order(Player.Columns.score.desc, Player.Columns.name)
}
}
-
-// MARK: - Player Randomization
-
-extension Player {
- private static let names = [
- "Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David",
- "Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette",
- "Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl",
- "Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole",
- "Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul",
- "Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain",
- "Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann",
- "Zazie", "Zoé"]
-
- static func randomName() -> String {
- names.randomElement()!
- }
-
- static func randomScore() -> Int {
- 10 * Int.random(in: 0...100)
- }
-}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayersViewController.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayersViewController.swift
deleted file mode 100644
index a3661e716d..0000000000
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayersViewController.swift
+++ /dev/null
@@ -1,225 +0,0 @@
-import UIKit
-import GRDB
-
-/// PlayerListViewController displays the list of players.
-class PlayerListViewController: UITableViewController {
- private enum PlayerOrdering {
- case byName
- case byScore
- }
-
- @IBOutlet private weak var newPlayerButtonItem: UIBarButtonItem!
- private var players: [Player] = []
- private var playersCancellable: DatabaseCancellable?
- private var playerCountCancellable: DatabaseCancellable?
- private var playerOrdering: PlayerOrdering = .byScore {
- didSet {
- configureOrderingBarButtonItem()
- configureTableView()
- }
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- configureToolbar()
- configureNavigationItem()
- configureTableView()
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- navigationController?.isToolbarHidden = false
- }
-
- private func configureToolbar() {
- toolbarItems = [
- UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(deletePlayers)),
- UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
- UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh)),
- UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
- UIBarButtonItem(title: "💣", style: .plain, target: self, action: #selector(stressTest)),
- ]
- }
-
- private func configureNavigationItem() {
- navigationItem.leftBarButtonItems = [editButtonItem, newPlayerButtonItem]
- configureOrderingBarButtonItem()
- configureTitle()
- }
-
- private func configureOrderingBarButtonItem() {
- switch playerOrdering {
- case .byScore:
- navigationItem.rightBarButtonItem = UIBarButtonItem(
- title: "Score ▼",
- style: .plain,
- target: self, action: #selector(sortByName))
- case .byName:
- navigationItem.rightBarButtonItem = UIBarButtonItem(
- title: "Name ▲",
- style: .plain,
- target: self, action: #selector(sortByScore))
- }
- }
-
- private func configureTitle() {
- playerCountCancellable = appDatabase.observePlayerCount(
- onError: { error in fatalError("Unexpected error: \(error)") },
- onChange: { [weak self] count in
- guard let self = self else { return }
- switch count {
- case 0: self.navigationItem.title = "No Player"
- case 1: self.navigationItem.title = "1 Player"
- default: self.navigationItem.title = "\(count) Players"
- }
- })
- }
-
- private func configureTableView() {
- switch playerOrdering {
- case .byName:
- playersCancellable = appDatabase.observePlayersOrderedByName(
- onError: { error in fatalError("Unexpected error: \(error)") },
- onChange: { [weak self] players in
- guard let self = self else { return }
- self.updateTableView(players)
- })
- case .byScore:
- playersCancellable = appDatabase.observePlayersOrderedByScore(
- onError: { error in fatalError("Unexpected error: \(error)") },
- onChange: { [weak self] players in
- guard let self = self else { return }
- self.updateTableView(players)
- })
- }
- }
-
- private func updateTableView(_ players: [Player]) {
- // Compute difference between current and new list of players
- let difference = players
- .difference(from: self.players)
- .inferringMoves()
-
- // Apply those changes to the table view
- tableView.performBatchUpdates({
- self.players = players
- for change in difference {
- switch change {
- case let .remove(offset, _, associatedWith):
- if let associatedWith = associatedWith {
- self.tableView.moveRow(
- at: IndexPath(row: offset, section: 0),
- to: IndexPath(row: associatedWith, section: 0))
- } else {
- self.tableView.deleteRows(
- at: [IndexPath(row: offset, section: 0)],
- with: .fade)
- }
- case let .insert(offset, _, associatedWith):
- if associatedWith == nil {
- self.tableView.insertRows(
- at: [IndexPath(row: offset, section: 0)],
- with: .fade)
- }
- }
- }
- }, completion: nil)
- }
-}
-
-
-// MARK: - Navigation
-
-extension PlayerListViewController {
-
- override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
- if segue.identifier == "Edit" {
- let player = players[tableView.indexPathForSelectedRow!.row]
- let controller = segue.destination as! PlayerEditionViewController
- controller.title = player.name
- controller.player = player
- controller.presentation = .push
- }
- else if segue.identifier == "New" {
- setEditing(false, animated: true)
- let navigationController = segue.destination as! UINavigationController
- let controller = navigationController.viewControllers.first as! PlayerEditionViewController
- controller.title = "New Player"
- controller.player = Player(id: nil, name: "", score: 0)
- controller.presentation = .modal
- }
- }
-
- @IBAction func cancelPlayerEdition(_ segue: UIStoryboardSegue) {
- // Player creation cancelled
- }
-
- @IBAction func commitPlayerEdition(_ segue: UIStoryboardSegue) {
- // Player creation committed
- }
-}
-
-
-// MARK: - UITableViewDataSource
-
-extension PlayerListViewController {
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- players.count
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: "Player", for: indexPath)
- configure(cell, at: indexPath)
- return cell
- }
-
- override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
- // Delete the player
- let player = players[indexPath.row]
- try! appDatabase.deletePlayer(player)
- }
-
- private func configure(_ cell: UITableViewCell, at indexPath: IndexPath) {
- let player = players[indexPath.row]
- if player.name.isEmpty {
- cell.textLabel?.text = "-"
- } else {
- cell.textLabel?.text = player.name
- }
- cell.detailTextLabel?.text = abs(player.score) > 1 ? "\(player.score) points" : "0 point"
- }
-}
-
-
-// MARK: - Actions
-
-extension PlayerListViewController {
- @IBAction func sortByName() {
- setEditing(false, animated: true)
- playerOrdering = .byName
- }
-
- @IBAction func sortByScore() {
- setEditing(false, animated: true)
- playerOrdering = .byScore
- }
-
- @IBAction func deletePlayers() {
- setEditing(false, animated: true)
- try! appDatabase.deleteAllPlayers()
- }
-
- @IBAction func refresh() {
- setEditing(false, animated: true)
- try! appDatabase.refreshPlayers()
- }
-
- @IBAction func stressTest() {
- setEditing(false, animated: true)
- for _ in 0..<50 {
- DispatchQueue.global().async {
- try! appDatabase.refreshPlayers()
- }
- }
- }
-}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..29d91251df
--- /dev/null
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,116 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "icon_20pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "icon_20pt@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "icon_29pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "icon_29pt@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "icon_40pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "icon_40pt@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "icon_60pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "icon_60pt@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "icon_20pt.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "icon_20pt@2x-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "icon_29pt.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "icon_29pt@2x-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "icon_40pt.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "icon_40pt@2x-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "icon_76pt.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "icon_76pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "icon_83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png
new file mode 100644
index 0000000000..66b1931a14
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png
new file mode 100644
index 0000000000..90648b3f40
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png
new file mode 100644
index 0000000000..a077a6f490
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png
new file mode 100644
index 0000000000..a077a6f490
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png
new file mode 100644
index 0000000000..600bdbd9cd
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png
new file mode 100644
index 0000000000..8e04af0dd8
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png
new file mode 100644
index 0000000000..686e8d99e2
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png
new file mode 100644
index 0000000000..686e8d99e2
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png
new file mode 100644
index 0000000000..1d013c3d33
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png
new file mode 100644
index 0000000000..a077a6f490
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png
new file mode 100644
index 0000000000..da66b9ba82
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png
new file mode 100644
index 0000000000..da66b9ba82
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png
new file mode 100644
index 0000000000..59346ef4b6
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png
new file mode 100644
index 0000000000..59346ef4b6
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png
new file mode 100644
index 0000000000..d4640afc9a
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png
new file mode 100644
index 0000000000..e3a04522bf
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png
new file mode 100644
index 0000000000..593ebd783d
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
new file mode 100644
index 0000000000..ca02cd03bc
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json
new file mode 100644
index 0000000000..2cbe59d5ec
--- /dev/null
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "LaunchIcon.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf
new file mode 100644
index 0000000000..2660891492
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf differ
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/LaunchScreen.storyboard b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000000..307d45b11d
--- /dev/null
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Base.lproj/Main.storyboard b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/Main.storyboard
similarity index 100%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Base.lproj/Main.storyboard
rename to Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/Main.storyboard
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/SceneDelegate.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/SceneDelegate.swift
new file mode 100644
index 0000000000..b484ed11dc
--- /dev/null
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/SceneDelegate.swift
@@ -0,0 +1,40 @@
+import UIKit
+
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+ var window: UIWindow?
+
+ func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+ // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
+ // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
+ // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
+ guard let _ = (scene as? UIWindowScene) else { return }
+ }
+
+ func sceneDidDisconnect(_ scene: UIScene) {
+ // Called as the scene is being released by the system.
+ // This occurs shortly after the scene enters the background, or when its session is discarded.
+ // Release any resources associated with this scene that can be re-created the next time the scene connects.
+ // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
+ }
+
+ func sceneDidBecomeActive(_ scene: UIScene) {
+ // Called when the scene has moved from an inactive state to an active state.
+ // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
+ }
+
+ func sceneWillResignActive(_ scene: UIScene) {
+ // Called when the scene will move from an active state to an inactive state.
+ // This may occur due to temporary interruptions (ex. an incoming phone call).
+ }
+
+ func sceneWillEnterForeground(_ scene: UIScene) {
+ // Called as the scene transitions from the background to the foreground.
+ // Use this method to undo the changes made on entering the background.
+ }
+
+ func sceneDidEnterBackground(_ scene: UIScene) {
+ // Called as the scene transitions from the foreground to the background.
+ // Use this method to save data, release shared resources, and store enough scene-specific state information
+ // to restore the scene back to its current state.
+ }
+}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayerEditionViewController.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift
similarity index 98%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayerEditionViewController.swift
rename to Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift
index 9c1b15ffdd..96f1a63644 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayerEditionViewController.swift
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift
@@ -127,7 +127,7 @@ extension PlayerEditionViewController: UITextFieldDelegate {
}
player.name = nameTextField.text ?? ""
player.score = scoreTextField.text.flatMap { Int($0) } ?? 0
- try! appDatabase.savePlayer(&player)
+ try! Current.database().savePlayer(&player)
self.player = player
}
}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayerListViewController.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerListViewController.swift
similarity index 92%
rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayerListViewController.swift
rename to Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerListViewController.swift
index 54c2ff6655..8719184d3f 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayerListViewController.swift
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerListViewController.swift
@@ -38,7 +38,7 @@ class PlayerListViewController: UITableViewController {
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh)),
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
- UIBarButtonItem(title: "💣", style: .plain, target: self, action: #selector(stressTest)),
+ UIBarButtonItem(image: UIImage(systemName: "tornado"), style: .plain, target: self, action: #selector(stressTest)),
]
}
@@ -67,7 +67,7 @@ class PlayerListViewController: UITableViewController {
}
private func configureTitle() {
- playerCountCancellable = appDatabase.observePlayerCount(
+ playerCountCancellable = Current.database().observePlayerCount(
onError: { error in fatalError("Unexpected error: \(error)") },
onChange: { [weak self] count in
guard let self = self else { return }
@@ -82,14 +82,14 @@ class PlayerListViewController: UITableViewController {
private func configureTableView() {
switch playerOrdering {
case .byName:
- playersCancellable = appDatabase.observePlayersOrderedByName(
+ playersCancellable = Current.database().observePlayersOrderedByName(
onError: { error in fatalError("Unexpected error: \(error)") },
onChange: { [weak self] players in
guard let self = self else { return }
self.updateTableView(with: players)
})
case .byScore:
- playersCancellable = appDatabase.observePlayersOrderedByScore(
+ playersCancellable = Current.database().observePlayersOrderedByScore(
onError: { error in fatalError("Unexpected error: \(error)") },
onChange: { [weak self] players in
guard let self = self else { return }
@@ -193,8 +193,9 @@ extension PlayerListViewController {
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
// Delete the player
- let player = players[indexPath.row]
- try! appDatabase.deletePlayer(player)
+ if let id = players[indexPath.row].id {
+ try! Current.database().deletePlayers(ids: [id])
+ }
}
private func configure(_ cell: UITableViewCell, at indexPath: IndexPath) {
@@ -224,19 +225,19 @@ extension PlayerListViewController {
@IBAction func deletePlayers() {
setEditing(false, animated: true)
- try! appDatabase.deleteAllPlayers()
+ try! Current.database().deleteAllPlayers()
}
@IBAction func refresh() {
setEditing(false, animated: true)
- try! appDatabase.refreshPlayers()
+ try! Current.database().refreshPlayers()
}
@IBAction func stressTest() {
setEditing(false, animated: true)
for _ in 0..<50 {
DispatchQueue.global().async {
- try! appDatabase.refreshPlayers()
+ try! Current.database().refreshPlayers()
}
}
}
diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/World.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/World.swift
new file mode 100644
index 0000000000..05f47b9bda
--- /dev/null
+++ b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/World.swift
@@ -0,0 +1,11 @@
+/// Dependency Injection based on the "How to Control the World" article:
+/// https://www.pointfree.co/blog/posts/21-how-to-control-the-world
+struct World {
+ /// The application database
+ var database: () -> AppDatabase
+}
+
+/// The current world.
+///
+/// Its setup is done by `AppDelegate`, or tests.
+var Current = World(database: { fatalError("Database is uninitialized") })
diff --git a/Documentation/DemoApps/GRDBDemoiOS/README.md b/Documentation/DemoApps/GRDBDemoiOS/README.md
index 99d5e747b1..77585e2a4c 100644
--- a/Documentation/DemoApps/GRDBDemoiOS/README.md
+++ b/Documentation/DemoApps/GRDBDemoiOS/README.md
@@ -1,9 +1,11 @@
-Demo Application
-================
+UIKit Demo Application
+======================
-
+
-**This demo application shows you:**
+**This demo application is a storyboard-based UIKit application, based on the MVC design pattern.** For a demo application that uses Combine and SwiftUI, see [GRDBCombineDemo](../GRDBCombineDemo/README.md).
+
+The topics covered in this demo are:
- How to setup a database in an iOS app.
- How to define a simple [Codable Record](../../../README.md#codable-records).
@@ -24,10 +26,10 @@ Demo Application
`Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records). It defines the database requests used by the application.
-- [PlayerListViewController.swift](GRDBDemoiOS/PlayerListViewController.swift)
+- [PlayerListViewController.swift](GRDBDemoiOS/ViewControllers/PlayerListViewController.swift)
`PlayerListViewController` displays the list of players.
-- [PlayerEditionViewController.swift](GRDBDemoiOS/PlayerEditionViewController.swift)
+- [PlayerEditionViewController.swift](GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift)
`PlayerEditionViewController` can create or edit a player, and save it in the database.
diff --git a/Documentation/DemoApps/GRDBDemoiOS/Screenshot.png b/Documentation/DemoApps/GRDBDemoiOS/Screenshot.png
new file mode 100644
index 0000000000..7a0fccd710
Binary files /dev/null and b/Documentation/DemoApps/GRDBDemoiOS/Screenshot.png differ
diff --git a/Documentation/DemoApps/README.md b/Documentation/DemoApps/README.md
new file mode 100644
index 0000000000..6be27437e9
--- /dev/null
+++ b/Documentation/DemoApps/README.md
@@ -0,0 +1,6 @@
+Demo Applications
+=================
+
+- [UIKit Demo Application](GRDBDemoiOS/README.md): a storyboard-based UIKit application, based on the MVC design pattern.
+- [Combine + SwiftUI Demo Application](GRDBCombineDemo/README.md): a Combine + SwiftUI application, based on the MVVM design pattern.
+
diff --git a/Documentation/FetchedRecordsController.md b/Documentation/FetchedRecordsController.md
index 8f8ff42d18..6051dc1fff 100644
--- a/Documentation/FetchedRecordsController.md
+++ b/Documentation/FetchedRecordsController.md
@@ -341,7 +341,5 @@ try controller.performFetch()
[ValueObservation]: ../README.md#valueobservation
-[GRDBCombine]: http://github.com/groue/GRDBCombine
-[RxGRDB]: http://github.com/RxSwiftCommunity/RxGRDB
[FetchableRecord]: ../README.md#fetchablerecord-protocol
[TableRecord]: ../README.md#tablerecord-protocol
diff --git a/Documentation/FullTextSearch.md b/Documentation/FullTextSearch.md
index 0b1ce5baa3..7de8f52e61 100644
--- a/Documentation/FullTextSearch.md
+++ b/Documentation/FullTextSearch.md
@@ -280,7 +280,7 @@ let pattern = FTS3Pattern(matchingAnyTokenIn: "") // nil
let pattern = FTS3Pattern(matchingAnyTokenIn: "*") // nil
```
-FTS3Pattern are regular [values](../README.md#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/5.0.0-beta.5/Structs/StatementArguments.html):
+FTS3Pattern are regular [values](../README.md#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/5.0.0-beta.6/Structs/StatementArguments.html):
```swift
let documents = try Document.fetchAll(db,
@@ -529,7 +529,7 @@ let pattern = FTS5Pattern(matchingAnyTokenIn: "") // nil
let pattern = FTS5Pattern(matchingAnyTokenIn: "*") // nil
```
-FTS5Pattern are regular [values](../README.md#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/5.0.0-beta.5/Structs/StatementArguments.html):
+FTS5Pattern are regular [values](../README.md#values). You can use them as query [arguments](http://groue.github.io/GRDB.swift/docs/5.0.0-beta.6/Structs/StatementArguments.html):
```swift
let documents = try Document.fetchAll(db,
diff --git a/Documentation/GRDB5MigrationGuide.md b/Documentation/GRDB5MigrationGuide.md
index a2f0cd3dd4..8e3b7b3432 100644
--- a/Documentation/GRDB5MigrationGuide.md
+++ b/Documentation/GRDB5MigrationGuide.md
@@ -6,6 +6,7 @@ Migrating From GRDB 4 to GRDB 5
- [Preparing the Migration to GRDB 5](#preparing-the-migration-to-grdb-5)
- [New requirements](#new-requirements)
- [ValueObservation](#valueobservation)
+- [Combine Integration](#combine-integration)
- [Other Changes](#other-changes)
@@ -34,7 +35,7 @@ GRDB requirements have been bumped:
[ValueObservation] is the database observation tool that tracks changes in database values. It has quite changed in GRDB 5.
-Those changes have the vanilla GRDB, [GRDBCombine], and [RxGRDB], offer a common API, and a common behavior. This greatly helps choosing or switching your preferred database observation technique. In previous versions of GRDB, the three companion libraries used to have subtle differences that were just opportunities for bugs.
+Those changes have the vanilla GRDB, its [Combine publishers], and [RxGRDB] offer a common API, and a common behavior. This greatly helps choosing or switching your preferred database observation technique. In previous versions of GRDB, the three companion libraries used to have subtle differences that were just opportunities for bugs.
In the end, this migration step might require some work. But it's for the benefit of all!
@@ -188,7 +189,7 @@ The changes can quite impact your application. We'll describe them below, as wel
Note that the `.immediate` scheduling requires that the observation starts from the main thread. A fatal error is raised otherwise.
- GRDBCombine impact
+ Combine impact
```swift
let observation = ValueObservation.tracking(Player.fetchAll)
@@ -262,37 +263,68 @@ The changes can quite impact your application. We'll describe them below, as wel
1. ValueObservation used to have a `compactMap` method. This method has been removed without any replacement.
- If your application uses GRDBCombine or RxGRDB, then use the `compactMap` method from Combine or RxSwift instead.
+ If your application uses Combine publishers or RxGRDB, then use the `compactMap` method from Combine or RxSwift instead.
2. ValueObservation used to have a `combine` method. This method has been removed without any replacement.
In your application, replace combined observations with a single observation:
```swift
+ struct HallOfFame {
+ var totalPlayerCount: Int
+ var bestPlayers: [Player]
+ }
+
// BEFORE: GRDB 4
- let playerCountObservation = ValueObservation.tracking(Player.fetchCount)
+ let totalPlayerCountObservation = ValueObservation.tracking(Player.fetchCount)
+
let bestPlayersObservation = ValueObservation.tracking(Player
.limit(10)
.order(Column("score").desc)
.fetchAll)
+
let observation = ValueObservation
- .combine(playerCountObservation, bestPlayersObservation)
+ .combine(totalPlayerCountObservation, bestPlayersObservation)
.map(HallOfFame.init)
// NEW: GRDB 5
let observation = ValueObservation.tracking { db -> HallOfFame in
- let playerCount = try Player.fetchCount(db)
+ let totalPlayerCount = try Player.fetchCount(db)
+
let bestPlayers = try Player
- .limit(10)
.order(Column("score").desc)
+ .limit(10)
.fetchAll(db)
- return HallOfFame(playerCount: playerCount, bestPlayers: bestPlayers)
+
+ return HallOfFame(
+ totalPlayerCount: totalPlayerCount,
+ bestPlayers: bestPlayers)
}
```
As is previous versions of GRDB, do not use the `combineLatest` operators of Combine or RxSwift in order to combine several ValueObservation. You would lose all guarantees of [data consistency](https://en.wikipedia.org/wiki/Consistency_(database_systems)).
+## Combine Integration
+
+GRDB 4 had a companion library named GRDBCombine. Combine support is now embedded right into GRDB 5, and you have to remove any dependency on GRDBCombine.
+
+GRDBCombine used to define a `fetchOnSubscription()` method of the ValueObservation subscriber. It has been removed. Replace it with `scheduling: .immediate` for the same effect (an initial value is notified immediately, synchronously, when the publisher is subscribed):
+
+```swift
+// BEFORE: GRDB 4 + GRDBCombine
+let observation = ValueObservation.tracking { db in ... }
+let publisher = observation
+ .publisher(in: dbQueue)
+ .fetchOnSubscription()
+
+// NEW: GRDB 5
+let observation = ValueObservation.tracking { db in ... }
+let publisher = observation
+ .publisher(in: dbQueue, scheduling: .immediate)
+```
+
+
## Other Changes
1. The `Configuration.trace` property has been removed. You know use the `Database.trace(options:_:)` method instead:
@@ -429,7 +461,6 @@ The changes can quite impact your application. We'll describe them below, as wel
[ValueObservation]: ../README.md#valueobservation
[DatabaseRegionObservation]: ../README.md#databaseregionobservation
[RxGRDB]: http://github.com/RxSwiftCommunity/RxGRDB
-[GRDBCombine]: http://github.com/groue/GRDBCombine
[Observing a Varying Database Region]: ../README.md#observing-a-varying-database-region
[removeDuplicates]: ../README.md#valueobservationremoveduplicates
[Custom SQL functions]: ../README.md#custom-sql-functions
@@ -438,3 +469,4 @@ The changes can quite impact your application. We'll describe them below, as wel
[SQLLiteral]: SQLInterpolation.md#sqlliteral
[SQLRequest]: ../README.md#custom-requests
[QueryInterfaceRequest]: ../README.md#requests
+[Combine publishers]: Combine.md
diff --git a/Documentation/GoodPracticesForDesigningRecordTypes.md b/Documentation/GoodPracticesForDesigningRecordTypes.md
index a26167f904..9c20bfac78 100644
--- a/Documentation/GoodPracticesForDesigningRecordTypes.md
+++ b/Documentation/GoodPracticesForDesigningRecordTypes.md
@@ -679,7 +679,7 @@ Instead, have a look at [Database Observation]:
> :bulb: **Tip**: [ValueObservation] performs automated tracking of database changes.
>
-> :bulb: **Tip**: [GRDBCombine] performs automated tracking of database changes, in the [Combine](https://developer.apple.com/documentation/combine) way.
+> :bulb: **Tip**: [Combine Support] allows automated tracking of database changes, in the [Combine](https://developer.apple.com/documentation/combine) way.
>
> :bulb: **Tip**: [RxGRDB] performs automated tracking of database changes, in the [RxSwift](https://github.com/ReactiveX/RxSwift) way.
>
@@ -710,7 +710,6 @@ Instead, have a look at [Database Observation]:
[Database Observation]: ../README.md#database-changes-observation
[ValueObservation]: ../README.md#valueobservation
[RxGRDB]: http://github.com/RxSwiftCommunity/RxGRDB
-[GRDBCombine]: http://github.com/groue/GRDBCombine
[TransactionObserver]: ../README.md#transactionobserver-protocol
[Trust SQLite More Than Yourself]: #trust-sqlite-more-than-yourself
[Persistable Record Types are Responsible for Their Tables]: #persistable-record-types-are-responsible-for-their-tables
@@ -727,3 +726,4 @@ Instead, have a look at [Database Observation]:
[Association Aggregates]: AssociationsBasics.md#association-aggregates
[Codable Record]: ../README.md#codable-records
[CodingKeys]: https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
+[Combine Support]: Combine.md
diff --git a/Documentation/Images/GRDBDemoScreenshot.png b/Documentation/Images/GRDBDemoScreenshot.png
deleted file mode 100644
index f8f3a7bac2..0000000000
Binary files a/Documentation/Images/GRDBDemoScreenshot.png and /dev/null differ
diff --git a/Documentation/Migrations.md b/Documentation/Migrations.md
index a4572ecbcf..4cb84459a8 100644
--- a/Documentation/Migrations.md
+++ b/Documentation/Migrations.md
@@ -68,7 +68,7 @@ try dbQueue.read { db in
}
```
-See the [DatabaseMigrator reference](http://groue.github.io/GRDB.swift/docs/5.0.0-beta.5/Structs/DatabaseMigrator.html) for more migrator methods.
+See the [DatabaseMigrator reference](http://groue.github.io/GRDB.swift/docs/5.0.0-beta.6/Structs/DatabaseMigrator.html) for more migrator methods.
## The `eraseDatabaseOnSchemaChange` Option
diff --git a/Documentation/SharingADatabase.md b/Documentation/SharingADatabase.md
index de2f52a9a3..978965a1be 100644
--- a/Documentation/SharingADatabase.md
+++ b/Documentation/SharingADatabase.md
@@ -254,7 +254,7 @@ See https://developer.apple.com/library/archive/technotes/tn2151/_index.html for
## How to perform cross-process database observation
-GRDB [Database Observation] features, as well as [GRDBCombine] and [RxGRDB], are not able to notify database changes performed by other processes.
+GRDB [Database Observation] features, as well as its [Combine publishers] and [RxGRDB], are not able to notify database changes performed by other processes.
Whenever you need to notify other processes that the database has been changed, you will have to use a cross-process notification mechanism such as [NSFileCoordinator] or [CFNotificationCenterGetDarwinNotifyCenter].
@@ -280,10 +280,10 @@ let observer = try observation.start(in: dbPool) { (db: Database) in
[How to perform cross-process database observation]: #how-to-perform-cross-process-database-observation
[Database Pool]: ../README.md#database-pools
[Database Observation]: ../README.md#database-changes-observation
-[GRDBCombine]: http://github.com/groue/GRDBCombine
[RxGRDB]: https://github.com/RxSwiftCommunity/RxGRDB
[NSFileCoordinator]: https://developer.apple.com/documentation/foundation/nsfilecoordinator
[CFNotificationCenterGetDarwinNotifyCenter]: https://developer.apple.com/documentation/corefoundation/1542572-cfnotificationcentergetdarwinnot
[DatabaseRegionObservation]: ../README.md#databaseregionobservation
[WAL mode]: https://www.sqlite.org/wal.html
[How do I monitor the duration of database statements execution?]: ../README.md#how-do-i-monitor-the-duration-of-database-statements-execution
+[Combine publishers]: Combine.md
diff --git a/Documentation/WhyAdoptGRDB.md b/Documentation/WhyAdoptGRDB.md
index b7a8c0aa05..e34d5a694f 100644
--- a/Documentation/WhyAdoptGRDB.md
+++ b/Documentation/WhyAdoptGRDB.md
@@ -117,7 +117,7 @@ let players = try Player.fetchAll(db)
**Fetched records behave just like an in-memory cache of the database content.** Your application is free to decide, on its own, how it should handle the lifetime of those cached values: by ignoring future database changes, or by observing database changes and react accordingly.
-In order to keep your views synchronized with the database content, you can use [ValueObservation]. It notifies fresh values after each database change. The [GRDBCombine] and [RxGRDB] companion libraries provide support for [Combine] and [RxSwift].
+In order to keep your views synchronized with the database content, you can use [ValueObservation]. It notifies fresh values after each database change, with convenient support for [Combine](Combine.md) and [RxSwift](https://github.com/RxSwiftCommunity/RxGRDB).
```swift
/// An observation of [Player]
@@ -130,7 +130,7 @@ let cancellable = observation.start(in: dbQueue,
onError: { error in ... },
onChange: { (players: [Player]) in print("Fresh players") })
-// GRDBCombine
+// GRDB + Combine
let cancellable = observation.publisher(in: dbQueue).sink(
receiveCompletion: { completion in ... },
receiveValue: { (players: [Player]) in print("Fresh players") })
@@ -254,14 +254,14 @@ extension Player {
let player = try Player.filter(name: "Arthur O'Brien").fetchOne(db)
```
-Custom SQL requests as the one above are welcome in database observation tools like the built-in [ValueObservation], or the companion libraries [GRDBCombine] and [RxGRDB]:
+Custom SQL requests as the one above are welcome in database observation tools like the built-in [ValueObservation] and its [Combine](Combine.md) and [RxSwift](https://github.com/RxSwiftCommunity/RxGRDB) flavors:
```swift
let playerObservation = ValueObservation.tracking { db in
try Player.filter(name: "Arthur O'Brien").fetchOne(db)
}
-// Observe the SQL request with GRDBCombine
+// Observe the SQL request with Combine
let cancellable = playerObservation.publisher(in: dbQueue).sink(
receiveCompletion: { completion in ... },
receiveValue: { (player: Player?) in print("Player has changed") })
@@ -300,10 +300,6 @@ Happy GRDB! :gift:
[PersistableRecord]: ../README.md#records
[Realm]: http://realm.io
[FetchableRecord]: ../README.md#records
-[RxGRDB]: https://github.com/RxSwiftCommunity/RxGRDB
-[RxSwift]: https://github.com/ReactiveX/RxSwift
-[GRDBCombine]: http://github.com/groue/GRDBCombine
-[Combine]: https://developer.apple.com/documentation/combine
[SQLite.swift]: http://github.com/stephencelis/SQLite.swift
[StORM]: https://www.perfect.org/docs/StORM.html
[Swift-Kuery]: http://github.com/IBM-Swift/Swift-Kuery
diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec
index 7645dd7545..f1507899db 100644
--- a/GRDB.swift.podspec
+++ b/GRDB.swift.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'GRDB.swift'
- s.version = '5.0.0-beta.5'
+ s.version = '5.0.0-beta.6'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.summary = 'A toolkit for SQLite databases, with a focus on application development.'
diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj
index fddc1c8fb4..b54da4114e 100755
--- a/GRDB.xcodeproj/project.pbxproj
+++ b/GRDB.xcodeproj/project.pbxproj
@@ -166,6 +166,25 @@
563B071621862C4700B38F35 /* ValueObservationRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B071421862C4600B38F35 /* ValueObservationRecordTests.swift */; };
563B071821862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B071721862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift */; };
563B071921862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B071721862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift */; };
+ 563B8F92249E6171007A48C9 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8F91249E6171007A48C9 /* Trace.swift */; };
+ 563B8F93249E6171007A48C9 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8F91249E6171007A48C9 /* Trace.swift */; };
+ 563B8F94249E6171007A48C9 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8F91249E6171007A48C9 /* Trace.swift */; };
+ 563B8F95249E6171007A48C9 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8F91249E6171007A48C9 /* Trace.swift */; };
+ 563B8FA1249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FA0249E8ACB007A48C9 /* ValueObservationPrintTests.swift */; };
+ 563B8FA2249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FA0249E8ACB007A48C9 /* ValueObservationPrintTests.swift */; };
+ 563B8FA3249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FA0249E8ACB007A48C9 /* ValueObservationPrintTests.swift */; };
+ 563B8FAC24A1CE43007A48C9 /* DatabasePublishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FAB24A1CE43007A48C9 /* DatabasePublishers.swift */; };
+ 563B8FAD24A1CE44007A48C9 /* DatabasePublishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FAB24A1CE43007A48C9 /* DatabasePublishers.swift */; };
+ 563B8FAE24A1CE44007A48C9 /* DatabasePublishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FAB24A1CE43007A48C9 /* DatabasePublishers.swift */; };
+ 563B8FAF24A1CE44007A48C9 /* DatabasePublishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FAB24A1CE43007A48C9 /* DatabasePublishers.swift */; };
+ 563B8FB524A1D029007A48C9 /* ReceiveValuesOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */; };
+ 563B8FB624A1D029007A48C9 /* ReceiveValuesOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */; };
+ 563B8FB724A1D029007A48C9 /* ReceiveValuesOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */; };
+ 563B8FB824A1D029007A48C9 /* ReceiveValuesOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */; };
+ 563B8FC524A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */; };
+ 563B8FC624A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */; };
+ 563B8FC724A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */; };
+ 563B8FC824A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */; };
563C67B324628BEA00E94EDC /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563C67B224628BEA00E94EDC /* DatabasePoolTests.swift */; };
563C67B424628BEA00E94EDC /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563C67B224628BEA00E94EDC /* DatabasePoolTests.swift */; };
563C67B524628BEA00E94EDC /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563C67B224628BEA00E94EDC /* DatabasePoolTests.swift */; };
@@ -185,6 +204,51 @@
563EF45321631E21007DAACD /* QueryInterfacePromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF45221631E21007DAACD /* QueryInterfacePromiseTests.swift */; };
563EF45421631E21007DAACD /* QueryInterfacePromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF45221631E21007DAACD /* QueryInterfacePromiseTests.swift */; };
563EF45D2163309F007DAACD /* Inflections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF4492161F179007DAACD /* Inflections.swift */; };
+ 56419C5124A51998004967E1 /* Finished.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6124A51601004967E1 /* Finished.swift */; };
+ 56419C5224A51998004967E1 /* NextOne.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6224A51601004967E1 /* NextOne.swift */; };
+ 56419C5324A51998004967E1 /* Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6324A51601004967E1 /* Next.swift */; };
+ 56419C5424A51998004967E1 /* Recording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6424A51601004967E1 /* Recording.swift */; };
+ 56419C5524A51998004967E1 /* Prefix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6524A51601004967E1 /* Prefix.swift */; };
+ 56419C5624A51998004967E1 /* Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6624A51601004967E1 /* Map.swift */; };
+ 56419C5724A51998004967E1 /* Inverted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6724A51601004967E1 /* Inverted.swift */; };
+ 56419C5824A51998004967E1 /* PublisherExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6824A51601004967E1 /* PublisherExpectation.swift */; };
+ 56419C5924A51999004967E1 /* Finished.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6124A51601004967E1 /* Finished.swift */; };
+ 56419C5A24A51999004967E1 /* NextOne.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6224A51601004967E1 /* NextOne.swift */; };
+ 56419C5B24A51999004967E1 /* Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6324A51601004967E1 /* Next.swift */; };
+ 56419C5C24A51999004967E1 /* Recording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6424A51601004967E1 /* Recording.swift */; };
+ 56419C5D24A51999004967E1 /* Prefix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6524A51601004967E1 /* Prefix.swift */; };
+ 56419C5E24A51999004967E1 /* Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6624A51601004967E1 /* Map.swift */; };
+ 56419C5F24A51999004967E1 /* Inverted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6724A51601004967E1 /* Inverted.swift */; };
+ 56419C6024A51999004967E1 /* PublisherExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6824A51601004967E1 /* PublisherExpectation.swift */; };
+ 56419C6124A5199B004967E1 /* Finished.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6124A51601004967E1 /* Finished.swift */; };
+ 56419C6224A5199B004967E1 /* NextOne.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6224A51601004967E1 /* NextOne.swift */; };
+ 56419C6324A5199B004967E1 /* Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6324A51601004967E1 /* Next.swift */; };
+ 56419C6424A5199B004967E1 /* Recording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6424A51601004967E1 /* Recording.swift */; };
+ 56419C6524A5199B004967E1 /* Prefix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6524A51601004967E1 /* Prefix.swift */; };
+ 56419C6624A5199B004967E1 /* Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6624A51601004967E1 /* Map.swift */; };
+ 56419C6724A5199B004967E1 /* Inverted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6724A51601004967E1 /* Inverted.swift */; };
+ 56419C6824A5199B004967E1 /* PublisherExpectation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6824A51601004967E1 /* PublisherExpectation.swift */; };
+ 56419C6924A519A2004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6A24A51601004967E1 /* DatabaseWriterWritePublisherTests.swift */; };
+ 56419C6A24A519A2004967E1 /* DatabaseReaderReadPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6B24A51601004967E1 /* DatabaseReaderReadPublisherTests.swift */; };
+ 56419C6B24A519A2004967E1 /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6C24A51601004967E1 /* Support.swift */; };
+ 56419C6C24A519A2004967E1 /* DatabaseRegionObservationPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6D24A51601004967E1 /* DatabaseRegionObservationPublisherTests.swift */; };
+ 56419C6D24A519A2004967E1 /* ValueObservationPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6E24A51601004967E1 /* ValueObservationPublisherTests.swift */; };
+ 56419C6E24A519A3004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6A24A51601004967E1 /* DatabaseWriterWritePublisherTests.swift */; };
+ 56419C6F24A519A3004967E1 /* DatabaseReaderReadPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6B24A51601004967E1 /* DatabaseReaderReadPublisherTests.swift */; };
+ 56419C7024A519A3004967E1 /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6C24A51601004967E1 /* Support.swift */; };
+ 56419C7124A519A3004967E1 /* DatabaseRegionObservationPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6D24A51601004967E1 /* DatabaseRegionObservationPublisherTests.swift */; };
+ 56419C7224A519A3004967E1 /* ValueObservationPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6E24A51601004967E1 /* ValueObservationPublisherTests.swift */; };
+ 56419C7324A519A4004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6A24A51601004967E1 /* DatabaseWriterWritePublisherTests.swift */; };
+ 56419C7424A519A4004967E1 /* DatabaseReaderReadPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6B24A51601004967E1 /* DatabaseReaderReadPublisherTests.swift */; };
+ 56419C7524A519A4004967E1 /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6C24A51601004967E1 /* Support.swift */; };
+ 56419C7624A519A4004967E1 /* DatabaseRegionObservationPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6D24A51601004967E1 /* DatabaseRegionObservationPublisherTests.swift */; };
+ 56419C7724A519A4004967E1 /* ValueObservationPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A6E24A51601004967E1 /* ValueObservationPublisherTests.swift */; };
+ 56419C7824A51A38004967E1 /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A5E24A51601004967E1 /* Recorder.swift */; };
+ 56419C7924A51A38004967E1 /* RecordingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A5F24A51601004967E1 /* RecordingError.swift */; };
+ 56419C7A24A51A39004967E1 /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A5E24A51601004967E1 /* Recorder.swift */; };
+ 56419C7B24A51A39004967E1 /* RecordingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A5F24A51601004967E1 /* RecordingError.swift */; };
+ 56419C7C24A51A3A004967E1 /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A5E24A51601004967E1 /* Recorder.swift */; };
+ 56419C7D24A51A3A004967E1 /* RecordingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419A5F24A51601004967E1 /* RecordingError.swift */; };
564390D52414FC2C00BA61E6 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AAA4DCFE230F1E0600C74B15 /* GRDB.framework */; };
564448831EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; };
564448871EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; };
@@ -364,9 +428,9 @@
56677C0D241CD0D00050755D /* ValueObservationRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C0C241CD0D00050755D /* ValueObservationRecorder.swift */; };
56677C0E241CD0D00050755D /* ValueObservationRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C0C241CD0D00050755D /* ValueObservationRecorder.swift */; };
56677C0F241CD0D00050755D /* ValueObservationRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C0C241CD0D00050755D /* ValueObservationRecorder.swift */; };
- 56677C15241D14450050755D /* FailableTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C14241D14450050755D /* FailableTestCase.swift */; };
- 56677C16241D14450050755D /* FailableTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C14241D14450050755D /* FailableTestCase.swift */; };
- 56677C17241D14450050755D /* FailableTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C14241D14450050755D /* FailableTestCase.swift */; };
+ 56677C15241D14450050755D /* FailureTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C14241D14450050755D /* FailureTestCase.swift */; };
+ 56677C16241D14450050755D /* FailureTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C14241D14450050755D /* FailureTestCase.swift */; };
+ 56677C17241D14450050755D /* FailureTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C14241D14450050755D /* FailureTestCase.swift */; };
56677C19241D217F0050755D /* ValueObservationRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C18241D217F0050755D /* ValueObservationRecorderTests.swift */; };
56677C1A241D217F0050755D /* ValueObservationRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C18241D217F0050755D /* ValueObservationRecorderTests.swift */; };
56677C1B241D217F0050755D /* ValueObservationRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56677C18241D217F0050755D /* ValueObservationRecorderTests.swift */; };
@@ -1245,6 +1309,11 @@
563B0704218627F700B38F35 /* ValueObservationRowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationRowTests.swift; sourceTree = ""; };
563B071421862C4600B38F35 /* ValueObservationRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationRecordTests.swift; sourceTree = ""; };
563B071721862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationDatabaseValueConvertibleTests.swift; sourceTree = ""; };
+ 563B8F91249E6171007A48C9 /* Trace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trace.swift; sourceTree = ""; };
+ 563B8FA0249E8ACB007A48C9 /* ValueObservationPrintTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationPrintTests.swift; sourceTree = ""; };
+ 563B8FAB24A1CE43007A48C9 /* DatabasePublishers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabasePublishers.swift; sourceTree = ""; };
+ 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveValuesOn.swift; sourceTree = ""; };
+ 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnDemandFuture.swift; sourceTree = ""; };
563C67B224628BEA00E94EDC /* DatabasePoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolTests.swift; sourceTree = ""; };
563DE4EC231A91E2005081B7 /* DatabaseConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseConfigurationTests.swift; sourceTree = ""; };
563EF414215F87EB007DAACD /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; };
@@ -1252,6 +1321,21 @@
563EF43E216131D1007DAACD /* AssociationAggregateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociationAggregateTests.swift; sourceTree = ""; };
563EF4492161F179007DAACD /* Inflections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inflections.swift; sourceTree = ""; };
563EF45221631E21007DAACD /* QueryInterfacePromiseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryInterfacePromiseTests.swift; sourceTree = ""; };
+ 56419A5E24A51601004967E1 /* Recorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = ""; };
+ 56419A5F24A51601004967E1 /* RecordingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingError.swift; sourceTree = ""; };
+ 56419A6124A51601004967E1 /* Finished.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Finished.swift; sourceTree = ""; };
+ 56419A6224A51601004967E1 /* NextOne.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextOne.swift; sourceTree = ""; };
+ 56419A6324A51601004967E1 /* Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Next.swift; sourceTree = ""; };
+ 56419A6424A51601004967E1 /* Recording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recording.swift; sourceTree = ""; };
+ 56419A6524A51601004967E1 /* Prefix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prefix.swift; sourceTree = ""; };
+ 56419A6624A51601004967E1 /* Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Map.swift; sourceTree = ""; };
+ 56419A6724A51601004967E1 /* Inverted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inverted.swift; sourceTree = ""; };
+ 56419A6824A51601004967E1 /* PublisherExpectation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherExpectation.swift; sourceTree = ""; };
+ 56419A6A24A51601004967E1 /* DatabaseWriterWritePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseWriterWritePublisherTests.swift; sourceTree = ""; };
+ 56419A6B24A51601004967E1 /* DatabaseReaderReadPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseReaderReadPublisherTests.swift; sourceTree = ""; };
+ 56419A6C24A51601004967E1 /* Support.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Support.swift; sourceTree = ""; };
+ 56419A6D24A51601004967E1 /* DatabaseRegionObservationPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRegionObservationPublisherTests.swift; sourceTree = ""; };
+ 56419A6E24A51601004967E1 /* ValueObservationPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueObservationPublisherTests.swift; sourceTree = ""; };
564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = ""; };
5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = ""; };
564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = ""; };
@@ -1317,7 +1401,7 @@
5665FA132129C9D6004D8612 /* DatabaseDateDecodingStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseDateDecodingStrategyTests.swift; sourceTree = ""; };
5665FA322129EEA0004D8612 /* DatabaseDateEncodingStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseDateEncodingStrategyTests.swift; sourceTree = ""; };
56677C0C241CD0D00050755D /* ValueObservationRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueObservationRecorder.swift; sourceTree = ""; };
- 56677C14241D14450050755D /* FailableTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailableTestCase.swift; sourceTree = ""; };
+ 56677C14241D14450050755D /* FailureTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailureTestCase.swift; sourceTree = ""; };
56677C18241D217F0050755D /* ValueObservationRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueObservationRecorderTests.swift; sourceTree = ""; };
566A84192041146100E50BFD /* DatabaseSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSnapshot.swift; sourceTree = ""; };
566A8424204120B700E50BFD /* DatabaseSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSnapshotTests.swift; sourceTree = ""; };
@@ -1691,7 +1775,7 @@
children = (
562EA81E1F17B26F00FA528C /* Compilation */,
56A238111B9C74A90082EB20 /* Core */,
- 56677C14241D14450050755D /* FailableTestCase.swift */,
+ 56677C14241D14450050755D /* FailureTestCase.swift */,
5698AC3E1DA2BEBB0056AF8C /* FTS */,
56176CA01EACEE2A000F3F2B /* GRDBCipher */,
5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */,
@@ -1815,6 +1899,7 @@
563B071721862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift */,
563B06C92185D2E500B38F35 /* ValueObservationFetchTests.swift */,
564CE4E821B2E06F00652B19 /* ValueObservationMapTests.swift */,
+ 563B8FA0249E8ACB007A48C9 /* ValueObservationPrintTests.swift */,
5656A7F822946B33001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift */,
563B06C22185D29F00B38F35 /* ValueObservationReadonlyTests.swift */,
56677C0C241CD0D00050755D /* ValueObservationRecorder.swift */,
@@ -1827,6 +1912,43 @@
name = ValueObservation;
sourceTree = "";
};
+ 56419A5D24A51601004967E1 /* CombineExpectations */ = {
+ isa = PBXGroup;
+ children = (
+ 56419A5E24A51601004967E1 /* Recorder.swift */,
+ 56419A5F24A51601004967E1 /* RecordingError.swift */,
+ 56419A6024A51601004967E1 /* PublisherExpectations */,
+ 56419A6824A51601004967E1 /* PublisherExpectation.swift */,
+ );
+ path = CombineExpectations;
+ sourceTree = "";
+ };
+ 56419A6024A51601004967E1 /* PublisherExpectations */ = {
+ isa = PBXGroup;
+ children = (
+ 56419A6124A51601004967E1 /* Finished.swift */,
+ 56419A6224A51601004967E1 /* NextOne.swift */,
+ 56419A6324A51601004967E1 /* Next.swift */,
+ 56419A6424A51601004967E1 /* Recording.swift */,
+ 56419A6524A51601004967E1 /* Prefix.swift */,
+ 56419A6624A51601004967E1 /* Map.swift */,
+ 56419A6724A51601004967E1 /* Inverted.swift */,
+ );
+ path = PublisherExpectations;
+ sourceTree = "";
+ };
+ 56419A6924A51601004967E1 /* GRDBCombineTests */ = {
+ isa = PBXGroup;
+ children = (
+ 56419A6A24A51601004967E1 /* DatabaseWriterWritePublisherTests.swift */,
+ 56419A6B24A51601004967E1 /* DatabaseReaderReadPublisherTests.swift */,
+ 56419A6C24A51601004967E1 /* Support.swift */,
+ 56419A6D24A51601004967E1 /* DatabaseRegionObservationPublisherTests.swift */,
+ 56419A6E24A51601004967E1 /* ValueObservationPublisherTests.swift */,
+ );
+ path = GRDBCombineTests;
+ sourceTree = "";
+ };
564390D42414FC2C00BA61E6 /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -1945,9 +2067,11 @@
563EF4492161F179007DAACD /* Inflections.swift */,
569BBA482291707D00478429 /* Inflections+English.swift */,
566BE7172342542F00A8254B /* LockedBox.swift */,
+ 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */,
563EF414215F87EB007DAACD /* OrderedDictionary.swift */,
5659F4971EA8D989004A4992 /* Pool.swift */,
5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */,
+ 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */,
56781B0A243F86E600650A83 /* Refinable.swift */,
5659F4871EA8D94E004A4992 /* Utils.swift */,
);
@@ -2150,6 +2274,7 @@
56A238731B9C75030082EB20 /* DatabaseError.swift */,
564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */,
560A37A31C8F625000949E71 /* DatabasePool.swift */,
+ 563B8FAB24A1CE43007A48C9 /* DatabasePublishers.swift */,
56A238741B9C75030082EB20 /* DatabaseQueue.swift */,
563363BF1C942C04000BE133 /* DatabaseReader.swift */,
569EF0E1200D2D8400A9FA45 /* DatabaseRegion.swift */,
@@ -2172,8 +2297,8 @@
56A238781B9C75030082EB20 /* Statement.swift */,
566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */,
560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */,
- 5605F1471C672E4000235C62 /* Support */,
566B91321FA4D3810012D5B0 /* TransactionObserver.swift */,
+ 5605F1471C672E4000235C62 /* Support */,
);
path = Core;
sourceTree = "";
@@ -2209,6 +2334,7 @@
56AACAA722ACED7100A40F2A /* Fetch.swift */,
5613ED3421A95A5C00DC7A68 /* Map.swift */,
564CE59621B7A8B500652B19 /* RemoveDuplicates.swift */,
+ 563B8F91249E6171007A48C9 /* Trace.swift */,
5613ED4321A95B2C00DC7A68 /* ValueReducer.swift */,
);
path = ValueReducer;
@@ -2265,8 +2391,10 @@
DC10500F19C904DD00D8CA30 /* Tests */ = {
isa = PBXGroup;
children = (
- 56176C581EACC2D8000F3F2B /* GRDBTests */,
+ 56419A5D24A51601004967E1 /* CombineExpectations */,
569530FA1C9067CC00CF1A2B /* Crash */,
+ 56419A6924A51601004967E1 /* GRDBCombineTests */,
+ 56176C581EACC2D8000F3F2B /* GRDBTests */,
DC37740319C8CBB3004FCF85 /* Supporting Files */,
);
path = Tests;
@@ -2540,7 +2668,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0730;
- LastUpgradeCheck = 1020;
+ LastUpgradeCheck = 1200;
ORGANIZATIONNAME = "Gwendal Roué";
TargetAttributes = {
5654909F1D5A4798005622CB = {
@@ -2755,6 +2883,7 @@
5659F48E1EA8D94E004A4992 /* Utils.swift in Sources */,
564CE43321AA901800652B19 /* ValueObserver.swift in Sources */,
56CEB5671EAA359A00BFAF62 /* SQLSelectable.swift in Sources */,
+ 563B8FC724A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */,
5653EB272094A14400F46237 /* QueryInterfaceRequest+Association.swift in Sources */,
565490B91D5AE236005622CB /* DatabasePool.swift in Sources */,
563B06AD217EF0CC00B38F35 /* ValueObservation.swift in Sources */,
@@ -2793,6 +2922,7 @@
5659F49E1EA8D989004A4992 /* Pool.swift in Sources */,
5653EB0E20944C7C00F46237 /* HasManyAssociation.swift in Sources */,
565490DA1D5AE252005622CB /* QueryInterfaceRequest.swift in Sources */,
+ 563B8FAE24A1CE44007A48C9 /* DatabasePublishers.swift in Sources */,
565490CA1D5AE252005622CB /* DatabaseDateComponents.swift in Sources */,
565490D01D5AE252005622CB /* NSString.swift in Sources */,
565490DF1D5AE252005622CB /* TableDefinition.swift in Sources */,
@@ -2809,6 +2939,7 @@
5671FC261DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */,
5653EC142098738B00F46237 /* SQLGenerationContext.swift in Sources */,
56CEB5521EAA359A00BFAF62 /* SQLExpressible.swift in Sources */,
+ 563B8F94249E6171007A48C9 /* Trace.swift in Sources */,
568D13212207213F00674B58 /* SQLQueryGenerator.swift in Sources */,
565490DB1D5AE252005622CB /* SQLCollatedExpression.swift in Sources */,
5674A7071F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift in Sources */,
@@ -2824,6 +2955,7 @@
565490E11D5AE252005622CB /* PersistableRecord.swift in Sources */,
56FC987E1D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */,
569BBA50229170FA00478429 /* Inflections+English.swift in Sources */,
+ 563B8FB724A1D029007A48C9 /* ReceiveValuesOn.swift in Sources */,
565490E01D5AE252005622CB /* FetchedRecordsController.swift in Sources */,
566B91391FA4D3810012D5B0 /* TransactionObserver.swift in Sources */,
565490C81D5AE252005622CB /* CGFloat.swift in Sources */,
@@ -2878,6 +3010,7 @@
5636E9BF1D22574100B9B05F /* FetchRequest.swift in Sources */,
563EF42E2161180D007DAACD /* AssociationAggregate.swift in Sources */,
56BB6EAC1D3009B100A1CA52 /* SchedulingWatchdog.swift in Sources */,
+ 563B8FC624A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */,
56300B791C53F592005A543B /* QueryInterfaceRequest.swift in Sources */,
5656A8B12295BFD7001FF3FF /* TableRecord+QueryInterfaceRequest.swift in Sources */,
56CEB5561EAA359A00BFAF62 /* SQLExpression.swift in Sources */,
@@ -2911,6 +3044,7 @@
5698AD1B1DAAD17D0056AF8C /* FTS5Tokenizer.swift in Sources */,
5653EB0420944C7C00F46237 /* BelongsToAssociation.swift in Sources */,
56B964B41DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */,
+ 563B8FB624A1D029007A48C9 /* ReceiveValuesOn.swift in Sources */,
560A37A81C8FF6E500949E71 /* SerializedDatabase.swift in Sources */,
566A841B2041146100E50BFD /* DatabaseSnapshot.swift in Sources */,
56CEB54F1EAA359A00BFAF62 /* SQLExpressible.swift in Sources */,
@@ -2930,6 +3064,7 @@
569BBA4F229170F900478429 /* Inflections+English.swift in Sources */,
5656A81F2295B12F001FF3FF /* SQLAssociation.swift in Sources */,
5617294F223533F40006E219 /* EncodableRecord.swift in Sources */,
+ 563B8FAD24A1CE44007A48C9 /* DatabasePublishers.swift in Sources */,
5653EC132098738B00F46237 /* SQLGenerationContext.swift in Sources */,
5657AB121D10899D006283EF /* URL.swift in Sources */,
5616AAF2207CD45E00AC3664 /* RequestProtocols.swift in Sources */,
@@ -2976,6 +3111,7 @@
5605F1681C672E4000235C62 /* NSNumber.swift in Sources */,
564CE5AD21B8FAB400652B19 /* DatabaseRegionObservation.swift in Sources */,
56CEB5041EAA2F4D00BFAF62 /* FTS4.swift in Sources */,
+ 563B8F93249E6171007A48C9 /* Trace.swift in Sources */,
5605F1741C672E4000235C62 /* StandardLibrary.swift in Sources */,
56E9FACC221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift in Sources */,
560D92481C672C4B00F4F92B /* PersistableRecord.swift in Sources */,
@@ -3007,6 +3143,7 @@
files = (
563B06C82185D29F00B38F35 /* ValueObservationReadonlyTests.swift in Sources */,
6340BF841E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift in Sources */,
+ 56419C5E24A51999004967E1 /* Map.swift in Sources */,
56300B861C54DC95005A543B /* Record+QueryInterfaceRequestTests.swift in Sources */,
56F26C1C1CEE3F32007969C4 /* RowAdapterTests.swift in Sources */,
56DAA2D61DE99DAB006E10C8 /* DatabaseCursorTests.swift in Sources */,
@@ -3034,6 +3171,7 @@
5657AB4A1D108BA9006283EF /* FoundationNSNullTests.swift in Sources */,
569531351C919DF200CF1A2B /* DatabasePoolCollationTests.swift in Sources */,
56A2385A1B9C74A90082EB20 /* RecordPrimaryKeySingleTests.swift in Sources */,
+ 56419C7224A519A3004967E1 /* ValueObservationPublisherTests.swift in Sources */,
562756471E963AAC0035B653 /* DatabaseWriterTests.swift in Sources */,
569531381C919DF700CF1A2B /* DatabasePoolFunctionTests.swift in Sources */,
563B06F821861D8400B38F35 /* ValueObservationCountTests.swift in Sources */,
@@ -3061,6 +3199,7 @@
56E4F7EF2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */,
561CFA992376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */,
5698AC4D1DA2D48A0056AF8C /* FTS3RecordTests.swift in Sources */,
+ 56419C6E24A519A3004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */,
56057C632291C7C600A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */,
56B15D0B1CD4C35100A24C8B /* FetchedRecordsControllerTests.swift in Sources */,
56A238681B9C74A90082EB20 /* RecordInitializersTests.swift in Sources */,
@@ -3075,18 +3214,21 @@
5653EADD20944B4F00F46237 /* AssociationBelongsToSQLTests.swift in Sources */,
56176C6B1EACCCC9000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */,
56300B621C53C42C005A543B /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */,
+ 56419C5D24A51999004967E1 /* Prefix.swift in Sources */,
56EA869F1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift in Sources */,
- 56677C16241D14450050755D /* FailableTestCase.swift in Sources */,
+ 56677C16241D14450050755D /* FailureTestCase.swift in Sources */,
5615B288222B17C000061C1C /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */,
567F45AC1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
5682D722239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */,
5657AB621D108BA9006283EF /* FoundationNSURLTests.swift in Sources */,
5657AB6A1D108BA9006283EF /* FoundationURLTests.swift in Sources */,
+ 56419C5A24A51999004967E1 /* NextOne.swift in Sources */,
564FCE5F20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */,
5653EADF20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */,
5695961D222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
56F3E74D1E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */,
56EB0AB31BCD787300A3DC55 /* DataMemoryTests.swift in Sources */,
+ 56419C5B24A51999004967E1 /* Next.swift in Sources */,
56B6EF57208CB4E3002F0ACB /* ColumnExpressionTests.swift in Sources */,
563B071621862C4700B38F35 /* ValueObservationRecordTests.swift in Sources */,
56176C7F1EACCD2F000F3F2B /* EncryptionTests.swift in Sources */,
@@ -3095,6 +3237,7 @@
56A2385E1B9C74A90082EB20 /* RecordCopyTests.swift in Sources */,
56B86E7A220FF4E000524C16 /* SQLLiteralTests.swift in Sources */,
56176C6C1EACCCC9000F3F2B /* FTS5PatternTests.swift in Sources */,
+ 56419C7024A519A3004967E1 /* Support.swift in Sources */,
563363B21C933FF8000BE133 /* PersistableRecordTests.swift in Sources */,
5615B26B222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift in Sources */,
561CFA9D2376EC86000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */,
@@ -3105,6 +3248,7 @@
563B071921862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift in Sources */,
5665F85E203EE6030084C6C0 /* ColumnInfoTests.swift in Sources */,
56A238441B9C74A90082EB20 /* RawRepresentable+DatabaseValueConvertibleTests.swift in Sources */,
+ 56419C7B24A51A39004967E1 /* RecordingError.swift in Sources */,
560A37AC1C90085D00949E71 /* DatabasePoolConcurrencyTests.swift in Sources */,
5607EFD41BB827FD00605DE3 /* TransactionObserverTests.swift in Sources */,
5690C33B1D23E7D200E59934 /* FoundationDateTests.swift in Sources */,
@@ -3115,12 +3259,14 @@
563B06CB2185D2E500B38F35 /* ValueObservationFetchTests.swift in Sources */,
5623935B1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */,
5674A72D1F30A9090095F066 /* FetchableRecordDecodableTests.swift in Sources */,
+ 56419C5F24A51999004967E1 /* Inverted.swift in Sources */,
560432A4228F1668009D3FE2 /* AssociationPrefetchingObservationTests.swift in Sources */,
566A84412041914000E50BFD /* MutablePersistableRecordChangesTests.swift in Sources */,
56176C701EACCCC9000F3F2B /* FTS5WrapperTokenizerTests.swift in Sources */,
56FEE7FF1F47253700D930EA /* TableRecordTests.swift in Sources */,
56057C562291B16A00A7CB10 /* AssociationHasManyRowScopeTests.swift in Sources */,
56FEB8F9248403010081AF83 /* DatabaseTraceTests.swift in Sources */,
+ 56419C5924A51999004967E1 /* Finished.swift in Sources */,
56A2386A1B9C74A90082EB20 /* RecordSubClassTests.swift in Sources */,
56A2383E1B9C74A90082EB20 /* DatabaseValueTests.swift in Sources */,
567156181CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift in Sources */,
@@ -3137,8 +3283,10 @@
56A4CDB41D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift in Sources */,
56CC9247201E058100CB597E /* DropFirstCursorTests.swift in Sources */,
56915783231BF28B00E1D237 /* PoolTests.swift in Sources */,
+ 56419C7124A519A3004967E1 /* DatabaseRegionObservationPublisherTests.swift in Sources */,
56E8CE0E1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift in Sources */,
5665FA342129EEA0004D8612 /* DatabaseDateEncodingStrategyTests.swift in Sources */,
+ 563B8FA2249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */,
56A238581B9C74A90082EB20 /* RecordPrimaryKeyRowIDTests.swift in Sources */,
56A5EF131EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */,
5653EAE920944B4F00F46237 /* AssociationChainSQLTests.swift in Sources */,
@@ -3147,8 +3295,10 @@
56A238621B9C74A90082EB20 /* RecordEditedTests.swift in Sources */,
5653EAE120944B4F00F46237 /* AssociationTableAliasTestsSQLTests.swift in Sources */,
5653EAE720944B4F00F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */,
+ 56419C6F24A519A3004967E1 /* DatabaseReaderReadPublisherTests.swift in Sources */,
563EF440216131D1007DAACD /* AssociationAggregateTests.swift in Sources */,
5695312A1C908A5B00CF1A2B /* DatabasePoolSchemaCacheTests.swift in Sources */,
+ 56419C6024A51999004967E1 /* PublisherExpectation.swift in Sources */,
5653EADB20944B4F00F46237 /* AssociationRowScopeSearchTests.swift in Sources */,
56677C0E241CD0D00050755D /* ValueObservationRecorder.swift in Sources */,
5698ACA21DA4B0430056AF8C /* FTS4TableBuilderTests.swift in Sources */,
@@ -3199,9 +3349,11 @@
5653EAF520944B4F00F46237 /* AssociationParallelSQLTests.swift in Sources */,
563B06BE2185CCD300B38F35 /* ValueObservationTests.swift in Sources */,
56FF455A1D2CDA5200F21EF9 /* RecordUniqueIndexTests.swift in Sources */,
+ 56419C7A24A51A39004967E1 /* Recorder.swift in Sources */,
56B7F42A1BE14A1900E39BBF /* CGFloatTests.swift in Sources */,
5605F1881C69111300235C62 /* DatabaseRegionTests.swift in Sources */,
56176C6D1EACCCC9000F3F2B /* FTS5RecordTests.swift in Sources */,
+ 56419C5C24A51999004967E1 /* Recording.swift in Sources */,
569178491CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */,
562393761DEE104400A6B01F /* MapCursorTests.swift in Sources */,
566A8426204120B700E50BFD /* DatabaseSnapshotTests.swift in Sources */,
@@ -3215,6 +3367,7 @@
files = (
563B06C72185D29F00B38F35 /* ValueObservationReadonlyTests.swift in Sources */,
6340BF801E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift in Sources */,
+ 56419C5624A51998004967E1 /* Map.swift in Sources */,
56D496771D81309E008276D7 /* RecordInitializersTests.swift in Sources */,
56D496651D813076008276D7 /* DatabaseMigratorTests.swift in Sources */,
56D496591D81304E008276D7 /* FoundationNSDateTests.swift in Sources */,
@@ -3242,6 +3395,7 @@
56D496841D813147008276D7 /* SelectStatementTests.swift in Sources */,
56D496B11D8133BC008276D7 /* DatabaseQueueReadOnlyTests.swift in Sources */,
56D4968C1D81316E008276D7 /* RawRepresentable+DatabaseValueConvertibleTests.swift in Sources */,
+ 56419C6D24A519A2004967E1 /* ValueObservationPublisherTests.swift in Sources */,
56D496981D81317B008276D7 /* DatabaseValueTests.swift in Sources */,
56D4966B1D81309E008276D7 /* MutablePersistableRecordDeleteTests.swift in Sources */,
563B06F721861D8400B38F35 /* ValueObservationCountTests.swift in Sources */,
@@ -3269,6 +3423,7 @@
56E4F7EE2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */,
561CFA982376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */,
56D496881D81316E008276D7 /* DatabaseValueConversionTests.swift in Sources */,
+ 56419C6924A519A2004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */,
56057C622291C7C600A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */,
56D496BA1D813482008276D7 /* DatabaseQueueSchemaCacheTests.swift in Sources */,
56D496781D81309E008276D7 /* RecordSubClassTests.swift in Sources */,
@@ -3283,18 +3438,21 @@
56CC922C201DFFB900CB597E /* DropWhileCursorTests.swift in Sources */,
5653EADC20944B4F00F46237 /* AssociationBelongsToSQLTests.swift in Sources */,
56D5075E1F6BAE8600AE1C5B /* PrimaryKeyInfoTests.swift in Sources */,
+ 56419C5524A51998004967E1 /* Prefix.swift in Sources */,
56176C591EACCCC7000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */,
- 56677C15241D14450050755D /* FailableTestCase.swift in Sources */,
+ 56677C15241D14450050755D /* FailureTestCase.swift in Sources */,
5615B289222B17C000061C1C /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */,
56D4968A1D81316E008276D7 /* DatabaseValueConvertibleSubclassTests.swift in Sources */,
5682D721239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */,
56D496601D81304E008276D7 /* FoundationNSUUIDTests.swift in Sources */,
567F45A81F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
+ 56419C5224A51998004967E1 /* NextOne.swift in Sources */,
56D4968D1D81316E008276D7 /* DatabaseFunctionTests.swift in Sources */,
564FCE5E20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */,
5695961C222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
5653EADE20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */,
56D496661D813086008276D7 /* QueryInterfaceRequestTests.swift in Sources */,
+ 56419C5324A51998004967E1 /* Next.swift in Sources */,
56F3E7491E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */,
563B071521862C4700B38F35 /* ValueObservationRecordTests.swift in Sources */,
56B6EF56208CB4E3002F0ACB /* ColumnExpressionTests.swift in Sources */,
@@ -3303,6 +3461,7 @@
56741EA81E66A8B3003E422D /* FetchRequestTests.swift in Sources */,
56B86E79220FF4E000524C16 /* SQLLiteralTests.swift in Sources */,
56D496831D813147008276D7 /* DatabaseSavepointTests.swift in Sources */,
+ 56419C6B24A519A2004967E1 /* Support.swift in Sources */,
56D496871D81316E008276D7 /* DatabaseTimestampTests.swift in Sources */,
5615B26A222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift in Sources */,
561CFA9C2376EC86000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */,
@@ -3313,6 +3472,7 @@
563B071821862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift in Sources */,
5674A70F1F3087710095F066 /* DatabaseValueConvertibleEncodableTests.swift in Sources */,
56D496711D81309E008276D7 /* RecordPrimaryKeySingleTests.swift in Sources */,
+ 56419C7924A51A38004967E1 /* RecordingError.swift in Sources */,
5665F85D203EE6030084C6C0 /* ColumnInfoTests.swift in Sources */,
5698AC031D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */,
56D496611D81304E008276D7 /* Row+FoundationTests.swift in Sources */,
@@ -3323,12 +3483,14 @@
563B06CA2185D2E500B38F35 /* ValueObservationFetchTests.swift in Sources */,
56D496541D812F5B008276D7 /* SQLExpressionLiteralTests.swift in Sources */,
56D496961D81317B008276D7 /* PersistableRecordTests.swift in Sources */,
+ 56419C5724A51998004967E1 /* Inverted.swift in Sources */,
560432A3228F1668009D3FE2 /* AssociationPrefetchingObservationTests.swift in Sources */,
5698AC9E1DA4B0430056AF8C /* FTS4TableBuilderTests.swift in Sources */,
5674A72A1F30A9090095F066 /* FetchableRecordDecodableTests.swift in Sources */,
566A84402041914000E50BFD /* MutablePersistableRecordChangesTests.swift in Sources */,
56057C552291B16A00A7CB10 /* AssociationHasManyRowScopeTests.swift in Sources */,
56FEB8F8248403000081AF83 /* DatabaseTraceTests.swift in Sources */,
+ 56419C5124A51998004967E1 /* Finished.swift in Sources */,
56176C5E1EACCCC7000F3F2B /* FTS5WrapperTokenizerTests.swift in Sources */,
56FEE7FB1F47253700D930EA /* TableRecordTests.swift in Sources */,
56D496641D81304E008276D7 /* FoundationUUIDTests.swift in Sources */,
@@ -3345,8 +3507,10 @@
5653EAE420944B4F00F46237 /* AssociationParallelRowScopesTests.swift in Sources */,
56D4968F1D81316E008276D7 /* RowFromDictionaryTests.swift in Sources */,
56915782231BF28B00E1D237 /* PoolTests.swift in Sources */,
+ 56419C6C24A519A2004967E1 /* DatabaseRegionObservationPublisherTests.swift in Sources */,
56D4968E1D81316E008276D7 /* RowCopiedFromStatementTests.swift in Sources */,
5665FA332129EEA0004D8612 /* DatabaseDateEncodingStrategyTests.swift in Sources */,
+ 563B8FA1249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */,
56CC9246201E058100CB597E /* DropFirstCursorTests.swift in Sources */,
5698AC401DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */,
562393571DEE013C00A6B01F /* FilterCursorTests.swift in Sources */,
@@ -3355,8 +3519,10 @@
56A5EF0F1EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */,
56D496B61D813434008276D7 /* DatabaseRegionTests.swift in Sources */,
5653EAE020944B4F00F46237 /* AssociationTableAliasTestsSQLTests.swift in Sources */,
+ 56419C6A24A519A2004967E1 /* DatabaseReaderReadPublisherTests.swift in Sources */,
563EF43F216131D1007DAACD /* AssociationAggregateTests.swift in Sources */,
5653EAE620944B4F00F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */,
+ 56419C5824A51998004967E1 /* PublisherExpectation.swift in Sources */,
562393181DECC02000A6B01F /* RowFetchTests.swift in Sources */,
56677C0D241CD0D00050755D /* ValueObservationRecorder.swift in Sources */,
5653EADA20944B4F00F46237 /* AssociationRowScopeSearchTests.swift in Sources */,
@@ -3407,9 +3573,11 @@
5653EAF420944B4F00F46237 /* AssociationParallelSQLTests.swift in Sources */,
563B06BD2185CCD300B38F35 /* ValueObservationTests.swift in Sources */,
56D496971D81317B008276D7 /* DatabaseReaderTests.swift in Sources */,
+ 56419C7824A51A38004967E1 /* Recorder.swift in Sources */,
56D496911D81316E008276D7 /* RowFromStatementTests.swift in Sources */,
56071A411DB54ED000CA6E47 /* FetchedRecordsControllerTests.swift in Sources */,
56176C5B1EACCCC7000F3F2B /* FTS5RecordTests.swift in Sources */,
+ 56419C5424A51998004967E1 /* Recording.swift in Sources */,
56D496B91D81346F008276D7 /* DatabaseValueConvertibleEscapingTests.swift in Sources */,
56D496901D81316E008276D7 /* RowAdapterTests.swift in Sources */,
566A8425204120B700E50BFD /* DatabaseSnapshotTests.swift in Sources */,
@@ -3424,6 +3592,7 @@
AAA4DC77230F1E0600C74B15 /* FetchRequest.swift in Sources */,
AAA4DC78230F1E0600C74B15 /* AssociationAggregate.swift in Sources */,
AAA4DC79230F1E0600C74B15 /* SchedulingWatchdog.swift in Sources */,
+ 563B8FC824A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */,
AAA4DC7A230F1E0600C74B15 /* QueryInterfaceRequest.swift in Sources */,
AAA4DC7B230F1E0600C74B15 /* TableRecord+QueryInterfaceRequest.swift in Sources */,
AAA4DC7C230F1E0600C74B15 /* SQLExpression.swift in Sources */,
@@ -3457,6 +3626,7 @@
AAA4DC98230F1E0600C74B15 /* FTS5Tokenizer.swift in Sources */,
AAA4DC99230F1E0600C74B15 /* BelongsToAssociation.swift in Sources */,
AAA4DC9A230F1E0600C74B15 /* FTS5TokenizerDescriptor.swift in Sources */,
+ 563B8FB824A1D029007A48C9 /* ReceiveValuesOn.swift in Sources */,
AAA4DC9B230F1E0600C74B15 /* SerializedDatabase.swift in Sources */,
AAA4DC9C230F1E0600C74B15 /* DatabaseSnapshot.swift in Sources */,
AAA4DC9D230F1E0600C74B15 /* SQLExpressible.swift in Sources */,
@@ -3476,6 +3646,7 @@
AAA4DCA8230F1E0600C74B15 /* Inflections+English.swift in Sources */,
AAA4DCA9230F1E0600C74B15 /* SQLAssociation.swift in Sources */,
AAA4DCAA230F1E0600C74B15 /* EncodableRecord.swift in Sources */,
+ 563B8FAF24A1CE44007A48C9 /* DatabasePublishers.swift in Sources */,
AAA4DCAC230F1E0600C74B15 /* SQLGenerationContext.swift in Sources */,
AAA4DCAD230F1E0600C74B15 /* URL.swift in Sources */,
AAA4DCAE230F1E0600C74B15 /* RequestProtocols.swift in Sources */,
@@ -3522,6 +3693,7 @@
AAA4DCD9230F1E0600C74B15 /* NSNumber.swift in Sources */,
AAA4DCDB230F1E0600C74B15 /* DatabaseRegionObservation.swift in Sources */,
AAA4DCDC230F1E0600C74B15 /* FTS4.swift in Sources */,
+ 563B8F95249E6171007A48C9 /* Trace.swift in Sources */,
AAA4DCDD230F1E0600C74B15 /* StandardLibrary.swift in Sources */,
AAA4DCDE230F1E0600C74B15 /* SQLInterpolation+QueryInterface.swift in Sources */,
AAA4DCDF230F1E0600C74B15 /* PersistableRecord.swift in Sources */,
@@ -3553,6 +3725,7 @@
files = (
AAA4DD0B230F262000C74B15 /* ValueObservationReadonlyTests.swift in Sources */,
AAA4DD0C230F262000C74B15 /* RecordPersistenceConflictPolicy.swift in Sources */,
+ 56419C6624A5199B004967E1 /* Map.swift in Sources */,
AAA4DD0D230F262000C74B15 /* Record+QueryInterfaceRequestTests.swift in Sources */,
AAA4DD0E230F262000C74B15 /* RowAdapterTests.swift in Sources */,
AAA4DD0F230F262000C74B15 /* DatabaseCursorTests.swift in Sources */,
@@ -3580,6 +3753,7 @@
AAA4DD23230F262000C74B15 /* FoundationNSNullTests.swift in Sources */,
AAA4DD24230F262000C74B15 /* DatabasePoolCollationTests.swift in Sources */,
AAA4DD25230F262000C74B15 /* RecordPrimaryKeySingleTests.swift in Sources */,
+ 56419C7724A519A4004967E1 /* ValueObservationPublisherTests.swift in Sources */,
AAA4DD26230F262000C74B15 /* DatabaseWriterTests.swift in Sources */,
AAA4DD27230F262000C74B15 /* DatabasePoolFunctionTests.swift in Sources */,
AAA4DD28230F262000C74B15 /* ValueObservationCountTests.swift in Sources */,
@@ -3607,6 +3781,7 @@
56E4F7F02392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */,
561CFA9A2376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */,
AAA4DD3E230F262000C74B15 /* FTS3RecordTests.swift in Sources */,
+ 56419C7324A519A4004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */,
AAA4DD3F230F262000C74B15 /* AssociationHasManyThroughRowScopeTests.swift in Sources */,
AAA4DD40230F262000C74B15 /* FetchedRecordsControllerTests.swift in Sources */,
AAA4DD41230F262000C74B15 /* RecordInitializersTests.swift in Sources */,
@@ -3621,18 +3796,21 @@
AAA4DD4A230F262000C74B15 /* AssociationBelongsToSQLTests.swift in Sources */,
AAA4DD4B230F262000C74B15 /* FTS5CustomTokenizerTests.swift in Sources */,
AAA4DD4C230F262000C74B15 /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */,
+ 56419C6524A5199B004967E1 /* Prefix.swift in Sources */,
AAA4DD4D230F262000C74B15 /* DatabasePoolReadOnlyTests.swift in Sources */,
- 56677C17241D14450050755D /* FailableTestCase.swift in Sources */,
+ 56677C17241D14450050755D /* FailureTestCase.swift in Sources */,
AAA4DD4E230F262000C74B15 /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */,
AAA4DD4F230F262000C74B15 /* TruncateOptimizationTests.swift in Sources */,
5682D723239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */,
AAA4DD50230F262000C74B15 /* FoundationNSURLTests.swift in Sources */,
AAA4DD51230F262000C74B15 /* FoundationURLTests.swift in Sources */,
+ 56419C6224A5199B004967E1 /* NextOne.swift in Sources */,
AAA4DD53230F262000C74B15 /* DatabaseValueConversionErrorTests.swift in Sources */,
AAA4DD54230F262000C74B15 /* AssociationHasOneSQLDerivationTests.swift in Sources */,
AAA4DD55230F262000C74B15 /* AssociationHasManyThroughSQLTests.swift in Sources */,
AAA4DD56230F262000C74B15 /* ResultCodeTests.swift in Sources */,
AAA4DD57230F262000C74B15 /* DataMemoryTests.swift in Sources */,
+ 56419C6324A5199B004967E1 /* Next.swift in Sources */,
AAA4DD58230F262000C74B15 /* ColumnExpressionTests.swift in Sources */,
AAA4DD59230F262000C74B15 /* ValueObservationRecordTests.swift in Sources */,
AAA4DD5A230F262000C74B15 /* EncryptionTests.swift in Sources */,
@@ -3641,6 +3819,7 @@
AAA4DD5D230F262000C74B15 /* RecordCopyTests.swift in Sources */,
AAA4DD5E230F262000C74B15 /* SQLLiteralTests.swift in Sources */,
AAA4DD5F230F262000C74B15 /* FTS5PatternTests.swift in Sources */,
+ 56419C7524A519A4004967E1 /* Support.swift in Sources */,
AAA4DD60230F262000C74B15 /* PersistableRecordTests.swift in Sources */,
AAA4DD61230F262000C74B15 /* AssociationHasOneThroughRowScopeTests.swift in Sources */,
561CFA9E2376EC86000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */,
@@ -3651,6 +3830,7 @@
AAA4DD66230F262000C74B15 /* ValueObservationDatabaseValueConvertibleTests.swift in Sources */,
AAA4DD67230F262000C74B15 /* ColumnInfoTests.swift in Sources */,
AAA4DD68230F262000C74B15 /* RawRepresentable+DatabaseValueConvertibleTests.swift in Sources */,
+ 56419C7D24A51A3A004967E1 /* RecordingError.swift in Sources */,
AAA4DD69230F262000C74B15 /* DatabasePoolConcurrencyTests.swift in Sources */,
AAA4DD6A230F262000C74B15 /* TransactionObserverTests.swift in Sources */,
AAA4DD6B230F262000C74B15 /* FoundationDateTests.swift in Sources */,
@@ -3661,12 +3841,14 @@
AAA4DD70230F262000C74B15 /* ValueObservationFetchTests.swift in Sources */,
AAA4DD71230F262000C74B15 /* FilterCursorTests.swift in Sources */,
AAA4DD73230F262000C74B15 /* FetchableRecordDecodableTests.swift in Sources */,
+ 56419C6724A5199B004967E1 /* Inverted.swift in Sources */,
AAA4DD74230F262000C74B15 /* AssociationPrefetchingObservationTests.swift in Sources */,
AAA4DD75230F262000C74B15 /* MutablePersistableRecordChangesTests.swift in Sources */,
AAA4DD76230F262000C74B15 /* FTS5WrapperTokenizerTests.swift in Sources */,
AAA4DD77230F262000C74B15 /* TableRecordTests.swift in Sources */,
AAA4DD78230F262000C74B15 /* AssociationHasManyRowScopeTests.swift in Sources */,
56FEB8FA248403020081AF83 /* DatabaseTraceTests.swift in Sources */,
+ 56419C6124A5199B004967E1 /* Finished.swift in Sources */,
AAA4DD79230F262000C74B15 /* RecordSubClassTests.swift in Sources */,
AAA4DD7A230F262000C74B15 /* DatabaseValueTests.swift in Sources */,
AAA4DD7B230F262000C74B15 /* DatabaseQueueReadOnlyTests.swift in Sources */,
@@ -3683,8 +3865,10 @@
AAA4DD86230F262000C74B15 /* SQLExpressionLiteralTests.swift in Sources */,
AAA4DD87230F262000C74B15 /* DropFirstCursorTests.swift in Sources */,
56915784231BF28B00E1D237 /* PoolTests.swift in Sources */,
+ 56419C7624A519A4004967E1 /* DatabaseRegionObservationPublisherTests.swift in Sources */,
AAA4DD88230F262000C74B15 /* DatabaseValueConvertibleFetchTests.swift in Sources */,
AAA4DD89230F262000C74B15 /* DatabaseDateEncodingStrategyTests.swift in Sources */,
+ 563B8FA3249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */,
AAA4DD8A230F262000C74B15 /* RecordPrimaryKeyRowIDTests.swift in Sources */,
AAA4DD8B230F262000C74B15 /* ForeignKeyInfoTests.swift in Sources */,
AAA4DD8C230F262000C74B15 /* AssociationChainSQLTests.swift in Sources */,
@@ -3693,8 +3877,10 @@
AAA4DD8E230F262000C74B15 /* RecordEditedTests.swift in Sources */,
AAA4DD8F230F262000C74B15 /* AssociationTableAliasTestsSQLTests.swift in Sources */,
AAA4DD90230F262000C74B15 /* AssociationBelongsToFetchableRecordTests.swift in Sources */,
+ 56419C7424A519A4004967E1 /* DatabaseReaderReadPublisherTests.swift in Sources */,
AAA4DD91230F262000C74B15 /* AssociationAggregateTests.swift in Sources */,
AAA4DD92230F262000C74B15 /* DatabasePoolSchemaCacheTests.swift in Sources */,
+ 56419C6824A5199B004967E1 /* PublisherExpectation.swift in Sources */,
AAA4DD93230F262000C74B15 /* AssociationRowScopeSearchTests.swift in Sources */,
56677C0F241CD0D00050755D /* ValueObservationRecorder.swift in Sources */,
AAA4DD94230F262000C74B15 /* FTS4TableBuilderTests.swift in Sources */,
@@ -3745,9 +3931,11 @@
AAA4DDC2230F262000C74B15 /* AssociationParallelSQLTests.swift in Sources */,
AAA4DDC3230F262000C74B15 /* ValueObservationTests.swift in Sources */,
AAA4DDC4230F262000C74B15 /* RecordUniqueIndexTests.swift in Sources */,
+ 56419C7C24A51A3A004967E1 /* Recorder.swift in Sources */,
AAA4DDC5230F262000C74B15 /* CGFloatTests.swift in Sources */,
AAA4DDC6230F262000C74B15 /* DatabaseRegionTests.swift in Sources */,
AAA4DDC7230F262000C74B15 /* FTS5RecordTests.swift in Sources */,
+ 56419C6424A5199B004967E1 /* Recording.swift in Sources */,
AAA4DDC8230F262000C74B15 /* DatabaseQueueTests.swift in Sources */,
AAA4DDC9230F262000C74B15 /* MapCursorTests.swift in Sources */,
AAA4DDCA230F262000C74B15 /* DatabaseSnapshotTests.swift in Sources */,
@@ -3762,6 +3950,7 @@
563EF42D2161180D007DAACD /* AssociationAggregate.swift in Sources */,
56D91AB02205F8AC00770D8D /* SQLQuery.swift in Sources */,
5616AAF1207CD45E00AC3664 /* RequestProtocols.swift in Sources */,
+ 563B8FC524A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */,
5656A8B02295BFD7001FF3FF /* TableRecord+QueryInterfaceRequest.swift in Sources */,
5613ED4421A95B2C00DC7A68 /* ValueReducer.swift in Sources */,
5636E9BC1D22574100B9B05F /* FetchRequest.swift in Sources */,
@@ -3795,6 +3984,7 @@
566475CC1D981D5E00FF74B8 /* SQLFunctions.swift in Sources */,
5698AC371D9E5A590056AF8C /* FTS3Pattern.swift in Sources */,
563EF415215F87EB007DAACD /* OrderedDictionary.swift in Sources */,
+ 563B8FB524A1D029007A48C9 /* ReceiveValuesOn.swift in Sources */,
564F9C2D1F075DD200877A00 /* DatabaseFunction.swift in Sources */,
564CE5AC21B8FAB400652B19 /* DatabaseRegionObservation.swift in Sources */,
5659F4981EA8D989004A4992 /* Pool.swift in Sources */,
@@ -3814,6 +4004,7 @@
56B964B11DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */,
560A37A71C8FF6E500949E71 /* SerializedDatabase.swift in Sources */,
5653EB252094A14400F46237 /* QueryInterfaceRequest+Association.swift in Sources */,
+ 563B8FAC24A1CE43007A48C9 /* DatabasePublishers.swift in Sources */,
5605F1691C672E4000235C62 /* NSString.swift in Sources */,
560D92401C672C3E00F4F92B /* DatabaseValueConvertible.swift in Sources */,
56A8C2301D1914540096E9D4 /* UUID.swift in Sources */,
@@ -3860,6 +4051,7 @@
5674A6FB1F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */,
5653EB0620944C7C00F46237 /* SQLForeignKeyRequest.swift in Sources */,
56E9FACB221046FD00C703A8 /* SQLInterpolation+QueryInterface.swift in Sources */,
+ 563B8F92249E6171007A48C9 /* Trace.swift in Sources */,
5698AC781DA37DCB0056AF8C /* VirtualTableModule.swift in Sources */,
56A2387D1B9C75030082EB20 /* Database.swift in Sources */,
566AD8B21D5318F4002EC1A8 /* TableDefinition.swift in Sources */,
@@ -4254,6 +4446,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -4310,6 +4503,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
diff --git a/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBOSX.xcscheme b/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBOSX.xcscheme
index 21b4938e8b..6de72bd207 100644
--- a/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBOSX.xcscheme
+++ b/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBOSX.xcscheme
@@ -1,6 +1,6 @@
-
-
-
-
diff --git a/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBWatchOS.xcscheme b/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBWatchOS.xcscheme
index 0981524dd5..942d5488c8 100644
--- a/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBWatchOS.xcscheme
+++ b/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBWatchOS.xcscheme
@@ -1,6 +1,6 @@
-
-
-
-
diff --git a/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBtvOS.xcscheme b/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBtvOS.xcscheme
index aedc7dad18..dcf1ba7f76 100644
--- a/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBtvOS.xcscheme
+++ b/GRDB.xcodeproj/xcshareddata/xcschemes/GRDBtvOS.xcscheme
@@ -1,6 +1,6 @@
-
-
-
-
diff --git a/GRDB.xcworkspace/contents.xcworkspacedata b/GRDB.xcworkspace/contents.xcworkspacedata
index 4919b79c0f..244a3cb1c4 100644
--- a/GRDB.xcworkspace/contents.xcworkspacedata
+++ b/GRDB.xcworkspace/contents.xcworkspacedata
@@ -53,4 +53,7 @@
+
+
diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift
index 9896121cc0..8cace9c575 100644
--- a/GRDB/Core/DatabasePool.swift
+++ b/GRDB/Core/DatabasePool.swift
@@ -188,10 +188,6 @@ extension DatabasePool {
/// Listens to UIApplicationDidEnterBackgroundNotification and
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
/// as much memory as possible.
- ///
- /// - param application: The UIApplication that will start a background
- /// task to let the database pool release its memory when the application
- /// enters background.
private func setupMemoryManagement() {
let center = NotificationCenter.default
center.addObserver(
@@ -813,7 +809,6 @@ extension DatabasePool: DatabaseReader {
public func _add(
observation: ValueObservation,
scheduling scheduler: ValueObservationScheduler,
- onError: @escaping (Error) -> Void,
onChange: @escaping (Reducer.Value) -> Void)
-> DatabaseCancellable
{
@@ -821,7 +816,6 @@ extension DatabasePool: DatabaseReader {
return _addReadOnly(
observation: observation,
scheduling: scheduler,
- onError: onError,
onChange: onChange)
}
@@ -829,7 +823,6 @@ extension DatabasePool: DatabaseReader {
let observer = _addWriteOnly(
observation: observation,
scheduling: scheduler,
- onError: onError,
onChange: onChange)
return AnyDatabaseCancellable(cancel: observer.cancel)
}
@@ -837,7 +830,6 @@ extension DatabasePool: DatabaseReader {
let observer = _addConcurrent(
observation: observation,
scheduling: scheduler,
- onError: onError,
onChange: onChange)
return AnyDatabaseCancellable(cancel: observer.cancel)
}
@@ -847,7 +839,6 @@ extension DatabasePool: DatabaseReader {
private func _addConcurrent(
observation: ValueObservation,
scheduling scheduler: ValueObservationScheduler,
- onError: @escaping (Error) -> Void,
onChange: @escaping (Reducer.Value) -> Void)
-> ValueObserver // For testability
{
@@ -857,13 +848,11 @@ extension DatabasePool: DatabaseReader {
let reduceQueueLabel = configuration.identifier(
defaultLabel: "GRDB",
purpose: "ValueObservation")
- let observer = ValueObserver(
- requiresWriteAccess: observation.requiresWriteAccess,
+ let observer = ValueObserver(
+ observation: observation,
writer: self,
- reducer: observation.makeReducer(),
- scheduling: scheduler,
+ scheduler: scheduler,
reduceQueue: configuration.makeDispatchQueue(label: reduceQueueLabel),
- onError: onError,
onChange: onChange)
// Starting a concurrent observation means that we'll fetch the initial
@@ -908,8 +897,8 @@ extension DatabasePool: DatabaseReader {
onChange(initialValue)
add(observer: observer, from: initialSnapshot)
} catch {
- observer.cancel()
- onError(error)
+ observer.complete()
+ observation.events.didFail?(error)
}
} else {
let label = configuration.identifier(
@@ -967,8 +956,11 @@ extension DatabasePool: DatabaseReader {
}
}
- if fetchNeeded, let value = try observer.fetchValue(db) {
- observer.notifyChange(value)
+ if fetchNeeded {
+ observer.events.databaseDidChange?()
+ if let value = try observer.fetchValue(db) {
+ observer.notifyChange(value)
+ }
}
return .commit
}
diff --git a/GRDB/Core/DatabasePublishers.swift b/GRDB/Core/DatabasePublishers.swift
new file mode 100644
index 0000000000..3aa69d9d93
--- /dev/null
+++ b/GRDB/Core/DatabasePublishers.swift
@@ -0,0 +1,5 @@
+#if canImport(Combine)
+/// A namespace for database Combine publishers.
+@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
+public enum DatabasePublishers { }
+#endif
diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift
index a3d2694529..b3d7db68f3 100644
--- a/GRDB/Core/DatabaseQueue.swift
+++ b/GRDB/Core/DatabaseQueue.swift
@@ -88,10 +88,6 @@ extension DatabaseQueue {
/// Listens to UIApplicationDidEnterBackgroundNotification and
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
/// as much memory as possible.
- ///
- /// - param application: The UIApplication that will start a background
- /// task to let the database queue release its memory when the application
- /// enters background.
private func setupMemoryManagement() {
let center = NotificationCenter.default
center.addObserver(
@@ -480,7 +476,6 @@ extension DatabaseQueue {
public func _add(
observation: ValueObservation,
scheduling scheduler: ValueObservationScheduler,
- onError: @escaping (Error) -> Void,
onChange: @escaping (Reducer.Value) -> Void)
-> DatabaseCancellable
{
@@ -488,14 +483,12 @@ extension DatabaseQueue {
return _addReadOnly(
observation: observation,
scheduling: scheduler,
- onError: onError,
onChange: onChange)
}
let observer = _addWriteOnly(
observation: observation,
scheduling: scheduler,
- onError: onError,
onChange: onChange)
return AnyDatabaseCancellable(cancel: observer.cancel)
}
diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift
index 58f079d111..e0aa1ea556 100644
--- a/GRDB/Core/DatabaseReader.swift
+++ b/GRDB/Core/DatabaseReader.swift
@@ -1,3 +1,6 @@
+#if canImport(Combine)
+import Combine
+#endif
import Dispatch
/// The protocol for all types that can fetch values from a database.
@@ -246,16 +249,12 @@ public protocol DatabaseReader: AnyObject {
/// method instead.
///
/// - parameter observation: the stared observation
- /// - parameter onError: a closure that is provided by eventual errors that happen
- /// during observation
- /// - parameter onChange: a closure that is provided fresh values
/// - returns: a TransactionObserver
///
/// :nodoc:
func _add(
observation: ValueObservation,
scheduling scheduler: ValueObservationScheduler,
- onError: @escaping (Error) -> Void,
onChange: @escaping (Reducer.Value) -> Void)
-> DatabaseCancellable
}
@@ -293,6 +292,87 @@ extension DatabaseReader {
}
}
+#if canImport(Combine)
+extension DatabaseReader {
+ // MARK: - Publishing Database Values
+
+ /// Returns a Publisher that asynchronously completes with a fetched value.
+ ///
+ /// // DatabasePublishers.Read<[Player]>
+ /// let players = dbQueue.readPublisher { db in
+ /// try Player.fetchAll(db)
+ /// }
+ ///
+ /// Its value and completion are emitted on the main dispatch queue.
+ ///
+ /// - parameter value: A closure which accesses the database.
+ @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
+ public func readPublisher