Repository: RxSwiftCommunity/RxGRDB Branch: master Commit: 7dc5a6a00012 Files: 43 Total size: 173.5 KB Directory structure: gitextract_sqrfypq0/ ├── .ci/ │ └── gemfiles/ │ └── Gemfile.travis ├── .gitignore ├── .swiftpm/ │ └── xcode/ │ ├── package.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ ├── RxGRDB.xcscheme │ └── RxGRDBTests.xcscheme ├── .travis.yml ├── CHANGELOG.md ├── Documentation/ │ └── RxGRDBDemo/ │ ├── README.md │ ├── RxGRDBDemo/ │ │ ├── AppDatabase.swift │ │ ├── AppDelegate.swift │ │ ├── Info.plist │ │ ├── Models/ │ │ │ ├── Player.swift │ │ │ └── Players.swift │ │ ├── Resources/ │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── UI/ │ │ │ ├── PlayersViewController.swift │ │ │ └── PlayersViewModel.swift │ │ └── World.swift │ ├── RxGRDBDemo.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── RxGRDBDemo.xcscheme │ └── RxGRDBDemoTests/ │ ├── AppDatabaseTests.swift │ ├── Info.plist │ ├── PlayerTests.swift │ ├── PlayersTests.swift │ └── PlayersViewModelTests.swift ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources/ │ └── RxGRDB/ │ ├── DatabaseReader+Rx.swift │ ├── DatabaseRegionObservation+Rx.swift │ ├── DatabaseWriter+Rx.swift │ ├── GRDBReactive.swift │ └── ValueObservation+Rx.swift ├── TODO.md └── Tests/ └── RxGRDBTests/ ├── DatabaseReaderReadTests.swift ├── DatabaseRegionObservationTests.swift ├── DatabaseWriterWriteTests.swift ├── Support.swift └── ValueObservationTests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .ci/gemfiles/Gemfile.travis ================================================ source 'https://rubygems.org' gem 'xcpretty' gem 'xcpretty-travis-formatter' gem 'cocoapods', '~> 1.7' ================================================ FILE: .gitignore ================================================ ## https://github.com/github/gitignore/blob/master/Global/macOS.gitignore *.DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ## https://github.com/github/gitignore/blob/master/Swift.gitignore # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## Build generated build/ DerivedData/ ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xccheckout *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins .build/ Package.resolved ## Pods/ ================================================ FILE: .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/RxGRDB.xcscheme ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/RxGRDBTests.xcscheme ================================================ ================================================ FILE: .travis.yml ================================================ # The OS X Build Environment # https://docs.travis-ci.com/user/reference/osx/#Xcode-version language: objective-c xcode_project: RxGRDB.xcodeproj # Caches cache: - bundler - cocoapods # Custom CocoaPods installation install: - bundle install - bundle exec pod repo update # Disable the default Travis-CI submodule logic # The various make commands ensure that the appropriate submodules are retrieved git: submodules: false jobs: include: - stage: Test Xcode 11.4 gemfile: .ci/gemfiles/Gemfile.travis osx_image: xcode11.4 env: - TID=RxGRDB [SPM] (macOS) script: make test_SPM - stage: Test Xcode 11.4 gemfile: .ci/gemfiles/Gemfile.travis osx_image: xcode11.4 env: - TID=CocoaPods Lint script: make test_CocoaPodsLint ================================================ FILE: CHANGELOG.md ================================================ Release Notes ============= ## 4.0.1 Released September 28, 2025 - Replace deprecated API methods by [@1Consumption](https://github.com/1Consumption) ## 4.0.0 Released March 15, 2025 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v3.0.0...v4.0.0) - **New**: Support for GRDB 7 - **Breaking Change**: Swift 6+ and Xcode 16+ are required. - **Breaking Change**: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ - **Breaking Change**: This version is not available on CocoaPods. ## 3.0.0 Released September 9, 2022 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v2.1.0...v3.0.0) - **New**: Support for GRDB 6 - **Breaking Change**: Swift 5.7+ and Xcode 14+ are required. - **Breaking Change**: iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ ## 2.1.0 Released October 17, 2021 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v2.0.0...v2.1.0) - **Breaking Change**: Minimum iOS version is now iOS 11.0, and 32-bits devices are no longer supported. This brings compatibility with GRDB v5.12.0 and Xcode 13. - **Breaking Change**: Minimum Swift version is now Swift 5.3. ## 2.0.0 Released January 3, 2021 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v1.0.0...v2.0.0) - **New**: Support for RxSwift 6 ## 1.0.0 Released September 20, 2020 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v1.0.0-beta.3...v1.0.0) ## 1.0.0-beta.3 Released June 7, 2020 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v1.0.0-beta.2...v1.0.0-beta.3) - Fixed CocoaPods integration (fix [#65](https://github.com/RxSwiftCommunity/RxGRDB/issues/65)) ## 1.0.0-beta.2 Released June 6, 2020 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v1.0.0-beta...v1.0.0-beta.2) - **Breaking**: The ValueObservation scheduler is now an argument of the `rx.observe(in:scheduler:)` method, which returns a regular Observable. ## 1.0.0-beta Released May 3, 2020 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.18.0...v1.0.0-beta) Check out the [Migration Guide](Documentation/RxGRDB1MigrationGuide.md). - [#63](https://github.com/RxSwiftCommunity/RxGRDB/pull/63): RxGRDB 1.0 ## 0.18.0 Released December 11, 2019 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.17.0...v0.18.0) - [#60](https://github.com/RxSwiftCommunity/RxGRDB/pull/60) by [@sammygutierrez](https://github.com/sammygutierrez): Add SwiftPM support - [#61](https://github.com/RxSwiftCommunity/RxGRDB/pull/61): Fix error handling of asynchronous writes - [#62](https://github.com/RxSwiftCommunity/RxGRDB/pull/62): Test Xcode 11.2 and SPM on Travis ## 0.17.0 Released June 28, 2019 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.16.0...v0.17.0) - [#58](https://github.com/RxSwiftCommunity/RxGRDB/pull/58): Expose `rx` joiner on database existentials ## 0.16.0 Released June 27, 2019 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.15.0...v0.16.0) - [#57](https://github.com/RxSwiftCommunity/RxGRDB/pull/57): Deprecate PrimaryKeyScanner The [demo app](Documentation/RxGRDBDemo/README.md) has been refactored with the latest GRDB good practices, MVVM architecture, and some tests of the database layer ## 0.15.0 Released June 20, 2019 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.14.0...v0.15.0) - [#55](https://github.com/RxSwiftCommunity/RxGRDB/pull/55): Asynchronous database access ```swift let dbQueue: DatabaseQueue = ... // Async write let write: Completable = dbQueue.rx.write { db in try Player(...).insert(db) } let newPlayerCount: Single = dbQueue.rx.writeAndReturn { db in try Player(...).insert(db) return try Player.fetchCount(db) } // Async read let players: Single<[Player]> = dbQueue.rx.read { db in try Player.fetchAll(db) } ``` ### Breaking Changes - Observation methods have been renamed from `fetch...` to `observe...`: ```diff -Player.all().rx.fetchOne(in: dbQueue) -Player.all().rx.fetchAll(in: dbQueue) -Player.all().rx.fetchCount(in: dbQueue) +Player.all().rx.observeFirst(in: dbQueue) +Player.all().rx.observeAll(in: dbQueue) +Player.all().rx.observeCount(in: dbQueue) ``` - The way to provide a specific scheduler to a value observable has changed: ```diff -Player.all().rx.fetchAll(in: dbQueue, scheduler: MainScheduler.asyncInstance) +Player.all().rx.observeAll(in: dbQueue, observeOn: MainScheduler.asyncInstance) ``` ## 0.14.0 Released May 24, 2019 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.13.0...v0.14.0) ### New - Support for Swift 5, GRDB 4.0, and RxSwift 5.0. - Reactive extension on DatabaseRegionObservation: ```swift let players = Player.all() let teams = Team.all() let observation = DatabaseRegionObservation(tracking: players, teams) observation.rx.changes(in: dbQueue) .subscribe(onNext: { db: Database in print("Players or teams have changed.") }) ``` ### Breaking Changes - Swift 4.0 and Swift 4.1 are no longer supported. - GRDB 3 and RxSwift 4 are no longer supported. - iOS 8 is no longer supported. Minimum deployment target is now iOS 9.0. - Deprecated APIs are no longer available. - `DatabaseWriter.rx.changes` is removed, replaced with `DatabaseRegionObservation.rx.changes`. - SQLCipher support is now available under the CocoaPods `RxGRDB/SQLCipher` name. ## 0.13.0 Released November 2, 2018 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.12.1...v0.13.0) - [#46](https://github.com/RxSwiftCommunity/RxGRDB/pull/46): Implement RxGRDB on top GRDB.ValueObservation ### Breaking Changes - The `DatabaseWriter.rx.fetch` method has been removed. Instead, use [`ValueObservation.rx.fetch`](README.md#valueobservationrxfetchinstartimmediatelyscheduler). - The `distinctUntilChanged` parameter is no longer available when one creates an RxGRDB observable. Filtering of consecutive identical database values is now the default behavior. ### New - One can now create a [values observable](README.md#values-observables) from a [DatabaseReader](https://groue.github.io/GRDB.swift/docs/3.5/Protocols/DatabaseReader.html). ## 0.12.1 Released October 25, 2018 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.12.0...v0.12.1) - Fixed GRDB Cocoapods dependency. ## 0.12.0 Released Septembre 17, 2018 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.11.0...v0.12.0) - [#39](https://github.com/RxSwiftCommunity/RxGRDB/pull/39): Xcode 10 & GRDB 3.3.0 ## 0.11.0 Released June 7, 2018 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.10.0...v0.11.0) ### New - Support for GRDB 3.0 - The new DatabaseRegionConvertible protocol allows better request encapsulation ([documentation](README.md#databaseregionconvertible-protocol)) ### Breaking Changes - "Fetch tokens" and the `mapFetch` operator were ill-advised, and have been removed. Now please use the new `DatabaseWriter.fetch(from:startImmediately:scheduler:values:)` method instead, which produces exactly the same observable: ```diff // Old way -let values = dbQueue.rx - .fetchTokens(in: [request, ...]) - .mapFetch { db in - try fetchResults(db) - } // New way +let values = dbQueue.rx.fetch(from: [request, ...]) { db in + try fetchResults(db) +} ``` ## 0.10.0 Released March 26, 2018 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.9.0...v0.10.0) ### New - Unless they are provided an explicit scheduler, [values observables](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#values-observables) subscribed from the main queue are now guaranteed a synchronous emission of their initial value ([#28](https://github.com/RxSwiftCommunity/RxGRDB/pull/28)). - The RxGRDB repository now uses CocoaPods for its inner dependencies GRBD and RxSwift. After you have downloaded the RxGRDB repository, run `pod repo update; pod install` in order to download all dependencies, build targets, or run tests ([#29](https://github.com/RxSwiftCommunity/RxGRDB/pull/29)). ### Documentation Diff - The [Values Observables](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#values-observables) chapter now describes the scheduling of fetched values. ## 0.9.0 Released February 25, 2018 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.8.1...v0.9.0) ### New - [Values observables](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#values-observables) can now be scheduled on any RxSwift scheduler (fixes [#22](https://github.com/RxSwiftCommunity/RxGRDB/issues/22)). ### Breaking Changees - "Change Tokens" have been renamed "Fetch Tokens" in order to better reflect their purpose, and to enhance the distinction between observables that emit values on any schedulers ("values observables") and have "fetch" in their definition, from observables that emit database connections on a GRDB dispatch queue ("changes observables") and have "changes" in their definition). ### Documentation Diff - The [Scheduling Guide](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#scheduling) has been augmented with a chapter on data consistency. ### API diff ```diff +struct FetchToken { } -struct ChangeToken { } extension Reactive where Base: DatabaseWriter { + func fetchTokens(in requests: [Request], startImmediately: Bool = true, scheduler: ImmediateSchedulerType = MainScheduler.instance) -> Observable - func changeTokens(in requests: [Request], startImmediately: Bool = true, scheduler: ImmediateSchedulerType = MainScheduler.instance) -> Observable } ``` ## 0.8.1 Released February 20, 2018 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.8.0...v0.8.1) - Fixes a bug that would have [values observables](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#values-observables) fail to observe some database changes. ## 0.8.0 Released January 18, 2018 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.7.0...v0.8.0) This version enhances the scheduling of database notifications, and the tracking of specific database rows. ## New - The tracking of requests that target specific rows, identified by their row ids, has been enhanced: In previous version of RxGRDB, tracking `Player.filter(key: 1)` would trigger change notifications for all changes to the players table. Now RxGRDB is able to precisely track the player of ID 1, and won't emit any notification for changes performed on other players. ## Fixed - RxGRDB observables used to require subscription and observation to happen on the same dispatch queue. It was easy to fail this precondition, and misuse the library. This has been fixed. - The [demo application](https://github.com/RxSwiftCommunity/RxGRDB/tree/master/Documentation/RxGRDBDemo) used to misuse MKMapView by converting database changes into annotation coordinate updates on the wrong dispatch queue. This has been fixed. ### Breaking Changes - GRDB dependency has been bumped to v2.6. - Database observation scheduling used to be managed through raw dispatch queues. One now uses regular [RxSwift schedulers](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Schedulers.md). The `synchronizedStart` parameter has been renamed to `startImmediately` in order to reflect the fact that not all schedulers can start synchronously. See the updated [documentation](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#documentation) of RxGRDB reactive methods. - The `Diffable` protocol was ill-advised, and has been removed. - The `primaryKeySortedDiff` operator has been replaced by `PrimaryKeyDiffScanner` ([documentation](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#primarykeydiffscanner)) ### API diff ```diff extension Reactive where Base: Request { - func changes( - in writer: DatabaseWriter, - synchronizedStart: Bool = true) - -> Observable + func changes( + in writer: DatabaseWriter, + startImmediately: Bool = true) + -> Observable - func fetchCount( - in writer: DatabaseWriter, - synchronizedStart: Bool = true, - resultQueue: DispatchQueue = DispatchQueue.main) - -> Observable + func fetchCount( + in writer: DatabaseWriter, + startImmediately: Bool = true, + scheduler: SerialDispatchQueueScheduler = MainScheduler.instance) + -> Observable } extension Reactive where Base: TypedRequest { - func fetchAll( - in writer: DatabaseWriter, - synchronizedStart: Bool = true, - resultQueue: DispatchQueue = DispatchQueue.main, - distinctUntilChanged: Bool = false) - -> Observable<[Base.RowDecoder]> + func fetchAll( + in writer: DatabaseWriter, + startImmediately: Bool = true, + scheduler: SerialDispatchQueueScheduler = MainScheduler.instance, + distinctUntilChanged: Bool = false) + -> Observable<[Base.RowDecoder]> - func fetchOne( - in writer: DatabaseWriter, - synchronizedStart: Bool = true, - resultQueue: DispatchQueue = DispatchQueue.main, - distinctUntilChanged: Bool = false) - -> Observable + func fetchOne( + in writer: DatabaseWriter, + startImmediately: Bool = true, + scheduler: SerialDispatchQueueScheduler = MainScheduler.instance, + distinctUntilChanged: Bool = false) + -> Observable } extension ObservableType where E == ChangeToken { - func mapFetch( - resultQueue: DispatchQueue = DispatchQueue.main, - _ fetch: @escaping (Database) throws -> R) - -> Observable + func mapFetch(_ fetch: @escaping (Database) throws -> R) -> Observable } extension Reactive where Base: DatabaseWriter { - public func changes( - in requests: [Request], - synchronizedStart: Bool = true) - -> Observable + public func changes( + in requests: [Request], + startImmediately: Bool = true) + -> Observable - func changeTokens( - in requests: [Request], - synchronizedStart: Bool = true) - -> Observable + func changeTokens( + in requests: [Request], + startImmediately: Bool = true, + scheduler: SerialDispatchQueueScheduler = MainScheduler.instance) + -> Observable } -protocol Diffable { - func updated(with row: Row) -> Self -} -extension Reactive where Base: TypedRequest, Base.RowDecoder: RowConvertible & MutablePersistable & Diffable { - func primaryKeySortedDiff( - in writer: DatabaseWriter, - initialElements: [Base.RowDecoder] = []) - -> Observable> -} -struct PrimaryKeySortedDiff { ... } +struct PrimaryKeyDiffScanner { + let diff: PrimaryKeyDiff + init( + database: Database, + request: Request, + initialRecords: [Record], + updateRecord: ((Record, Row) -> Record)? = nil) + throws + where Request: TypedRequest, Request.RowDecoder == Record + func diffed(from rows: [Row]) -> PrimaryKeyDiffScanner +} +struct PrimaryKeyDiff { + let inserted: [Record] + let updated: [Record] + let deleted: [Record] + var isEmpty: Bool +} ``` ## 0.7.0 Released October 18, 2017 • [diff](https://github.com/RxSwiftCommunity/RxGRDB/compare/v0.6.0...v0.7.0) ### New - Support for Swift 4 - Support for various diff algorithms ([Documentation](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#diffs)) - New [demo application](https://github.com/RxSwiftCommunity/RxGRDB/tree/master/Documentation/RxGRDBDemo) for various diff algorithms. ### Fixed - Observables that emit fetched values used to emit their first element on the wrong dispatch queue when their `synchronizedStart` option is true. That first element is now correctly emitted on the subscription dispatch queue. ### Breaking Changes - Requirements have changed: Xcode 9+, Swift 4, GRDB 2.0 ## 0.6.0 Released July 13, 2017 - **Fixed**: Support for macOS, broken in v0.5.0 - **New**: GRDB dependency bumped to v1.2 ## 0.5.0 Released July 8, 2017 ### New RxGRDB has learned how to observe multiple requests and fetch from other requests. [Documentation](https://github.com/RxSwiftCommunity/RxGRDB/blob/master/README.md#observing-multiple-requests) To get a single notification when a transaction has modified several requests, use `DatabaseWriter.rx.changes`: ```swift // Observable dbQueue.rx.changes(in: [request, ...]) ``` To turn a change notification into consistent results fetched from multiple requests, use `DatabaseWriter.rx.changeTokens` and the `mapFetch` operator: ```swift dbQueue.rx .changeTokens(in: [request, ...]) .mapFetch { (db: Database) in return ... } ``` ### API diff ```diff +extension Reactive where Base: DatabaseWriter { + func changes(in requests: [Request], synchronizedStart: Bool = true) -> Observable + func changeTokens(in requests: [Request], synchronizedStart: Bool = true) -> Observable +} +struct ChangeToken { + var database: Database { get } +} +extension ObservableType where E == ChangeToken { + func func mapFetch(resultQueue: DispatchQueue = DispatchQueue.main, _ fetch: @escaping (Database) throws -> R) -> Observable +} ``` ## 0.4.1 Released June 20, 2017 ### Fixed - Podspec requirement for RxSwift changed to `~> 3.3` - Added missing support for new AdaptedRequest and AdaptedTypedRequest of GRDB 1.0 ## 0.4.0 Released June 20, 2017 ### Breaking Changes - RxGRDB now requires GRDB v1.0 ## 0.3.0 Released May 22, 2017 ### New - The new `distinctUntilChanged` parameter has RxGRDB avoid notifying consecutive identical values. ```swift request.rx.fetchAll(in: dbQueue, distinctUntilChanged: true)... ``` - Tracking of requests that fetch an array of optional values: ```swift // Email column may be NULL: let request = Person.select(email).bound(to: Optional.self) request.rx.fetchAll(in: dbQueue) .subscribe(onNext: { emails: [String?] in ... }) ``` ## 0.2.0 Released May 17, 2017 ### New - Support for SQLCipher. ## 0.1.2 Released April 6, 2017 ### Fixed - RxGRDB observables now support the `retry` operator, and no longer crash when disposed on a database queue. ## 0.1.1 Released April 5, 2017 ### New - `synchronizedStart` option - `Request.rx.fetchCount(in:synchronizedStart)` ## 0.1.0 Released April 5, 2017 Initial release ================================================ FILE: Documentation/RxGRDBDemo/README.md ================================================ RxGRDBDemo ========== This demo application uses [Action], [RxDataSources], [RxGRDB], and [RxSwift] to synchronize its view with the content of the database. To play with it: 1. Download the RxGRDB repository 2. Run `pod install` 3. Open `RxGRDB.xcworkspace` at the root of the repository 4. Run the RxGRDBDemo application. The rows of the players table view animate as you change the players ordering, delete all players, or refresh them (refreshing applies random transformations to the database) ## Models - [AppDatabase.swift](RxGRDBDemo/AppDatabase.swift) AppDatabase defines the database for the whole application. It uses [DatabaseMigrator](https://github.com/groue/GRDB.swift/blob/master/README.md#migrations) in order to setup the database schema. - [Player.swift](RxGRDBDemo/Models/Player.swift) Player is a [Record](https://github.com/groue/GRDB.swift/blob/master/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](https://github.com/groue/GRDB.swift/blob/master/README.md#codable-records). ```swift struct Player: Codable, Equatable { var id: Int64? var name: String var score: Int } ``` - [Players.swift](RxGRDBDemo/Models/Players.swift) Players defines read and write operations on the players database. ## User Interface - [PlayersViewModel.swift](RxGRDBDemo/UI/PlayersViewModel.swift) PlayersViewModel defines the content displayed on screen, and a bunch of available actions of players. - [PlayersViewController.swift](RxGRDBDemo/UI/PlayersViewController.swift) PlayersViewController feeds from PlayersViewModel and displays it on screen. [Action]: https://github.com/RxSwiftCommunity/Action [RxDataSources]: https://github.com/RxSwiftCommunity/RxDataSources [RxGRDB]: http://github.com/RxSwiftCommunity/RxGRDB [RxSwift]: https://github.com/ReactiveX/RxSwift ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/AppDatabase.swift ================================================ import GRDB /// A type responsible for initializing an application database. struct AppDatabase { /// Prepares a fully initialized database at path func setup(_ database: DatabaseWriter) throws { // Use DatabaseMigrator to define the database schema // See https://github.com/groue/GRDB.swift/#migrations try migrator.migrate(database) // Other possible setup include: custom functions, collations, // full-text tokenizers, etc. } /// The DatabaseMigrator that defines the database schema. // See https://github.com/groue/GRDB.swift/#migrations private var migrator: DatabaseMigrator { var migrator = DatabaseMigrator() #if DEBUG // Speed up development by nuking the database when migrations change migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("v1.0") { db in try db.create(table: "player") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text).notNull().collate(.localizedCaseInsensitiveCompare) t.column("score", .integer).notNull() } try db.create(table: "place") { t in t.autoIncrementedPrimaryKey("id") t.column("latitude", .double).notNull() t.column("longitude", .double).notNull() } } return migrator } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/AppDelegate.swift ================================================ import UIKit import GRDB @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Setup the Current World let dbPool = try! setupDatabase(application) Current = World(database: { dbPool }) // Application is nicer looking if it starts populated try! Current.players().populateIfEmpty() return true } private func setupDatabase(_ application: UIApplication) throws -> DatabasePool { // Create a DatabasePool for efficient multi-threading let databaseURL = try FileManager.default .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent("db.sqlite") let dbPool = try DatabasePool(path: databaseURL.path) // Setup the database try AppDatabase().setup(dbPool) return dbPool } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/Models/Player.swift ================================================ import GRDB // A player struct Player: Codable, Equatable { var id: Int64? var name: String var score: Int } // Adopt FetchableRecord so that we can fetch players from the database. // Implementation is automatically derived from Codable. extension Player: FetchableRecord { } // Adopt MutablePersistable so that we can create/update/delete players in the // database. Implementation is partially derived from Codable. extension Player: MutablePersistableRecord { // Update auto-incremented id upon successful insertion mutating func didInsert(with rowID: Int64, for column: String?) { id = rowID } } // Define columns that we can use for our database requests. // They are derived from the CodingKeys enum for extra safety. extension Player { fileprivate enum Columns { static let id = Column(CodingKeys.id) static let name = Column(CodingKeys.name) static let score = Column(CodingKeys.score) } } // Define requests of players in a constrained extension to the // DerivableRequest protocol. extension DerivableRequest where RowDecoder == Player { func orderByScore() -> Self { return order(Player.Columns.score.desc, Player.Columns.name) } func orderByName() -> Self { return order(Player.Columns.name, Player.Columns.score.desc) } } // Player randomization extension Player { private static let names = [ "Arthur", "Anita", "Barbara", "Bernard", "Clément", "Chiara", "David", "Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette", "Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl", "Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nolwenn", "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 { return names.randomElement()! } static func randomScore() -> Int { return 10 * Int.random(in: 0...100) } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/Models/Players.swift ================================================ import GRDB import RxGRDB import RxSwift /// Players is responsible for high-level operations on the players database. struct Players { private let database: DatabaseWriter init(database: DatabaseWriter) { self.database = database } // MARK: - Modify Players /// Creates random players if needed, and returns whether the database /// was empty. @discardableResult func populateIfEmpty() throws -> Bool { try database.write(_populateIfEmpty) } func deleteAll() -> Single { database.rx.write(updates: _deleteAll) } func deleteOne(_ player: Player) -> Single { database.rx.write(updates: { db in try self._deleteOne(db, player: player) }) } func refresh() -> Single { database.rx.write(updates: _refresh) } func stressTest() -> Single { Single.zip(repeatElement(refresh(), count: 50)).map { _ in } } // MARK: - Access Players /// An observable that tracks changes in the players func playersOrderedByScore() -> Observable<[Player]> { ValueObservation .tracking(Player.all().orderByScore().fetchAll) .rx.observe(in: database) } /// An observable that tracks changes in the players func playersOrderedByName() -> Observable<[Player]> { ValueObservation .tracking(Player.all().orderByName().fetchAll) .rx.observe(in: database) } // MARK: - Implementation // // ⭐️ Good practice: when we want to update the database, we define methods // that accept a Database connection, because they can easily be composed. /// Creates random players if needed, and returns whether the database /// was empty. private func _populateIfEmpty(_ db: Database) throws -> Bool { if try Player.fetchCount(db) > 0 { return false } // Insert new random players for _ in 0..<8 { var player = Player(id: nil, name: Player.randomName(), score: Player.randomScore()) try player.insert(db) } return true } private func _deleteAll(_ db: Database) throws { try Player.deleteAll(db) } private func _deleteOne(_ db: Database, player: Player) throws { try player.delete(db) } private func _refresh(_ db: Database) throws { if try _populateIfEmpty(db) { return } // Insert a player if Bool.random() { var player = Player(id: nil, name: Player.randomName(), score: Player.randomScore()) 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() } } } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "1x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "2x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "1x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "2x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "1x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "2x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "1x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "2x" }, { "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" }, { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/Resources/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/Resources/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/UI/PlayersViewController.swift ================================================ import UIKit import RxDataSources import RxSwift import RxCocoa /// An MVVM ViewController that displays PlayersViewModel class PlayersViewController: UIViewController { @IBOutlet private weak var tableView: UITableView! @IBOutlet private weak var emptyView: UIView! private let viewModel = PlayersViewModel() private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() setupNavigationItem() setupToolbar() setupTableView() setupEmptyView() } private func setupNavigationItem() { viewModel .orderingButtonTitle .subscribe(onNext: updateRightBarButtonItem) .disposed(by: disposeBag) } private func updateRightBarButtonItem(title: String?) { guard let title = title else { navigationItem.rightBarButtonItem = nil return } var barButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil) barButtonItem.rx.action = viewModel.toggleOrdering navigationItem.rightBarButtonItem = barButtonItem } private func setupToolbar() { var deleteAllButtonItem = UIBarButtonItem(barButtonSystemItem: .trash, target: nil, action: nil) deleteAllButtonItem.rx.action = viewModel.deleteAll var refreshButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: nil, action: nil) refreshButtonItem.rx.action = viewModel.refresh var stressTestButtonItem = UIBarButtonItem(title: "💣", style: .plain, target: nil, action: nil) stressTestButtonItem.rx.action = viewModel.stressTest toolbarItems = [ deleteAllButtonItem, UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), refreshButtonItem, UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), stressTestButtonItem, ] } private func setupTableView() { let dataSource = RxTableViewSectionedAnimatedDataSource
(configureCell: { (dataSource, tableView, indexPath, _) in let section = dataSource.sectionModels[indexPath.section] let player = section.items[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: "Player", for: indexPath) cell.textLabel?.text = player.name cell.detailTextLabel?.text = "\(player.score)" return cell }) dataSource.animationConfiguration = AnimationConfiguration( insertAnimation: .fade, reloadAnimation: .fade, deleteAnimation: .fade) dataSource.canEditRowAtIndexPath = { _, _ in true } viewModel .players .asDriver(onErrorJustReturn: []) .map { [Section(items: $0)] } .drive(tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) tableView.rx .itemDeleted .subscribe(onNext: { indexPath in let player = dataSource[indexPath] self.viewModel.deleteOne.execute(player) }) .disposed(by: disposeBag) } private func setupEmptyView() { viewModel .players .map { !$0.isEmpty } .asDriver(onErrorJustReturn: false) .drive(emptyView.rx.isHidden) .disposed(by: disposeBag) } } private struct Section { var items: [Player] } extension Section: AnimatableSectionModelType { var identity: Int { return 1 } init(original: Section, items: [Player]) { self.items = items } } extension Player: IdentifiableType { var identity: Int64 { return id! } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/UI/PlayersViewModel.swift ================================================ import Action import Foundation import GRDB import RxCocoa import RxGRDB import RxSwift /// An MVVM ViewModel for PlayersViewController class PlayersViewModel { // MARK: - Values Displayed on Screen var orderingButtonTitle: Observable var players: Observable<[Player]> // MARK: - Actions var toggleOrdering: CocoaAction var deleteAll: CocoaAction var deleteOne: CompletableAction var refresh: CocoaAction var stressTest: CocoaAction // MARK: - Implementation private enum Ordering: Equatable { case byScore case byName } private var ordering = BehaviorRelay(value: .byScore) init() { // The root of everything let ordering = BehaviorRelay(value: .byScore) // Values Displayed on Screen players = ordering .distinctUntilChanged() .flatMapLatest { ordering -> Observable<[Player]> in switch ordering { case .byScore: return Current.players().playersOrderedByScore() case .byName: return Current.players().playersOrderedByName() } } .share(replay: 1) orderingButtonTitle = Observable .combineLatest(players, ordering) .map { players, ordering -> String? in if players.isEmpty { return nil } switch ordering { case .byScore: return NSLocalizedString("Score ⬇︎", comment: "") case .byName: return NSLocalizedString("Name ⬆︎", comment: "") } } // Actions deleteAll = CocoaAction { Current.players().deleteAll() } deleteOne = CompletableAction { player in Current.players().deleteOne(player).asCompletable() } refresh = CocoaAction { Current.players().refresh() } stressTest = CocoaAction { Current.players().stressTest() } toggleOrdering = CocoaAction { switch ordering.value { case .byName: ordering.accept(.byScore) case .byScore: ordering.accept(.byName) } return Observable.just(()) } } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo/World.swift ================================================ import GRDB /// 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 { /// Access to the players database func players() -> Players { return Players(database: database()) } /// The database, private so that only high-level operations exposed by /// `players` are available to the rest of the application. private var database: () -> DatabaseWriter /// Creates a World with a database init(database: @escaping () -> DatabaseWriter) { self.database = database } } var Current = World(database: { fatalError("Database is uninitialized") }) ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ 565BD7C522C3F8D600BB9B5A /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565BD7C422C3F8D600BB9B5A /* AppDatabaseTests.swift */; }; 565BD7C622C3F92F00BB9B5A /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E1F9931F81021300793BFA /* AppDatabase.swift */; }; 565BD7C722C3F97900BB9B5A /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E1F9951F81033200793BFA /* Player.swift */; }; 565BD7C922C3FA8B00BB9B5A /* PlayersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565BD7C822C3FA8B00BB9B5A /* PlayersTests.swift */; }; 565BD7CA22C3FAA700BB9B5A /* Players.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567515A322C3A9BC00A6FF66 /* Players.swift */; }; 565BD7D022C49D2100BB9B5A /* PlayersViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565BD7CF22C49D2100BB9B5A /* PlayersViewModelTests.swift */; }; 565BD7D122C49D9800BB9B5A /* PlayersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567515A722C3B1F100A6FF66 /* PlayersViewModel.swift */; }; 565BD7D222C49E6600BB9B5A /* World.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567515A122C3A7DC00A6FF66 /* World.swift */; }; 565BD7D922C5DEE800BB9B5A /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565BD7D822C5DEE800BB9B5A /* PlayerTests.swift */; }; 567515A222C3A7DC00A6FF66 /* World.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567515A122C3A7DC00A6FF66 /* World.swift */; }; 567515A422C3A9BC00A6FF66 /* Players.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567515A322C3A9BC00A6FF66 /* Players.swift */; }; 567515A822C3B1F100A6FF66 /* PlayersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567515A722C3B1F100A6FF66 /* PlayersViewModel.swift */; }; 567CE94228CB747200A95C34 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 567CE94128CB747200A95C34 /* RxCocoa */; }; 567E66D02438A6F80091B5D8 /* RxGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 567E66CF2438A6F80091B5D8 /* RxGRDB */; }; 56D6EA2F2438AB77000D55EF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 56D6EA2E2438AB77000D55EF /* RxSwift */; }; 56D6EA322438ABAE000D55EF /* RxDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = 56D6EA312438ABAE000D55EF /* RxDataSources */; }; 56D6EA352438ABD7000D55EF /* Action in Frameworks */ = {isa = PBXBuildFile; productRef = 56D6EA342438ABD7000D55EF /* Action */; }; 56D6EA442438AF90000D55EF /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56D6EA432438AF90000D55EF /* GRDB */; }; 56E1F97D1F8101EE00793BFA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E1F97C1F8101EE00793BFA /* AppDelegate.swift */; }; 56E1F97F1F8101EE00793BFA /* PlayersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E1F97E1F8101EE00793BFA /* PlayersViewController.swift */; }; 56E1F9821F8101EE00793BFA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 56E1F9801F8101EE00793BFA /* Main.storyboard */; }; 56E1F9841F8101EE00793BFA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56E1F9831F8101EE00793BFA /* Assets.xcassets */; }; 56E1F9871F8101EE00793BFA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 56E1F9851F8101EE00793BFA /* LaunchScreen.storyboard */; }; 56E1F9941F81021300793BFA /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E1F9931F81021300793BFA /* AppDatabase.swift */; }; 56E1F9961F81033200793BFA /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E1F9951F81033200793BFA /* Player.swift */; }; 56E549F72438B4770060D2DC /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56E549F62438B4770060D2DC /* GRDB */; }; 56E549F92438B49A0060D2DC /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 56E549F82438B49A0060D2DC /* RxSwift */; }; 56E549FB2438B4A40060D2DC /* RxBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 56E549FA2438B4A40060D2DC /* RxBlocking */; }; 56E549FD2438B5510060D2DC /* RxGRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56E549FC2438B5510060D2DC /* RxGRDB */; }; 56E549FF2438B5670060D2DC /* Action in Frameworks */ = {isa = PBXBuildFile; productRef = 56E549FE2438B5670060D2DC /* Action */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 565BD7BB22C3F84A00BB9B5A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 56E1F9711F8101EE00793BFA /* Project object */; proxyType = 1; remoteGlobalIDString = 56E1F9781F8101EE00793BFA; remoteInfo = RxGRDBDemo; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 569E8501200A5C720028D1EC /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 565BD7B622C3F84A00BB9B5A /* RxGRDBDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RxGRDBDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 565BD7BA22C3F84A00BB9B5A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 565BD7C422C3F8D600BB9B5A /* AppDatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = ""; }; 565BD7C822C3FA8B00BB9B5A /* PlayersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayersTests.swift; sourceTree = ""; }; 565BD7CF22C49D2100BB9B5A /* PlayersViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayersViewModelTests.swift; sourceTree = ""; }; 565BD7D822C5DEE800BB9B5A /* PlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = ""; }; 567515A122C3A7DC00A6FF66 /* World.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = World.swift; sourceTree = ""; }; 567515A322C3A9BC00A6FF66 /* Players.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Players.swift; sourceTree = ""; }; 567515A722C3B1F100A6FF66 /* PlayersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayersViewModel.swift; sourceTree = ""; }; 567E66CD2438A6730091B5D8 /* RxGRDB */ = {isa = PBXFileReference; lastKnownFileType = folder; name = RxGRDB; path = ../..; sourceTree = ""; }; 56E1F9791F8101EE00793BFA /* RxGRDBDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RxGRDBDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 56E1F97C1F8101EE00793BFA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 56E1F97E1F8101EE00793BFA /* PlayersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayersViewController.swift; sourceTree = ""; }; 56E1F9811F8101EE00793BFA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56E1F9831F8101EE00793BFA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56E1F9861F8101EE00793BFA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 56E1F9881F8101EE00793BFA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56E1F9931F81021300793BFA /* AppDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = ""; }; 56E1F9951F81033200793BFA /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 565BD7B322C3F84A00BB9B5A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 56E549FF2438B5670060D2DC /* Action in Frameworks */, 56E549F92438B49A0060D2DC /* RxSwift in Frameworks */, 56E549FB2438B4A40060D2DC /* RxBlocking in Frameworks */, 56E549F72438B4770060D2DC /* GRDB in Frameworks */, 56E549FD2438B5510060D2DC /* RxGRDB in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 56E1F9761F8101EE00793BFA /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 567CE94228CB747200A95C34 /* RxCocoa in Frameworks */, 56D6EA352438ABD7000D55EF /* Action in Frameworks */, 56D6EA442438AF90000D55EF /* GRDB in Frameworks */, 56D6EA322438ABAE000D55EF /* RxDataSources in Frameworks */, 56D6EA2F2438AB77000D55EF /* RxSwift in Frameworks */, 567E66D02438A6F80091B5D8 /* RxGRDB in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 565BD7B722C3F84A00BB9B5A /* RxGRDBDemoTests */ = { isa = PBXGroup; children = ( 565BD7BA22C3F84A00BB9B5A /* Info.plist */, 565BD7C422C3F8D600BB9B5A /* AppDatabaseTests.swift */, 565BD7C822C3FA8B00BB9B5A /* PlayersTests.swift */, 565BD7CF22C49D2100BB9B5A /* PlayersViewModelTests.swift */, 565BD7D822C5DEE800BB9B5A /* PlayerTests.swift */, ); path = RxGRDBDemoTests; sourceTree = ""; }; 5675159E22C3A77800A6FF66 /* Models */ = { isa = PBXGroup; children = ( 56E1F9951F81033200793BFA /* Player.swift */, 567515A322C3A9BC00A6FF66 /* Players.swift */, ); path = Models; sourceTree = ""; }; 5675159F22C3A7AE00A6FF66 /* UI */ = { isa = PBXGroup; children = ( 56E1F97E1F8101EE00793BFA /* PlayersViewController.swift */, 567515A722C3B1F100A6FF66 /* PlayersViewModel.swift */, ); path = UI; sourceTree = ""; }; 567515A022C3A7BA00A6FF66 /* Resources */ = { isa = PBXGroup; children = ( 56E1F9831F8101EE00793BFA /* Assets.xcassets */, 56E1F9851F8101EE00793BFA /* LaunchScreen.storyboard */, 56E1F9801F8101EE00793BFA /* Main.storyboard */, ); path = Resources; sourceTree = ""; }; 567E66CE2438A6F80091B5D8 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; 56E1F9701F8101EE00793BFA = { isa = PBXGroup; children = ( 56E1F97B1F8101EE00793BFA /* RxGRDBDemo */, 565BD7B722C3F84A00BB9B5A /* RxGRDBDemoTests */, 56E1F97A1F8101EE00793BFA /* Products */, 567E66CD2438A6730091B5D8 /* RxGRDB */, 567E66CE2438A6F80091B5D8 /* Frameworks */, ); sourceTree = ""; }; 56E1F97A1F8101EE00793BFA /* Products */ = { isa = PBXGroup; children = ( 56E1F9791F8101EE00793BFA /* RxGRDBDemo.app */, 565BD7B622C3F84A00BB9B5A /* RxGRDBDemoTests.xctest */, ); name = Products; sourceTree = ""; }; 56E1F97B1F8101EE00793BFA /* RxGRDBDemo */ = { isa = PBXGroup; children = ( 56E1F9881F8101EE00793BFA /* Info.plist */, 56E1F9931F81021300793BFA /* AppDatabase.swift */, 56E1F97C1F8101EE00793BFA /* AppDelegate.swift */, 567515A122C3A7DC00A6FF66 /* World.swift */, 5675159E22C3A77800A6FF66 /* Models */, 5675159F22C3A7AE00A6FF66 /* UI */, 567515A022C3A7BA00A6FF66 /* Resources */, ); path = RxGRDBDemo; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 565BD7B522C3F84A00BB9B5A /* RxGRDBDemoTests */ = { isa = PBXNativeTarget; buildConfigurationList = 565BD7BD22C3F84A00BB9B5A /* Build configuration list for PBXNativeTarget "RxGRDBDemoTests" */; buildPhases = ( 565BD7B222C3F84A00BB9B5A /* Sources */, 565BD7B322C3F84A00BB9B5A /* Frameworks */, 565BD7B422C3F84A00BB9B5A /* Resources */, ); buildRules = ( ); dependencies = ( 565BD7BC22C3F84A00BB9B5A /* PBXTargetDependency */, ); name = RxGRDBDemoTests; packageProductDependencies = ( 56E549F62438B4770060D2DC /* GRDB */, 56E549F82438B49A0060D2DC /* RxSwift */, 56E549FA2438B4A40060D2DC /* RxBlocking */, 56E549FC2438B5510060D2DC /* RxGRDB */, 56E549FE2438B5670060D2DC /* Action */, ); productName = RxGRDBDemoTests; productReference = 565BD7B622C3F84A00BB9B5A /* RxGRDBDemoTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 56E1F9781F8101EE00793BFA /* RxGRDBDemo */ = { isa = PBXNativeTarget; buildConfigurationList = 56E1F98B1F8101EE00793BFA /* Build configuration list for PBXNativeTarget "RxGRDBDemo" */; buildPhases = ( 56E1F9751F8101EE00793BFA /* Sources */, 56E1F9761F8101EE00793BFA /* Frameworks */, 56E1F9771F8101EE00793BFA /* Resources */, 569E8501200A5C720028D1EC /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = RxGRDBDemo; packageProductDependencies = ( 567E66CF2438A6F80091B5D8 /* RxGRDB */, 56D6EA2E2438AB77000D55EF /* RxSwift */, 56D6EA312438ABAE000D55EF /* RxDataSources */, 56D6EA342438ABD7000D55EF /* Action */, 56D6EA432438AF90000D55EF /* GRDB */, 567CE94128CB747200A95C34 /* RxCocoa */, ); productName = RxGRDBDemo; productReference = 56E1F9791F8101EE00793BFA /* RxGRDBDemo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 56E1F9711F8101EE00793BFA /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1020; LastUpgradeCheck = 1230; ORGANIZATIONNAME = "Gwendal Roué"; TargetAttributes = { 565BD7B522C3F84A00BB9B5A = { CreatedOnToolsVersion = 10.2.1; ProvisioningStyle = Automatic; TestTargetID = 56E1F9781F8101EE00793BFA; }; 56E1F9781F8101EE00793BFA = { CreatedOnToolsVersion = 9.0; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = 56E1F9741F8101EE00793BFA /* Build configuration list for PBXProject "RxGRDBDemo" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 56E1F9701F8101EE00793BFA; packageReferences = ( 56D6EA2D2438AB77000D55EF /* XCRemoteSwiftPackageReference "RxSwift" */, 56D6EA302438ABAE000D55EF /* XCRemoteSwiftPackageReference "RxDataSources" */, 56D6EA332438ABD7000D55EF /* XCRemoteSwiftPackageReference "Action" */, 56D6EA422438AF90000D55EF /* XCRemoteSwiftPackageReference "GRDB.swift" */, ); productRefGroup = 56E1F97A1F8101EE00793BFA /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 56E1F9781F8101EE00793BFA /* RxGRDBDemo */, 565BD7B522C3F84A00BB9B5A /* RxGRDBDemoTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 565BD7B422C3F84A00BB9B5A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 56E1F9771F8101EE00793BFA /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 56E1F9871F8101EE00793BFA /* LaunchScreen.storyboard in Resources */, 56E1F9841F8101EE00793BFA /* Assets.xcassets in Resources */, 56E1F9821F8101EE00793BFA /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 565BD7B222C3F84A00BB9B5A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 565BD7D922C5DEE800BB9B5A /* PlayerTests.swift in Sources */, 565BD7D122C49D9800BB9B5A /* PlayersViewModel.swift in Sources */, 565BD7C922C3FA8B00BB9B5A /* PlayersTests.swift in Sources */, 565BD7CA22C3FAA700BB9B5A /* Players.swift in Sources */, 565BD7C622C3F92F00BB9B5A /* AppDatabase.swift in Sources */, 565BD7D022C49D2100BB9B5A /* PlayersViewModelTests.swift in Sources */, 565BD7C722C3F97900BB9B5A /* Player.swift in Sources */, 565BD7D222C49E6600BB9B5A /* World.swift in Sources */, 565BD7C522C3F8D600BB9B5A /* AppDatabaseTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 56E1F9751F8101EE00793BFA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 567515A422C3A9BC00A6FF66 /* Players.swift in Sources */, 56E1F9961F81033200793BFA /* Player.swift in Sources */, 56E1F9941F81021300793BFA /* AppDatabase.swift in Sources */, 567515A822C3B1F100A6FF66 /* PlayersViewModel.swift in Sources */, 56E1F97F1F8101EE00793BFA /* PlayersViewController.swift in Sources */, 56E1F97D1F8101EE00793BFA /* AppDelegate.swift in Sources */, 567515A222C3A7DC00A6FF66 /* World.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 565BD7BC22C3F84A00BB9B5A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 56E1F9781F8101EE00793BFA /* RxGRDBDemo */; targetProxy = 565BD7BB22C3F84A00BB9B5A /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 56E1F9801F8101EE00793BFA /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 56E1F9811F8101EE00793BFA /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 56E1F9851F8101EE00793BFA /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 56E1F9861F8101EE00793BFA /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 565BD7BE22C3F84A00BB9B5A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = AMD8W895CT; INFOPLIST_FILE = RxGRDBDemoTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "-Xcc -fmodule-map-file=$(PROJECT_TEMP_ROOT)/GeneratedModuleMaps-$(PLATFORM_NAME)/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.RxGRDBDemoTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RxGRDBDemo.app/RxGRDBDemo"; }; name = Debug; }; 565BD7BF22C3F84A00BB9B5A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = AMD8W895CT; INFOPLIST_FILE = RxGRDBDemoTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "-Xcc -fmodule-map-file=$(PROJECT_TEMP_ROOT)/GeneratedModuleMaps-$(PLATFORM_NAME)/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.RxGRDBDemoTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RxGRDBDemo.app/RxGRDBDemo"; }; name = Release; }; 56E1F9891F8101EE00793BFA /* 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_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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 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; CODE_SIGN_IDENTITY = "iPhone Developer"; 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; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 56E1F98A1F8101EE00793BFA /* 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_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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 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; CODE_SIGN_IDENTITY = "iPhone Developer"; 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; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = Release; }; 56E1F98C1F8101EE00793BFA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = AMD8W895CT; INFOPLIST_FILE = RxGRDBDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.RxGRDBDemo; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 56E1F98D1F8101EE00793BFA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = AMD8W895CT; INFOPLIST_FILE = RxGRDBDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.RxGRDBDemo; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 565BD7BD22C3F84A00BB9B5A /* Build configuration list for PBXNativeTarget "RxGRDBDemoTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 565BD7BE22C3F84A00BB9B5A /* Debug */, 565BD7BF22C3F84A00BB9B5A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 56E1F9741F8101EE00793BFA /* Build configuration list for PBXProject "RxGRDBDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( 56E1F9891F8101EE00793BFA /* Debug */, 56E1F98A1F8101EE00793BFA /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 56E1F98B1F8101EE00793BFA /* Build configuration list for PBXNativeTarget "RxGRDBDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( 56E1F98C1F8101EE00793BFA /* Debug */, 56E1F98D1F8101EE00793BFA /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 56D6EA2D2438AB77000D55EF /* XCRemoteSwiftPackageReference "RxSwift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ReactiveX/RxSwift.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 6.0.0; }; }; 56D6EA302438ABAE000D55EF /* XCRemoteSwiftPackageReference "RxDataSources" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/RxSwiftCommunity/RxDataSources.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 5.0.0; }; }; 56D6EA332438ABD7000D55EF /* XCRemoteSwiftPackageReference "Action" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/RxSwiftCommunity/Action.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 4.0.1; }; }; 56D6EA422438AF90000D55EF /* XCRemoteSwiftPackageReference "GRDB.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/groue/GRDB.swift.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 6.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 567CE94128CB747200A95C34 /* RxCocoa */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA2D2438AB77000D55EF /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxCocoa; }; 567E66CF2438A6F80091B5D8 /* RxGRDB */ = { isa = XCSwiftPackageProductDependency; productName = RxGRDB; }; 56D6EA2E2438AB77000D55EF /* RxSwift */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA2D2438AB77000D55EF /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxSwift; }; 56D6EA312438ABAE000D55EF /* RxDataSources */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA302438ABAE000D55EF /* XCRemoteSwiftPackageReference "RxDataSources" */; productName = RxDataSources; }; 56D6EA342438ABD7000D55EF /* Action */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA332438ABD7000D55EF /* XCRemoteSwiftPackageReference "Action" */; productName = Action; }; 56D6EA432438AF90000D55EF /* GRDB */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA422438AF90000D55EF /* XCRemoteSwiftPackageReference "GRDB.swift" */; productName = GRDB; }; 56E549F62438B4770060D2DC /* GRDB */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA422438AF90000D55EF /* XCRemoteSwiftPackageReference "GRDB.swift" */; productName = GRDB; }; 56E549F82438B49A0060D2DC /* RxSwift */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA2D2438AB77000D55EF /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxSwift; }; 56E549FA2438B4A40060D2DC /* RxBlocking */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA2D2438AB77000D55EF /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxBlocking; }; 56E549FC2438B5510060D2DC /* RxGRDB */ = { isa = XCSwiftPackageProductDependency; productName = RxGRDB; }; 56E549FE2438B5670060D2DC /* Action */ = { isa = XCSwiftPackageProductDependency; package = 56D6EA332438ABD7000D55EF /* XCRemoteSwiftPackageReference "Action" */; productName = Action; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 56E1F9711F8101EE00793BFA /* Project object */; } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemo.xcodeproj/xcshareddata/xcschemes/RxGRDBDemo.xcscheme ================================================ ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemoTests/AppDatabaseTests.swift ================================================ import XCTest import GRDB class AppDatabaseTests: XCTestCase { func testDatabaseSchemaContainsPlayerTable() throws { let dbQueue = try DatabaseQueue() try AppDatabase().setup(dbQueue) try dbQueue.read { db in try XCTAssert(db.tableExists("player")) let columns = try db.columns(in: "player") let columnNames = Set(columns.map { $0.name }) XCTAssertEqual(columnNames, ["id", "name", "score"]) } } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemoTests/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemoTests/PlayerTests.swift ================================================ import XCTest import GRDB class PlayerTests: XCTestCase { func testInsert() throws { let dbQueue = try DatabaseQueue() try AppDatabase().setup(dbQueue) try dbQueue.write { db in var player = Player(id: nil, name: "Arthur", score: 100) try player.insert(db) XCTAssertNotNil(player.id) } } func testRoundtrip() throws { let dbQueue = try DatabaseQueue() try AppDatabase().setup(dbQueue) try dbQueue.write { db in var insertedPlayer = Player(id: 1, name: "Arthur", score: 100) try insertedPlayer.insert(db) let fetchedPlayer = try Player.fetchOne(db, key: 1) XCTAssertEqual(insertedPlayer, fetchedPlayer) } } func testOrderByScore() throws { let dbQueue = try DatabaseQueue() try AppDatabase().setup(dbQueue) var player1 = Player(id: 1, name: "Arthur", score: 100) var player2 = Player(id: 2, name: "Barbara", score: 200) var player3 = Player(id: 3, name: "Craig", score: 150) var player4 = Player(id: 4, name: "David", score: 150) try dbQueue.write { db in try player1.insert(db) try player2.insert(db) try player3.insert(db) try player4.insert(db) } let request = Player.all().orderByScore() try XCTAssertEqual( dbQueue.read(request.fetchAll), [player2, player3, player4, player1]) try dbQueue.write { db in _ = try player1.updateChanges(db) { $0.score = 300 } } try XCTAssertEqual( dbQueue.read(request.fetchAll), [player1, player2, player3, player4]) } func testOrderByName() throws { let dbQueue = try DatabaseQueue() try AppDatabase().setup(dbQueue) var player1 = Player(id: 1, name: "Arthur", score: 100) var player2 = Player(id: 2, name: "Barbara", score: 200) var player3 = Player(id: 3, name: "Craig", score: 150) var player4 = Player(id: 4, name: "David", score: 150) try dbQueue.write { db in try player1.insert(db) try player2.insert(db) try player3.insert(db) try player4.insert(db) } let request = Player.all().orderByName() try XCTAssertEqual( dbQueue.read(request.fetchAll), [player1, player2, player3, player4]) try dbQueue.write { db in _ = try player1.updateChanges(db) { $0.name = "Craig" } } try XCTAssertEqual( dbQueue.read(request.fetchAll), [player2, player3, player1, player4]) } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemoTests/PlayersTests.swift ================================================ import GRDB import RxBlocking import RxSwift import XCTest class PlayersTests: XCTestCase { private func makeDatabase() throws -> DatabaseQueue { // Players needs a database. // Setup an in-memory database, for fast access. let database = try DatabaseQueue() try AppDatabase().setup(database) return database } func testPopulateIfEmptyFromEmptyDatabase() throws { let database = try makeDatabase() let players = Players(database: database) try XCTAssertEqual(database.read(Player.fetchCount), 0) try XCTAssertTrue(players.populateIfEmpty()) try XCTAssertGreaterThan(database.read(Player.fetchCount), 0) } func testPopulateIfEmptyFromNonEmptyDatabase() throws { let database = try makeDatabase() let players = Players(database: database) var player = Player(id: 1, name: "Arthur", score: 100) try database.write { db in try player.insert(db) } try XCTAssertFalse(players.populateIfEmpty()) try XCTAssertEqual(database.read(Player.fetchAll), [player]) } func testDeleteAll() throws { let database = try makeDatabase() let players = Players(database: database) try database.write { db in var player = Player(id: 1, name: "Arthur", score: 100) try player.insert(db) } _ = players.deleteAll().toBlocking().materialize() try XCTAssertEqual(database.read(Player.fetchCount), 0) } func testDeleteOne() throws { let database = try makeDatabase() let players = Players(database: database) var player1 = Player(id: 1, name: "Arthur", score: 100) var player2 = Player(id: 2, name: "Barbara", score: 200) try database.write { db in try player1.insert(db) try player2.insert(db) } _ = players.deleteOne(player1).toBlocking().materialize() try XCTAssertEqual(database.read(Player.fetchAll), [player2]) } func testRefreshPopulatesEmptyDatabase() throws { let database = try makeDatabase() let players = Players(database: database) try XCTAssertEqual(database.read(Player.fetchCount), 0) _ = players.refresh().toBlocking().materialize() try XCTAssertGreaterThan(database.read(Player.fetchCount), 0) } func testPlayersOrderedByName() throws { let database = try makeDatabase() let players = Players(database: database) let disposeBag = DisposeBag() let testSubject = ReplaySubject<[Player]>.createUnbounded() players .playersOrderedByName() .subscribe(testSubject) .disposed(by: disposeBag) var player1 = Player(id: 1, name: "Barbara", score: 100) var player2 = Player(id: 2, name: "Arthur", score: 300) var player3 = Player(id: 3, name: "Craig", score: 200) try database.write { db in try player1.insert(db) try player2.insert(db) } try database.write { db in try player2.delete(db) try player3.insert(db) } let expectedElements: [[Player]] = [ [], [player2, player1], [player1, player3], ] try XCTAssertEqual( testSubject .take(expectedElements.count) .toBlocking() .toArray(), expectedElements) } func testPlayersOrderedByScore() throws { let database = try makeDatabase() let players = Players(database: database) let disposeBag = DisposeBag() let testSubject = ReplaySubject<[Player]>.createUnbounded() players .playersOrderedByScore() .subscribe(testSubject) .disposed(by: disposeBag) var player1 = Player(id: 1, name: "Barbara", score: 100) var player2 = Player(id: 2, name: "Arthur", score: 300) var player3 = Player(id: 3, name: "Craig", score: 200) try database.write { db in try player1.insert(db) try player2.insert(db) } try database.write { db in try player2.delete(db) try player3.insert(db) } let expectedElements: [[Player]] = [ [], [player2, player1], [player3, player1], ] try XCTAssertEqual( testSubject .take(expectedElements.count) .toBlocking() .toArray(), expectedElements) } } ================================================ FILE: Documentation/RxGRDBDemo/RxGRDBDemoTests/PlayersViewModelTests.swift ================================================ import Action import GRDB import RxBlocking import RxSwift import XCTest class PlayersViewModelTests: XCTestCase { override func setUp() { // PlayerViewModel needs a Current World. // Setup one with an in-memory database, for fast access. let dbQueue = try! DatabaseQueue() try! AppDatabase().setup(dbQueue) Current = World(database: { dbQueue }) } func testInitialStateFromEmptyDatabase() throws { let viewModel = PlayersViewModel() let orderingButtonTitle = try viewModel.orderingButtonTitle.take(1).toBlocking().single() let players = try viewModel.players.take(1).toBlocking().single() XCTAssertNil(orderingButtonTitle) XCTAssert(players.isEmpty) } func testInitialStateFromNonEmptyDatabase() throws { try Current.players().populateIfEmpty() let viewModel = PlayersViewModel() let orderingButtonTitle = try viewModel.orderingButtonTitle.take(1).toBlocking().single() let players = try viewModel.players.take(1).toBlocking().single() XCTAssertEqual(orderingButtonTitle, "Score ⬇︎") XCTAssert(!players.isEmpty) } func testToggleOrdering() throws { try Current.players().populateIfEmpty() let viewModel = PlayersViewModel() _ = viewModel.toggleOrdering.execute().toBlocking().materialize() let orderingButtonTitle = try viewModel.orderingButtonTitle.take(1).toBlocking().single() XCTAssertEqual(orderingButtonTitle, "Name ⬆︎") } } ================================================ FILE: LICENSE ================================================ Copyright (C) 2018 RxSwiftCommunity Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # Rules # ===== # # make test - Run all tests but performance tests # make distclean - Restore repository to a pristine state default: test # Configuration # ============= GIT := $(shell command -v git) POD := $(shell command -v pod) XCRUN := $(shell command -v xcrun) SWIFT = $(shell $(XCRUN) --find swift 2> /dev/null) # Used to determine if xcpretty is available XCPRETTY_PATH := $(shell command -v xcpretty 2> /dev/null) # Tests # ===== # If xcpretty is available, use it for xcodebuild output XCPRETTY = ifdef XCPRETTY_PATH XCPRETTY = | xcpretty -c # On Travis-CI, use xcpretty-travis-formatter ifeq ($(TRAVIS),true) XCPRETTY += -f `xcpretty-travis-formatter` endif endif test: test_SPM test_SPM: $(SWIFT) package clean $(SWIFT) build $(SWIFT) build -c release set -o pipefail && $(SWIFT) test $(XCPRETTY) # Cleanup # ======= distclean: $(GIT) reset --hard $(GIT) clean -dffx . .PHONY: distclean test ================================================ FILE: Package.swift ================================================ // swift-tools-version:6.0 import PackageDescription let package = Package( name: "RxGRDB", platforms: [ .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v7), ], products: [ .library(name: "RxGRDB", targets: ["RxGRDB"]), ], dependencies: [ .package(url: "https://github.com/groue/GRDB.swift.git", .upToNextMajor(from: "7.1.0")), .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.0.0")) ], targets: [ .target( name: "RxGRDB", dependencies: [ .product(name: "GRDB", package: "GRDB.swift"), .product(name: "RxSwift", package: "RxSwift"), ]), .testTarget( name: "RxGRDBTests", dependencies: [ "RxGRDB", .product(name: "GRDB", package: "GRDB.swift"), .product(name: "RxBlocking", package: "RxSwift"), ]) ], swiftLanguageModes: [.v5] ) ================================================ FILE: README.md ================================================ RxGRDB [![Swift 6](https://img.shields.io/badge/swift-6-orange.svg?style=flat)](https://developer.apple.com/swift/) [![License](https://img.shields.io/github/license/RxSwiftCommunity/RxGRDB.svg?maxAge=2592000)](/LICENSE) ====== ### A set of extensions for [SQLite], [GRDB.swift], and [RxSwift] **Latest release**: September 28, 2025 • [version 4.0.1](https://github.com/RxSwiftCommunity/RxGRDB/tree/v4.0.1) • [Release Notes] **Requirements**: iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • Swift 6+ / Xcode 16+ --- ## Usage To connect to the database, please refer to [GRDB](https://github.com/groue/GRDB.swift), the database library that supports RxGRDB.
Asynchronously read from the database This observable reads a single value and delivers it. ```swift // Single<[Player]> let players = dbQueue.rx.read { db in try Player.fetchAll(db) } players.subscribe( onSuccess: { (players: [Player]) in print("Players: \(players)") }, onError: { error in ... }) ```
Asynchronously write in the database This observable completes after the database has been updated. ```swift // Single let write = dbQueue.rx.write { db in try Player(...).insert(db) } write.subscribe( onSuccess: { _ in print("Updates completed") }, onError: { error in ... }) // Single let newPlayerCount = dbQueue.rx.write { db -> Int in try Player(...).insert(db) return try Player.fetchCount(db) } newPlayerCount.subscribe( onSuccess: { (playerCount: Int) in print("New players count: \(playerCount)") }, onError: { error in ... }) ```
Observe changes in database values This observable delivers fresh values whenever the database changes: ```swift // Observable<[Player]> let observable = ValueObservation .tracking { db in try Player.fetchAll(db) } .rx.observe(in: dbQueue) observable.subscribe( onNext: { (players: [Player]) in print("Fresh players: \(players)") }, onError: { error in ... }) // Observable let observable = ValueObservation .tracking { db in try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player") } .rx.observe(in: dbQueue) observable.subscribe( onNext: { (maxScore: Int?) in print("Fresh maximum score: \(maxScore)") }, onError: { error in ... }) ```
Observe database transactions This observable delivers database connections whenever a database transaction has impacted an observed region: ```swift // Observable let observable = DatabaseRegionObservation .tracking(Player.all()) .rx.changes(in: dbQueue) observable.subscribe( onNext: { (db: Database) in print("Exclusive write access to the database after players have been impacted") }, onError: { error in ... }) // Observable let observable = DatabaseRegionObservation .tracking(SQLRequest(sql: "SELECT MAX(score) FROM player")) .rx.changes(in: dbQueue) observable.subscribe( onNext: { (db: Database) in print("Exclusive write access to the database after maximum score has been impacted") }, onError: { error in ... }) ```
Documentation ============= - [Installation] - [Demo Application] - [Asynchronous Database Access] - [Database Observation] ## Installation To use RxGRDB with the [Swift Package Manager], add a dependency to your `Package.swift` file: ```swift let package = Package( dependencies: [ .package(url: "https://github.com/RxSwiftCommunity/RxGRDB.git", ...) ] ) ``` To use RxGRDB with [CocoaPods](http://cocoapods.org/), specify in your `Podfile`: ```ruby # Pick only one pod 'RxGRDB' pod 'RxGRDB/SQLCipher' ``` # Asynchronous Database Access RxGRDB provide observables that perform asynchronous database accesses. - [`rx.read(observeOn:value:)`] - [`rx.write(observeOn:updates:)`] - [`rx.write(observeOn:updates:thenRead:)`] #### `DatabaseReader.rx.read(observeOn:value:)` This methods returns a [Single] that completes after database values have been asynchronously fetched. ```swift // Single<[Player]> let players = dbQueue.rx.read { 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 observable 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 `observeOn` argument. #### `DatabaseWriter.rx.write(observeOn:updates:)` This method returns a [Single] that completes after database updates have been successfully executed inside a database transaction. ```swift // Single let write = dbQueue.rx.write { db in try Player(...).insert(db) } // Single let newPlayerCount = dbQueue.rx.write { db -> Int in try Player(...).insert(db) return try Player.fetchCount(db) } ``` This observable 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 `observeOn` argument. You can ignore its value and turn it into a [Completable] with the `asCompletable` operator: ```swift // Completable let write = dbQueue.rx .write { db in try Player(...).insert(db) } .asCompletable() ``` 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 [`rx.write(observeOn:updates:thenRead:)`]. See below. #### `DatabaseWriter.rx.write(observeOn:updates:thenRead:)` This method returns a [Single] that completes after database updates have been successfully executed inside a database transaction, and values have been subsequently fetched: ```swift // Single let newPlayerCount = dbQueue.rx.write( updates: { db in try Player(...).insert(db) } thenRead: { db, _ in try Player.fetchCount(db) }) } ``` It publishes exactly the same values as [`rx.write(observeOn:updates:)`]: ```swift // Single let newPlayerCount = dbQueue.rx.write { 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](https://github.com/groue/GRDB.swift/blob/master/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 observable 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 `observeOn` argument. # Database Observation Database Observation observables are based on GRDB's [ValueObservation] and [DatabaseRegionObservation]. Please refer to their documentation for more information. If your application needs change notifications that are not built in RxGRDB, check the general [Database Changes Observation] chapter. - [`ValueObservation.rx.observe(in:scheduling:)`] - [`DatabaseRegionObservation.rx.changes(in:)`] #### `ValueObservation.rx.observe(in:scheduling:)` GRDB's [ValueObservation] tracks changes in database values. You can turn it into an RxSwift observable: ```swift let observation = ValueObservation.tracking { db in try Player.fetchAll(db) } // Observable<[Player]> let observable = observation.rx.observe(in: dbQueue) ``` This observable 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 `distinctUntilChanged()` RxSwift operator, but we suggest you have a look at the [removeDuplicates()](https://github.com/groue/GRDB.swift/blob/master/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 an RxSwift scheduler, but a [GRDB scheduler](https://github.com/groue/GRDB.swift/blob/master/README.md#valueobservation-scheduling). For example, the `.immediate` scheduler makes sure the initial value is notified immediately when the observable 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 disposable = observation.rx .observe( in: dbQueue, scheduling: .immediate) // <- .subscribe( onNext: { players: [Player] in print("fresh players: \(players)") }, onError: { error in ... }) // <- here "fresh players" is already printed. ``` Note that the `.immediate` scheduler requires that the observable is subscribed from the main thread. It raises a fatal error otherwise. See [ValueObservation Scheduling](https://github.com/groue/GRDB.swift/blob/master/README.md#valueobservation-scheduling) for more information. :warning: **ValueObservation and Data Consistency** When you compose ValueObservation observables together with the [combineLatest](http://reactivex.io/documentation/operators/combinelatest.html) 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 // DATA CONSISTENCY GUARANTEED let hallOfFameObservable = ValueObservation .tracking { db -> HallOfFame in let playerCount = try Player.fetchCount(db) let bestPlayers = try Player.limit(10).orderedByScore().fetchAll(db) return HallOfFame(playerCount:playerCount, bestPlayers:bestPlayers) } .rx.observe(in: dbQueue) ``` See [ValueObservation] for more information. #### `DatabaseRegionObservation.rx.changes(in:)` GRDB's [DatabaseRegionObservation] notifies all transactions that impact a tracked database region. You can turn it into an RxSwift observable: ```swift let request = Player.all() let observation = DatabaseRegionObservation.tracking(request) // Observable let observable = observation.rx.changes(in: dbQueue) ``` This observable 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 disposable = DatabaseRegionObservation .tracking(request) .rx.changes(in: dbQueue) .subscribe( onNext: { (db: Database) in print("Players have changed.") }, onError: { error in ... }) 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. [Asynchronous Database Access]: #asynchronous-database-access [RxSwift]: https://github.com/ReactiveX/RxSwift [Database Changes Observation]: https://github.com/groue/GRDB.swift/blob/master/README.md#database-changes-observation [Database Observation]: #database-observation [DatabaseRegionObservation]: https://github.com/groue/GRDB.swift/blob/master/README.md#databaseregionobservation [Demo Application]: Documentation/RxGRDBDemo/README.md [GRDB.swift]: https://github.com/groue/GRDB.swift [Installation]: #installation [Release Notes]: CHANGELOG.md [SQLite]: http://sqlite.org [Swift Package Manager]: https://swift.org/package-manager/ [ValueObservation]: https://github.com/groue/GRDB.swift/blob/master/README.md#valueobservation [`DatabaseRegionObservation.rx.changes(in:)`]: #databaseregionobservationrxchangesin [`ValueObservation.rx.observe(in:scheduling:)`]: #valueobservationrxobserveinscheduling [`rx.read(observeOn:value:)`]: #databasereaderrxreadobserveonvalue [`rx.write(observeOn:updates:)`]: #databasewriterrxwriteobserveonupdates [`rx.write(observeOn:updates:thenRead:)`]: #databasewriterrxwriteobserveonupdatesthenread [configured]: https://github.com/groue/GRDB.swift/blob/master/README.md#databasepool-configuration [database pool]: https://github.com/groue/GRDB.swift/blob/master/README.md#database-pools [database queue]: https://github.com/groue/GRDB.swift/blob/master/README.md#database-queues [database snapshot]: https://github.com/groue/GRDB.swift/blob/master/README.md#database-snapshots [scheduler]: https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Schedulers.md [Single]: https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Traits.md#single [Completable]: https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Traits.md#completable ================================================ FILE: Sources/RxGRDB/DatabaseReader+Rx.swift ================================================ import GRDB import RxSwift /// We want the `rx` joiner on DatabaseReader. /// Normally we'd use ReactiveCompatible. But ReactiveCompatible is unable to /// define `rx` on existentials as well: /// /// let reader: DatabaseReader /// reader.rx... /// /// :nodoc: extension DatabaseReader { /// Reactive extensions. public var rx: Reactive { Reactive(AnyDatabaseReader(self)) } } extension Reactive where Base: DatabaseReader { /// Returns a Single that asynchronously emits the fetched value. /// /// let dbQueue = try DatabaseQueue() /// let players: Single<[Player]> = dbQueue.rx.read { db in /// try Player.fetchAll(db) /// } /// /// By default, returned values are emitted on the main dispatch queue. If /// you give a *scheduler*, values are emitted on that scheduler. /// /// - parameter value: A closure which accesses the database. /// - parameter scheduler: The scheduler on which the single completes. /// Defaults to MainScheduler.instance. public func read( observeOn scheduler: ImmediateSchedulerType = MainScheduler.instance, value: @escaping (Database) throws -> T) -> Single { Single .create(subscribe: { observer in self.base.asyncRead { db in do { try observer(.success(value(db.get()))) } catch { observer(.failure(error)) } } return Disposables.create { } }) .observe(on: scheduler) } } ================================================ FILE: Sources/RxGRDB/DatabaseRegionObservation+Rx.swift ================================================ import GRDB import RxSwift extension DatabaseRegionObservation { /// Reactive extensions. public var rx: GRDBReactive { GRDBReactive(self) } } extension GRDBReactive where Base == DatabaseRegionObservation { /// Returns an Observable that emits the same elements as /// a DatabaseRegionObservation. /// /// All elements are emitted in a protected database dispatch queue, /// serialized with all database updates. If you set *startImmediately* to /// true (the default value), the first element is emitted synchronously /// upon subscription. See [GRDB Concurrency Guide](https://github.com/groue/GRDB.swift/blob/master/README.md#concurrency) /// for more information. /// /// let dbQueue = try DatabaseQueue() /// try dbQueue.write { db in /// try db.create(table: "player") { t in /// t.column("id", .integer).primaryKey() /// t.column("name", .text) /// } /// } /// /// struct Player: Encodable, PersistableRecord { /// var id: Int64 /// var name: String /// } /// /// let request = Player.all() /// let observation = DatabaseRegionObservation(tracking: request) /// observation.rx /// .changes(in: dbQueue) /// .subscribe(onNext: { db in /// let count = try! Player.fetchCount(db) /// print("Number of players: \(count)") /// }) /// // Prints "Number of players: 0" /// /// try dbQueue.write { db in /// try Player(id: 1, name: "Arthur").insert(db) /// try Player(id: 2, name: "Barbara").insert(db) /// } /// // Prints "Number of players: 2" /// /// try dbQueue.inDatabase { db in /// try Player(id: 3, name: "Craig").insert(db) /// // Prints "Number of players: 3" /// try Player(id: 4, name: "David").insert(db) /// // Prints "Number of players: 4" /// } /// /// - parameter writer: A DatabaseWriter (DatabaseQueue or DatabasePool). public func changes(in writer: DatabaseWriter) -> Observable { Observable.create { observer in let cancellable = self.base.start( in: writer, onError: observer.onError, onChange: observer.onNext) return Disposables.create(with: cancellable.cancel) } } } ================================================ FILE: Sources/RxGRDB/DatabaseWriter+Rx.swift ================================================ import GRDB import RxSwift /// We want the `rx` joiner on DatabaseWriter. /// Normally we'd use ReactiveCompatible. But ReactiveCompatible is unable to /// define `rx` on existentials as well: /// /// let writer: DatabaseWriter /// writer.rx... /// /// :nodoc: extension DatabaseWriter { /// Reactive extensions. public var rx: Reactive { Reactive(AnyDatabaseWriter(self)) } } extension Reactive where Base: DatabaseWriter { /// Returns a Single that asynchronously writes into the database. /// /// let newPlayerCount: Single = dbQueue.rx.write { db in /// try Player(...).insert(db) /// return try Player.fetchCount(db) /// } /// /// By default, the single completes on the main dispatch queue. If /// you give a *scheduler*, is completes on that scheduler. /// /// - parameter scheduler: The scheduler on which the observable completes. /// Defaults to MainScheduler.instance. /// - parameter updates: A closure which writes in the database. public func write( observeOn scheduler: ImmediateSchedulerType = MainScheduler.instance, updates: @escaping @Sendable (Database) throws -> T) -> Single { Single .create(subscribe: { observer in self.base.asyncWrite(updates, completion: { _, result in switch result { case let .success(value): observer(.success(value)) case let .failure(error): observer(.failure(error)) } }) return Disposables.create() }) // We don't want users to process emitted values on a // database dispatch queue. .observe(on: scheduler) } /// Returns a Single that asynchronously writes into the database. /// /// let newPlayerCount: Single = dbQueue.rx.write( /// updates: { db in try Player(...).insert(db) }, /// thenRead: { db, _ in try Player.fetchCount(db) }) /// /// By default, the single completes on the main dispatch queue. If /// you give a *scheduler*, is completes on that scheduler. /// /// - parameter scheduler: The scheduler on which the observable completes. /// Defaults to MainScheduler.instance. /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. public func write( observeOn scheduler: ImmediateSchedulerType = MainScheduler.instance, updates: @escaping @Sendable (Database) throws -> T, thenRead value: @escaping @Sendable (Database, T) throws -> U) -> Single { Single .create(subscribe: { observer in self.base.asyncWriteWithoutTransaction { db in var updatesValue: T? do { try db.inTransaction { updatesValue = try updates(db) return .commit } } catch { observer(.failure(error)) return } // Non-mutable copy so that compiler does not raise any warning. let updatesValue2 = updatesValue self.base.spawnConcurrentRead { dbResult in do { try observer(.success(value(dbResult.get(), updatesValue2!))) } catch { observer(.failure(error)) } } } return Disposables.create() }) // We don't want users to process emitted values on a // database dispatch queue. .observe(on: scheduler) } } ================================================ FILE: Sources/RxGRDB/GRDBReactive.swift ================================================ // Workaround https://github.com/ReactiveX/RxSwift/issues/2270 /// :nodoc: public struct GRDBReactive { /// Base object to extend. let base: Base /// Creates extensions with base object. /// /// - parameter base: Base object. init(_ base: Base) { self.base = base } } ================================================ FILE: Sources/RxGRDB/ValueObservation+Rx.swift ================================================ import Dispatch import GRDB import RxSwift extension ValueObservation { /// Reactive extensions. public var rx: GRDBReactive { GRDBReactive(self) } } /// :nodoc: public protocol _ValueObservationProtocol { associatedtype Reducer: _ValueReducer func _start( in reader: GRDB.DatabaseReader, scheduling scheduler: GRDB.ValueObservationScheduler, onError: @escaping (Error) -> Void, onChange: @escaping (Reducer.Value) -> Void) -> GRDB.DatabaseCancellable } /// :nodoc: extension ValueObservation: _ValueObservationProtocol where Reducer: ValueReducer { public func _start(in reader: GRDB.DatabaseReader, scheduling scheduler: GRDB.ValueObservationScheduler, onError: @escaping (Error) -> Void, onChange: @escaping (Reducer.Value) -> Void) -> GRDB.DatabaseCancellable { start(in: reader, scheduling: scheduler, onError: onError, onChange: onChange) } } extension GRDBReactive where Base: _ValueObservationProtocol { /// Creates an Observable which tracks changes in database values. /// /// For example: /// /// let observation = ValueObservation.tracking { db in /// try Player.fetchAll(db) /// } /// let disposable = observation.rx /// .observe(in: dbQueue) /// .subscribe( /// onNext: { players: [Player] in /// print("fresh players: \(players)") /// }, /// onError: { error in ... }) /// /// By default, fresh values are dispatched asynchronously on the /// main queue. You can change this behavior by by providing a scheduler. /// /// For example, `.immediate` notifies all values on the main queue as well, /// and the first one is immediately notified when the observable /// is subscribed: /// /// // on the main queue /// observation.rx /// .observe( /// in: dbQueue, /// scheduling: .immediate) // <- /// .subscribe(onNext: { (players: [Player]) in /// // on the main queue /// print("Fresh players: \(players)") /// }) /// // <- here "Fresh players" has been printed /// /// Note that the `.immediate` scheduler requires that the observable is /// subscribed from the main thread. It raises a fatal error otherwise. /// /// - parameter reader: A DatabaseReader (DatabaseQueue or DatabasePool). /// are emitted. /// - parameter scheduler: A Scheduler. By default, fresh values are /// dispatched asynchronously on the main queue. /// - returns: An Observable of fresh values. public func observe( in reader: DatabaseReader, scheduling scheduler: ValueObservationScheduler = .async(onQueue: .main)) -> Observable { Observable.create { observer in let cancellable = self.base._start( in: reader, scheduling: scheduler, onError: observer.onError, onChange: observer.onNext) return Disposables.create(with: cancellable.cancel) } } } ================================================ FILE: TODO.md ================================================ ================================================ FILE: Tests/RxGRDBTests/DatabaseReaderReadTests.swift ================================================ import GRDB import RxBlocking import RxGRDB import RxSwift import XCTest private struct Player: Codable, FetchableRecord, PersistableRecord { var id: Int64 var name: String var score: Int? static func createTable(_ db: Database) throws { try db.create(table: "player") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text).notNull() t.column("score", .integer) } } } class DatabaseReaderReadTests : XCTestCase { func testRxJoiner() { // Make sure `rx` joiner is available in various contexts func f1(_ reader: DatabasePool) { _ = reader.rx.read(value: { db in }) } func f2(_ reader: DatabaseQueue) { _ = reader.rx.read(value: { db in }) } func f3(_ reader: DatabaseSnapshot) { _ = reader.rx.read(value: { db in }) } func f4(_ reader: Reader) { _ = reader.rx.read(value: { db in }) } func f5(_ reader: DatabaseReader) { _ = reader.rx.read(value: { db in }) } } // MARK: - func testReadObservable() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(reader: DatabaseReader) throws { let single = reader.rx.read(value: { db in try Player.fetchCount(db) }) let value = try single.toBlocking(timeout: 1).single() XCTAssertEqual(value, 0) } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } } // MARK: - func testReadObservableError() throws { func test(reader: DatabaseReader) throws { let single = reader.rx.read(value: { db in try Row.fetchAll(db, sql: "THIS IS NOT SQL") }) do { _ = try single.toBlocking(timeout: 1).single() XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.sql, "THIS IS NOT SQL") } } try Test(test) .run { try DatabaseQueue() } .runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() } } // MARK: - func testReadObservableIsAsynchronous() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(reader: DatabaseReader) throws { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let expectation = self.expectation(description: "") let semaphore = DispatchSemaphore(value: 0) reader.rx .read(value: { db in try Player.fetchCount(db) }) .subscribe( onSuccess: { _ in semaphore.wait() expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) semaphore.signal() waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } } // MARK: - func testReadObservableDefaultScheduler() throws { if #available(OSX 10.12, iOS 10.0, watchOS 3.0, *) { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(reader: DatabaseReader) { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let expectation = self.expectation(description: "") reader.rx .read(value: { db in try Player.fetchCount(db) }) .subscribe( onSuccess: { _ in dispatchPrecondition(condition: .onQueue(.main)) expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } } } // MARK: - func testReadObservableCustomScheduler() throws { if #available(OSX 10.12, iOS 10.0, watchOS 3.0, *) { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(reader: DatabaseReader) { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let queue = DispatchQueue(label: "test") let expectation = self.expectation(description: "") reader.rx .read( observeOn: SerialDispatchQueueScheduler(queue: queue, internalSerialQueueName: "test"), value: { db in try Player.fetchCount(db) }) .subscribe( onSuccess: { _ in dispatchPrecondition(condition: .onQueue(queue)) expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } } } // MARK: - func testReadObservableIsReadonly() throws { func test(reader: DatabaseReader) throws { let single = reader.rx.read(value: { db in try Player.createTable(db) }) do { _ = try single.toBlocking(timeout: 1).single() XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_READONLY) } } try Test(test) .run { try DatabaseQueue() } .runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() } } } ================================================ FILE: Tests/RxGRDBTests/DatabaseRegionObservationTests.swift ================================================ import XCTest import GRDB import RxSwift import RxGRDB private struct Player: Codable, FetchableRecord, PersistableRecord { var id: Int64 var name: String var score: Int? static func createTable(_ db: Database) throws { try db.create(table: "player") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text).notNull() t.column("score", .integer) } } } class DatabaseRegionObservationTests : XCTestCase { func testChangesNotifications() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let disposeBag = DisposeBag() try withExtendedLifetime(disposeBag) { let testSubject = ReplaySubject.createUnbounded() DatabaseRegionObservation(tracking: Player.all()) .rx.changes(in: writer) .map(Player.fetchCount) .subscribe(testSubject) .disposed(by: disposeBag) try writer.writeWithoutTransaction { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) try db.inTransaction { try Player(id: 2, name: "Barbara", score: 750).insert(db) try Player(id: 3, name: "Craig", score: 500).insert(db) return .commit } } let expectedElements = [1, 3] let elements = try testSubject .take(expectedElements.count) .toBlocking(timeout: 1) .toArray() XCTAssertEqual(elements, expectedElements) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } // This is an usage test. Do the available APIs allow to prepend a // database connection synchronously, with the guarantee that no race can // have the subscriber miss an impactful change? // // TODO: do the same, but asynchronously. If this is too hard, update the // public API so that users can easily do it. func testPrependInitialDatabaseSync() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let disposeBag = DisposeBag() try withExtendedLifetime(disposeBag) { let expectation = self.expectation(description: "") let testSubject = ReplaySubject.createUnbounded() testSubject .map(Player.fetchCount) .take(3) .toArray() .subscribe( onSuccess: { value in XCTAssertEqual(value, [0, 1, 3]) expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) try writer .write({ db in DatabaseRegionObservation(tracking: Player.all()) .rx.changes(in: writer) .startWith(db) .subscribe(testSubject) }) .disposed(by: disposeBag) try writer.writeWithoutTransaction { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) try db.inTransaction { try Player(id: 2, name: "Barbara", score: 750).insert(db) try Player(id: 3, name: "Craig", score: 500).insert(db) return .commit } } waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } } ================================================ FILE: Tests/RxGRDBTests/DatabaseWriterWriteTests.swift ================================================ import GRDB import RxBlocking import RxGRDB import RxSwift import XCTest private struct Player: Codable, FetchableRecord, PersistableRecord { var id: Int64 var name: String var score: Int? static func createTable(_ db: Database) throws { try db.create(table: "player") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text).notNull() t.column("score", .integer) } } } class DatabaseWriterWriteTests : XCTestCase { func testRxJoiner() { // Make sure `rx` joiner is available in various contexts func f1(_ writer: DatabasePool) { _ = writer.rx.write(updates: { db in }) } func f2(_ writer: DatabaseQueue) { _ = writer.rx.write(updates: { db in }) } func f4(_ writer: Writer) { _ = writer.rx.write(updates: { db in }) } func f5(_ writer: DatabaseWriter) { _ = writer.rx.write(updates: { db in }) } } // MARK: - Write func testWriteObservable() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let single = writer.rx.write(updates: { db -> Int in try Player(id: 1, name: "Arthur", score: 1000).insert(db) return try Player.fetchCount(db) }) let count = try single.toBlocking(timeout: 1).single() XCTAssertEqual(count, 1) } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } // MARK: - func testWriteObservableAsCompletable() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let completable = writer.rx .write(updates: { db -> Int in try Player(id: 1, name: "Arthur", score: 1000).insert(db) return try Player.fetchCount(db) }) .asCompletable() _ = try completable.toBlocking(timeout: 1).last() } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } // MARK: - func testWriteObservableError() throws { func test(writer: DatabaseWriter) throws { let single = writer.rx.write(updates: { db in try db.execute(sql: "THIS IS NOT SQL") }) do { _ = try single.toBlocking(timeout: 1).single() XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.sql, "THIS IS NOT SQL") } } try Test(test) .run { try DatabaseQueue() } .runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } func testWriteObservableErrorRollbacksTransaction() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let single = writer.rx.write(updates: { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) try db.execute(sql: "THIS IS NOT SQL") }) do { _ = try single.toBlocking(timeout: 1).single() XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.sql, "THIS IS NOT SQL") } let count = try writer.read(Player.fetchCount) XCTAssertEqual(count, 0) } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } // MARK: - func testWriteObservableIsAsynchronous() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let expectation = self.expectation(description: "") let semaphore = DispatchSemaphore(value: 0) writer.rx .write(updates: { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) }) .subscribe( onSuccess: { semaphore.wait() expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) semaphore.signal() waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } func testWriteObservableDefaultScheduler() throws { if #available(OSX 10.12, iOS 10.0, watchOS 3.0, *) { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let expectation = self.expectation(description: "") writer.rx .write(updates: { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) }) .subscribe( onSuccess: { _ in dispatchPrecondition(condition: .onQueue(.main)) expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } } // MARK: - func testWriteObservableCustomScheduler() throws { if #available(OSX 10.12, iOS 10.0, watchOS 3.0, *) { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let queue = DispatchQueue(label: "test") let expectation = self.expectation(description: "") writer.rx .write( observeOn: SerialDispatchQueueScheduler(queue: queue, internalSerialQueueName: "test"), updates: { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) }) .subscribe( onSuccess: { _ in dispatchPrecondition(condition: .onQueue(queue)) expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } } // MARK: - WriteThenRead func testWriteThenReadObservable() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let single = writer.rx.write( updates: { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) }, thenRead: { db, _ in try Player.fetchCount(db) }) let count = try single.toBlocking(timeout: 1).single() XCTAssertEqual(count, 1) } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } // MARK: - func testWriteThenReadObservableIsReadonly() throws { func test(writer: DatabaseWriter) throws { let single = writer.rx.write( updates: { _ in }, thenRead: { db, _ in try Player.createTable(db) }) do { _ = try single.toBlocking(timeout: 1).single() XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_READONLY) } } try Test(test) .run { try DatabaseQueue() } .runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } // MARK: - func testWriteThenReadObservableWriteError() throws { func test(writer: DatabaseWriter) throws { let single = writer.rx.write( updates: { db in try db.execute(sql: "THIS IS NOT SQL") }, thenRead: { _, _ in }) do { _ = try single.toBlocking(timeout: 1).single() XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.sql, "THIS IS NOT SQL") } } try Test(test) .run { try DatabaseQueue() } .runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } func testWriteThenReadObservableWriteErrorRollbacksTransaction() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let single = writer.rx.write( updates: { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) try db.execute(sql: "THIS IS NOT SQL") }, thenRead: { _, _ in }) do { _ = try single.toBlocking(timeout: 1).single() XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.sql, "THIS IS NOT SQL") } let count = try writer.read(Player.fetchCount) XCTAssertEqual(count, 0) } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } // MARK: - func testWriteThenReadObservableReadError() throws { func test(writer: DatabaseWriter) throws { let single = writer.rx.write( updates: { _ in }, thenRead: { db, _ in try Row.fetchAll(db, sql: "THIS IS NOT SQL") }) do { _ = try single.toBlocking(timeout: 1).single() XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ERROR) XCTAssertEqual(error.sql, "THIS IS NOT SQL") } } try Test(test) .run { try DatabaseQueue() } .runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } // MARK: - func testWriteThenReadObservableDefaultScheduler() throws { if #available(OSX 10.12, iOS 10.0, watchOS 3.0, *) { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let expectation = self.expectation(description: "") writer.rx .write( updates: { _ in }, thenRead: { _, _ in }) .subscribe( onSuccess: { _ in dispatchPrecondition(condition: .onQueue(.main)) expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } } // MARK: - func testWriteThenReadObservableCustomScheduler() throws { if #available(OSX 10.12, iOS 10.0, watchOS 3.0, *) { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let queue = DispatchQueue(label: "test") let expectation = self.expectation(description: "") writer.rx .write( observeOn: SerialDispatchQueueScheduler(queue: queue, internalSerialQueueName: "test"), updates: { _ in }, thenRead: { _, _ in }) .subscribe( onSuccess: { _ in dispatchPrecondition(condition: .onQueue(queue)) expectation.fulfill() }, onFailure: { error in XCTFail("Unexpected error \(error)") }) .disposed(by: disposeBag) waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } } } ================================================ FILE: Tests/RxGRDBTests/Support.swift ================================================ import XCTest import RxSwift import GRDB final class Test { // Raise the repeatCount in order to help spotting flaky tests. private let repeatCount = 1 private let test: (Context) throws -> () init(_ test: @escaping (Context) throws -> ()) { self.test = test } @discardableResult func run(context: () throws -> Context) throws -> Self { for _ in 1...repeatCount { try test(context()) } return self } @discardableResult func runInTemporaryDirectory(context: (_ directoryURL: URL) throws -> Context) throws -> Self { for _ in 1...repeatCount { let directoryURL = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent("GRDBCombine", isDirectory: true) .appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString, isDirectory: true) try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) defer { try! FileManager.default.removeItem(at: directoryURL) } try test(context(directoryURL)) } return self } @discardableResult func runAtTemporaryDatabasePath(context: (_ path: String) throws -> Context) throws -> Self { try runInTemporaryDirectory { url in try context(url.appendingPathComponent("db.sqlite").path) } } } ================================================ FILE: Tests/RxGRDBTests/ValueObservationTests.swift ================================================ import XCTest import GRDB import RxSwift import RxGRDB private struct Player: Codable, FetchableRecord, PersistableRecord { var id: Int64 var name: String var score: Int? static func createTable(_ db: Database) throws { try db.create(table: "player") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text).notNull() t.column("score", .integer) } } } class ValueObservationTests : XCTestCase { // MARK: - Default Scheduler func testDefaultSchedulerChangesNotifications() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let disposeBag = DisposeBag() try withExtendedLifetime(disposeBag) { let testSubject = ReplaySubject.createUnbounded() ValueObservation .tracking(Player.fetchCount) .rx.observe(in: writer) .subscribe(testSubject) .disposed(by: disposeBag) try writer.writeWithoutTransaction { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) try db.inTransaction { try Player(id: 2, name: "Barbara", score: 750).insert(db) try Player(id: 3, name: "Craig", score: 500).insert(db) return .commit } } let expectedElements = [0, 1, 3] if writer is DatabaseQueue { let elements = try testSubject .take(expectedElements.count) .toBlocking(timeout: 1).toArray() XCTAssertEqual(elements, expectedElements) } else { let elements = try testSubject .take(until: { $0 == expectedElements.last }, behavior: .inclusive) .toBlocking(timeout: 1).toArray() assertValueObservationRecordingMatch(recorded: elements, expected: expectedElements) } } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } func testDefaultSchedulerFirstValueIsEmittedAsynchronously() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let expectation = self.expectation(description: "") let semaphore = DispatchSemaphore(value: 0) ValueObservation .tracking(Player.fetchCount) .rx.observe(in: writer) .subscribe(onNext: { _ in semaphore.wait() expectation.fulfill() }) .disposed(by: disposeBag) semaphore.signal() waitForExpectations(timeout: 1, handler: nil) } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } func testDefaultSchedulerError() throws { func test(writer: DatabaseWriter) throws { let observable = ValueObservation .tracking { try $0.execute(sql: "THIS IS NOT SQL") } .rx.observe(in: writer) let result = observable.toBlocking().materialize() switch result { case .completed: XCTFail("Expected error") case let .failed(elements: _, error: error): XCTAssertNotNil(error as? DatabaseError) } } try Test(test) .run { try DatabaseQueue() } .runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } // MARK: - Immediate Scheduler func testImmediateSchedulerChangesNotifications() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let disposeBag = DisposeBag() try withExtendedLifetime(disposeBag) { let testSubject = ReplaySubject.createUnbounded() ValueObservation .tracking(Player.fetchCount) .rx.observe(in: writer, scheduling: .immediate) .subscribe(testSubject) .disposed(by: disposeBag) try writer.writeWithoutTransaction { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) try db.inTransaction { try Player(id: 2, name: "Barbara", score: 750).insert(db) try Player(id: 3, name: "Craig", score: 500).insert(db) return .commit } } let expectedElements = [0, 1, 3] if writer is DatabaseQueue { let elements = try testSubject .take(expectedElements.count) .toBlocking(timeout: 1).toArray() XCTAssertEqual(elements, expectedElements) } else { let elements = try testSubject .take(until: { $0 == expectedElements.last }, behavior: .inclusive) .toBlocking(timeout: 1).toArray() assertValueObservationRecordingMatch(recorded: elements, expected: expectedElements) } } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } func testImmediateSchedulerEmitsFirstValueSynchronously() throws { func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer } func test(writer: DatabaseWriter) throws { let disposeBag = DisposeBag() withExtendedLifetime(disposeBag) { let semaphore = DispatchSemaphore(value: 0) ValueObservation .tracking(Player.fetchCount) .rx.observe(in: writer, scheduling: .immediate) .subscribe(onNext: { _ in semaphore.signal() }) .disposed(by: disposeBag) semaphore.wait() } } try Test(test) .run { try setUp(DatabaseQueue()) } .runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } .runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } } func testImmediateSchedulerError() throws { func test(writer: DatabaseWriter) throws { let observable = ValueObservation .tracking { try $0.execute(sql: "THIS IS NOT SQL") } .rx.observe(in: writer, scheduling: .immediate) let result = observable.toBlocking().materialize() switch result { case .completed: XCTFail("Expected error") case let .failed(elements: _, error: error): XCTAssertNotNil(error as? DatabaseError) } } try Test(test) .run { try DatabaseQueue() } .runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } .runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } func testIssue780() throws { func test(dbPool: DatabasePool) throws { struct Entity: Codable, FetchableRecord, PersistableRecord, Equatable { var id: Int64 var name: String } try dbPool.write { db in try db.create(table: "entity") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text) } } let observation = ValueObservation.tracking(Entity.fetchAll) let entities = try dbPool.rx .write { db in try Entity(id: 1, name: "foo").insert(db) } .asCompletable() .andThen(observation.rx.observe(in: dbPool, scheduling: .immediate)) .take(1) .toBlocking(timeout: 1) .single() XCTAssertEqual(entities, [Entity(id: 1, name: "foo")]) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } // MARK: - Utils /// This test checks the fundamental promise of ValueObservation by /// comparing recorded values with expected values. /// /// Recorded values match the expected values if and only if: /// /// - The last recorded value is the last expected value /// - Recorded values are in the same order as expected values /// /// However, both missing and repeated values are allowed - with the only /// exception of the last expected value which can not be missed. /// /// For example, if the expected values are [0, 1], then the following /// recorded values match: /// /// - `[0, 1]` (identical values) /// - `[1]` (missing value but the last one) /// - `[0, 0, 1, 1]` (repeated value) /// /// However the following recorded values don't match, and fail the test: /// /// - `[1, 0]` (wrong order) /// - `[0]` (missing last value) /// - `[]` (missing last value) /// - `[0, 1, 2]` (unexpected value) /// - `[1, 0, 1]` (unexpected value) func assertValueObservationRecordingMatch( recorded recordedValues: [Value], expected expectedValues: [Value], _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) where Value: Equatable { _assertValueObservationRecordingMatch( recorded: recordedValues, expected: expectedValues, // Last value can't be missed allowMissingLastValue: false, message(), file: file, line: line) } private func _assertValueObservationRecordingMatch( recorded recordedValues: R, expected expectedValues: E, allowMissingLastValue: Bool, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) where R: BidirectionalCollection, E: BidirectionalCollection, R.Element == E.Element, R.Element: Equatable { guard let value = expectedValues.last else { if !recordedValues.isEmpty { XCTFail("unexpected recorded prefix \(Array(recordedValues)) - \(message())", file: file, line: line) } return } let recordedSuffix = recordedValues.reversed().prefix(while: { $0 == value }) let expectedSuffix = expectedValues.reversed().prefix(while: { $0 == value }) if !allowMissingLastValue { // Both missing and repeated values are allowed in the recorded values. // This is because of asynchronous DatabasePool observations. if recordedSuffix.isEmpty { XCTFail("missing expected value \(value) - \(message())", file: file, line: line) } } let remainingRecordedValues = recordedValues.prefix(recordedValues.count - recordedSuffix.count) let remainingExpectedValues = expectedValues.prefix(expectedValues.count - expectedSuffix.count) _assertValueObservationRecordingMatch( recorded: remainingRecordedValues, expected: remainingExpectedValues, // Other values can be missed allowMissingLastValue: true, message(), file: file, line: line) } }