add swiftUI code

This commit is contained in:
zeus
2025-01-22 14:09:10 +08:00
parent 68e7b7347c
commit 8a99853829
2531 changed files with 486215 additions and 0 deletions
@@ -0,0 +1 @@
github: [groue]
@@ -0,0 +1,31 @@
<!--
We use Github issues for bug reports, feature requests, general support, and questions about the library.
Please fill out this template when filing an issue.
-->
### What did you do?
<!-- Please replace this with what you did. -->
### What did you expect to happen?
<!-- Please replace this with what you expected to happen. -->
### What happened instead?
<!-- Please replace this with of what happened instead. -->
### Environment
**GRDB flavor(s):** (GRDB, SQLCipher, Custom SQLite build?)
**GRDB version:**
**Installation method:** (CocoaPods, SPM, manual?)
**Xcode version:**
**Swift version:**
**Platform(s) running GRDB:** (iOS, macOS, watchOS?)
**macOS version running Xcode:**
### Demo Project
<!-- Please link to or upload a project we can download that reproduces the issue. -->
@@ -0,0 +1,14 @@
<!-- Please describe your pull request here. -->
### Pull Request Checklist
<!--
Please check the boxes that apply to your pull request:
-->
- [ ] CONTRIBUTING: You have read https://github.com/groue/GRDB.swift/blob/master/CONTRIBUTING.md
- [ ] BRANCH: This pull request is submitted against the `development` branch.
- [ ] DOCUMENTATION: Inline documentation has been updated.
- [ ] DOCUMENTATION: README.md or another dedicated guide has been updated.
- [ ] TESTS: Changes are tested.
- [ ] TESTS: The `make smokeTest` terminal command runs without failure.
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
@@ -0,0 +1,189 @@
# https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md
name: "GRDB CI"
on:
push:
branches:
- master
- development
paths:
- 'GRDB/**'
- 'Tests/**'
- '.github/workflows/**'
- 'Makefile'
- 'Package.swift'
- 'SQLiteCustom/src'
pull_request:
paths:
- 'GRDB/**'
- 'Tests/**'
- '.github/workflows/**'
- 'Makefile'
- 'Package.swift'
- 'SQLiteCustom/src'
concurrency:
group: ${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
jobs:
Framework:
name: Framework
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer"
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- xcode: "Xcode_14.3.1.app"
runsOn: macOS-13
destination: "platform=macOS"
name: "macOS"
- xcode: "Xcode_14.3.1.app"
runsOn: macOS-13
destination: "OS=16.4,name=iPhone 14 Pro"
name: "iOS"
- xcode: "Xcode_14.3.1.app"
runsOn: macOS-13
destination: "OS=16.4,name=Apple TV"
name: "tvOS"
- xcode: "Xcode_14.2.app"
runsOn: macOS-13
destination: "platform=macOS"
name: "macOS"
- xcode: "Xcode_14.2.app"
runsOn: macOS-13
destination: "OS=16.2,name=iPhone 14"
name: "iOS"
- xcode: "Xcode_14.1.app"
runsOn: macOS-13
destination: "platform=macOS"
name: "macOS"
- xcode: "Xcode_14.1.app"
runsOn: macOS-13
destination: "OS=16.1,name=iPhone 14"
name: "iOS"
- xcode: "Xcode_14.0.1.app"
runsOn: macOS-12
destination: "platform=macOS"
name: "macOS"
- xcode: "Xcode_14.0.1.app"
runsOn: macOS-12
destination: "OS=16.0,name=iPhone 14"
name: "iOS"
steps:
- uses: actions/checkout@v4
- name: ${{ matrix.name }}
run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project GRDB.xcodeproj -scheme GRDB -destination "${{ matrix.destination }}" OTHER_SWIFT_FLAGS='$(inherited) -D SQLITE_ENABLE_FTS5 -D SQLITE_ENABLE_PREUPDATE_HOOK' GCC_PREPROCESSOR_DEFINITIONS='$(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1' clean test
SPM:
name: SPM
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer"
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- xcode: "Xcode_14.3.1.app"
runsOn: macOS-13
name: "Xcode 14.3.1"
- xcode: "Xcode_14.2.app"
runsOn: macOS-13
name: "Xcode 14.2"
- xcode: "Xcode_14.1.app"
runsOn: macOS-13
name: "Xcode 14.1"
- xcode: "Xcode_14.0.1.app"
runsOn: macOS-12
name: "Xcode 14.0.1"
steps:
- uses: actions/checkout@v4
- name: ${{ matrix.name }}
run: make test_SPM test_install_SPM
SQLCipher3:
name: SQLCipher3
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer"
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- xcode: "Xcode_14.3.1.app"
runsOn: macOS-13
name: "Xcode 14.3.1"
- xcode: "Xcode_14.0.1.app"
runsOn: macOS-12
name: "Xcode 14.0.1"
steps:
- uses: actions/checkout@v4
- name: ${{ matrix.name }}
run: make test_framework_SQLCipher3Encrypted
SQLCipher4:
name: SQLCipher4
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer"
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- xcode: "Xcode_14.3.1.app"
runsOn: macOS-13
name: "Xcode 14.3.1"
- xcode: "Xcode_14.0.1.app"
runsOn: macOS-12
name: "Xcode 14.0.1"
steps:
- uses: actions/checkout@v4
- name: ${{ matrix.name }}
run: make test_framework_SQLCipher4Encrypted
CustomSQLite:
name: CustomSQLite
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer"
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- xcode: "Xcode_14.3.1.app"
runsOn: macOS-13
name: "Xcode 14.3.1"
- xcode: "Xcode_14.0.1.app"
runsOn: macOS-12
name: "Xcode 14.0.1"
steps:
- uses: actions/checkout@v4
- name: ${{ matrix.name }}
run: make test_framework_GRDBCustomSQLiteOSX
XCFramework:
name: XCFramework
runs-on: ${{ matrix.runsOn }}
env:
DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}/Contents/Developer"
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- xcode: "Xcode_14.3.1.app"
runsOn: macOS-13
name: "Xcode 14.3.1"
- xcode: "Xcode_14.0.1.app"
runsOn: macOS-12
name: "Xcode 14.0.1"
steps:
- uses: actions/checkout@v4
- name: ${{ matrix.name }}
run: make test_universal_xcframework
@@ -0,0 +1,117 @@
## 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
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
## https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
## https://github.com/github/gitignore/blob/master/Global/TextMate.gitignore
*.tmproj
*.tmproject
tmtags
## https://github.com/github/gitignore/blob/master/Global/Vim.gitignore
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
## https://github.com/github/gitignore/blob/master/Objective-C.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
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint
# Obj-C/Swift specific
*.hmap
*.ipa
# Performance test baselines
*.xcbaseline
# GRDBCustomSQLite USER configuration files
SQLiteCustom/GRDBCustomSQLite-USER.h
SQLiteCustom/GRDBCustomSQLite-USER.xcconfig
# CocoaPods test
Pods
Podfile.lock
# SPM
.build
GRDB.xcworkspace/xcshareddata/swiftpm
#Package.resolved
Tests/SPM/Packages
# Ruby
.ruby-version
# Test products
Tests/products
@@ -0,0 +1,3 @@
[submodule "SQLiteCustom/src"]
path = SQLiteCustom/src
url = https://github.com/swiftlyfalling/SQLiteLib.git
@@ -0,0 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [GRDB]
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GRDB_GRDBTests"
BuildableName = "GRDB_GRDBTests"
BlueprintName = "GRDB_GRDBTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GRDB"
BuildableName = "GRDB"
BlueprintName = "GRDB"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GRDB-dynamic"
BuildableName = "GRDB-dynamic"
BlueprintName = "GRDB-dynamic"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GRDBTests"
BuildableName = "GRDBTests"
BlueprintName = "GRDBTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CSQLite"
BuildableName = "CSQLite"
BlueprintName = "CSQLite"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GRDBTests"
BuildableName = "GRDBTests"
BlueprintName = "GRDBTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "GRDB_GRDBTests"
BuildableName = "GRDB_GRDBTests"
BlueprintName = "GRDB_GRDBTests"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,161 @@
# The OS X Build Environment
# https://docs.travis-ci.com/user/reference/osx/#xcode-version
branches:
only:
- master
- development
language: objective-c
xcode_project: GRDB.xcodeproj
# Caches
cache:
- bundler
- cocoapods
# Custom CocoaPods installation so that Podfile.lock is honored
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:
###########################################
## Test GRDB Xcode 11.4
- name: "Test GRDB Xcode 11.4 - GRDBiOS iOS maxTarget"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode11.4
script: make test_framework_GRDBiOS_maxTarget_maxSwift
- name: "Test GRDB Xcode 11.4 - GRDBiOS iOS minTarget"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode11.4
script: make test_framework_GRDBiOS_minTarget
###########################################
## Test GRDB Xcode 12.5
- name: "Test GRDB Xcode 12.5 - GRDBOSX - Swift 5.3"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBOSX_maxSwift
- name: "Test GRDB Xcode 12.5 - GRDBOSX - Swift 5.2"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBOSX_minSwift
- name: "Test GRDB Xcode 12.5 - GRDBWatchOS"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBWatchOS
- name: "Test GRDB Xcode 12.5 - GRDBiOS iOS maxTarget"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBiOS_maxTarget_maxSwift
- name: "Test GRDB Xcode 12.5 - GRDBiOS iOS minTarget"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBiOS_minTarget
- name: "Test GRDB Xcode 12.5 - GRDBtvOS maxTarget"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBtvOS_maxTarget_maxSwift
- name: "Test GRDB Xcode 12.5 - GRDBtvOS minTarget"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBtvOS_minTarget
- name: "Test GRDB Xcode 12.5 - SPM"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_SPM
###########################################
## Test GRDBCustom Xcode 12.5
- name: "Test GRDBCustom Xcode 12.5 - GRDBOSX"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBCustomSQLiteOSX
- name: "Test GRDBCustom Xcode 12.5 - GRDBiOS maxTarget"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBCustomSQLiteiOS_maxTarget_maxSwift
- name: "Test GRDBCustom Xcode 12.5 - GRDBiOS minTarget"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_GRDBCustomSQLiteiOS_minTarget
###########################################
## Test SQLCipher Xcode 12.5
- name: "Test SQLCipher Xcode 12.5 - SQLCipher 3"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_SQLCipher3
- name: "Test SQLCipher Xcode 12.5 - SQLCipher 4"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_framework_SQLCipher4
###########################################
## Test Installation Xcode 12.5
# Manual Install
- name: "Test Installation Xcode 12.5 - Manual Install"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_install_manual
# Travis is much too slow, and always fail this job
# # CocoaPods Lint
# - name: "Test Installation Xcode 12.5 - CocoaPods Lint"
# gemfile: .ci/gemfiles/Gemfile.travis
# osx_image: xcode12.5
# script: make test_CocoaPodsLint_GRDB
# CocoaPods Install GRDB
- name: "Test Installation Xcode 12.5 - CocoaPods Framework"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_install_GRDB_CocoaPods_framework
# CocoaPods Install GRDB
- name: "Test Installation Xcode 12.5 - CocoaPods Static"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_install_GRDB_CocoaPods_static
# SPM Install
- name: "Test Installation Xcode 12.5 - SPM Package"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_install_SPM_Package
# SPM Install
- name: "Test Installation Xcode 12.5 - SPM Package in Xcode Project"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_install_SPM_Project
# Custom SQLite Install
- name: "Test Installation Xcode 12.5 - Custom SQLite"
gemfile: .ci/gemfiles/Gemfile.travis
osx_image: xcode12.5
script: make test_install_customSQLite
+181
View File
@@ -0,0 +1,181 @@
# Brag Document
Here is a list of people who were happy to meet GRDB :-)
---
https://twitter.com/DamienPetrilli/status/1623922310976786433
> I can't overstate how much more productive I am since I ditched the obsolete Core Data framework and started to use GRDB instead.
>
> The hard stuff is doable, and the simple stuff is even simpler, as it should be.
---
https://twitter.com/fullqueuedev/status/1603350969357684736
> Yes, I much prefer GRDB. Ive tested many SQLite wrappers over the years 💜
---
https://twitter.com/cassiuspacheco/status/1574948903857770497
> SQLite using @groues GRDB, no brainer! Awesome documentation, support and beautifully designed.
---
https://twitter.com/simrandotdev/status/1552004471122587649
> We get Core Data from Apple which is the worst library to deal with.
> Then there is Realm which is nice but again you do not get the RAW SQL power as it is its own type of Db.
> Then comes the most beautiful library every written which is very close to Room called GRDB.
---
https://twitter.com/bellebcooper/status/1534070340850094080
> When I first started using GRDB I didn't know SQL or much at all about databases 😬 So I've always really liked the query API. But these days I really appreciate that you support raw SQL too, because it gives me more control and freedom when I need it. 👍👍👍
---
https://twitter.com/mikeash/status/1504810545429499909
> Another cool library using SQL interpolation!
---
https://twitter.com/swiftkarim/status/1354753451377483781
> I cannot recommend GRDB enough. It works well with Swift and eliminates whole classes of potential programming errors that can be made with Core Data. Also being able to use raw SQL queries if needed can be extremely useful!
---
https://twitter.com/peres/status/1352219836508729346
> Just the occasional reminder that GRDB and its documentation is the gold standard. That's it.
---
https://twitter.com/steipete/status/1275305583718129664
> This is the future of Core Data
---
https://twitter.com/_maxheld/status/1266509824994553860
> I tried out GRDB since I saw you mention it a few days ago and absolutely love it. Its so nice and testing the database logic is so much nicer than CoreData
---
https://twitter.com/stephencelis/status/1263518304980918274
> GRDB.swift is great! (I say this as the person that started SQLite.swift long ago.) And @groues very helpful and friendly.
---
https://twitter.com/peres/status/1262682199482142720
> GRDB in already in my top3 when it comes to documentation. Clearly and pragmatically explained.
---
https://twitter.com/layoutsubviews/status/1249940744929816582
> Mixing SQLite / GRDB with Combine is my whole world right now, and it is delightful.
---
https://twitter.com/steipete/status/1231105339954802694
> It [Core Data] gets the job done, but folks made some really amazing Swift native sqlite wrappers.
---
https://twitter.com/BelleBCooper/status/1223148814980091904
> I LOVE GRDB and highly recommend it. If you dont need it now, do keep it in mind for future because its great and the documentation and support are top notch.
---
https://twitter.com/layoutSubviews/status/1202092984822648832
> GRDB is quickly becoming the best package I have ever dealt with.
> What an exceptional piece of work: the philosophy, the documentation, the API, the abstractions, the Swiftiness👌
---
https://twitter.com/fpillet/status/1193608995405209607
> GRDB keeps being the best-in-class interface to your database, and if you're not using it you're missing out. As always, amazing work from @groue !
---
https://twitter.com/lordmooch/status/1193537540583755776
> Best SQLite/Swift integration 😀👍🏻
---
https://twitter.com/BelleBCooper/status/1192195655743111168
> Ive been working on a side project recently that uses SwiftUI (+UIKit for parent controllers and nav), Combine, GRDB, and GRDBCombine for automatic updating based on DB changes. Im really liking this setup.
---
https://twitter.com/BelleBCooper/status/1186034269577048064
> Working on a new project today and using GRDB by @groue for the backend. This is my go-to for new Swift projects. Highly recommend it!
---
https://twitter.com/krzyzanowskim/status/1156568071303290882
> I think GRDB.swift is a godsend. So far sooooo good. (change my mind?)
---
https://twitter.com/iOSDevWeekly/status/1133780078398021637
> How had I never heard of GRDB before this week? Was I the only one missing out, or is this wonderful database library from @groue news to you all too? https://buff.ly/2X8yHGf #iosdev
----
https://twitter.com/micpringle/status/1132264501812891648
> Spent the last few weeks integrating it and its amazing! And this release brings a whole load more features—especially excited about eager loading of hasMany relationships.
>
> Quite possibly one of the best documented open source libs Ive come across as well👌🏼
---
https://twitter.com/danielemargutti/status/1132217792902647808
> Its a great successor for FMDB with tons of great stuff. Worth a look, we have choose it in our new project even if, actually, I preferred not using some of the ORM like features.
---
https://twitter.com/lordmooch/status/1131664588494254086
> Woohoo! Absolutely superb Swift library for SQLite - I love it 😀
---
https://twitter.com/JadenGeller/status/1115483150564487168
> Wow, there are some *really* good looking SQLite frameworks in Swift.
---
https://twitter.com/strawb3rryx7/status/1073620070784516096
> If you looking for a SQLite wrapper in Swift, just take a look the GRDB library! 👇🏼 https://github.com/groue/GRDB.swift Well-maintained and documented! ✊🏼
---
https://twitter.com/BelleBCooper/status/1047017928112128000
> http://blog.bellebcooper.com/2018-10-02-16-39.html
>
> I love the way issues are handled in GRDB (SQLite for Swift). Groue is so friendly, and always goes above-and-beyond in helping to solve problems or explore new features for the library. It makes me happy to be a GRDB user.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating
documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic
addresses, without explicit permission
- Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to
fairly and consistently applying these principles to every aspect of managing
this project. Project maintainers who do not follow or enforce the Code of
Conduct may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting a project maintainer at [gr@pierlis.com](mailto:gr@pierlis.com).
All complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. Maintainers are
obligated to maintain confidentiality with regard to the reporter of an
incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 1.3.0, available at
[http://contributor-covenant.org/version/1/3/0/][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/3/0/
@@ -0,0 +1,99 @@
How you can Contribute
======================
- [Contribute to the GRDB codebase]
- [Answer issues and contribute to discussions]
- [Financial support and sponsoring]
## Contribute to the GRDB codebase
1. **Install** the following tools:
- For running tests:
- Ruby
- [CocoaPods](https://cocoapods.org) (gem)
- [xcpretty](https://github.com/xcpretty/xcpretty) (gem)
- For contributing code:
- [Swiftlint](https://github.com/realm/SwiftLint) (present in the `$PATH` or in `/opt/homebrew/bin/`)
2. **Clone the `development` branch on your machine.**
3. **Run `make SQLiteCustom` in the terminal**, at the root of the repository.
This step is not strictly necessary if you do not plan to work on custom SQLite builds. But it will avoid a lot of warnings in Xcode.
4. **Open `GRDB.xcworkspace`.** This workspace contains six projects:
- `GRDB.xcodeproj`: the project which contains the GRDB framework and test targets. The sources are organized in two groups:
- `GRDB`: GRDB sources
- `Tests`: GRDB tests
- `GRDBCustom.xcodeproj`: the project which builds GRDB with a custom SQLite build.
- `GRDBProfiling.xcodeproj`: a project which helps profiling GRDB with Instruments. No code here is really precious.
- `GRDBDemoiOS.xcodeproj`, `GRDBCombineDemo.xcodeproj`, `GRDBAsyncDemo.xcodeproj`: the demo apps.
To work with `GRDBCustom.xcodeproj`:
- Make sure Xcode is installed in `/Applications`.
- Run `make SQLiteCustom` in the terminal so that SQLite sources are present.
5. **Run tests**
You can run tests from `GRDB.xcworkspace`, after selecting your scheme (`GRDB` from `GRDB.xcodeproj` or `GRDBCustom` from `GRDBCustom.xcodeproj`).
Before submitting a pull request, please run in the terminal:
```sh
make smokeTest
```
The "smoke tests" perform minimal testing of the system SQLite, SQLCipher, custom SQLite builds, as well as SPM integration.
Before a release, the full test suite must pass:
```sh
make test
```
The full test suite performs many more checks, such as the ability to archive an XCFramework, various installation methods, the demo apps, etc.
6. **Please respect the existing coding style**
- Get familiar with the [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/).
- Spaces, not tabs.
- Whitespace-only lines are not trimmed.
- Documentation comments are hard-wrapped at column 76 (Xcode > Preferences > Text Editing > Display > [X] Page guide at column: 76).
- No Swiftlint warning after a build.
7. **Please provide documentation for your changes**
GRDB documentation is provided as a DocC reference, and guides ([README.md](README.md) and the [Documentation](Documentation) folder).
Please keep the reference and the guides up-to-date. To control the quality of your DocC reference documentation, close the workspace, open `Package.swift` in Xcode, and use Product > Build Documentation.
GRDB is "documentation-driven", which means that nothing ships until it is supported by documentation that makes sense. Documentation makes sense when someone who is not you is able to figure out what is the purpose of your contribution, how to use it, and what are its eventual caveats and corner cases. When the documentation is hard to write, or reveals too many caveats, it is the sign that the api needs to be fixed.
8. **Open a pull request with your changes (targeting the `development` branch)!**
9. **If you are granted a push access to the repository**, check [ReleaseProcess.md](Documentation/ReleaseProcess.md) in order to publish a new GRDB version.
## Answer issues and contribute to discussions
Answering [issues](https://github.com/groue/GRDB.swift/issues), participating in [discussions](https://github.com/groue/GRDB.swift/discussions) and in the [forum](https://forums.swift.org/c/related-projects/grdb/36) is a great way to help, get familiar with the library, and shape its direction.
## Financial support and sponsoring
GRDB is free, and openly developed by its contributors, on their free time, according to their will, needs, and availability. It is not controlled by any company.
Support is free as well, as long as it happens publicly, and is available to everyone as well as internet search engines. This excludes the private channels of social networks, closed environments such as Slack, etc.
When you have specific development or support needs, please send an email to [Gwendal Roué](mailto:gr@pierlis.com) so that we can enter a regular business relationship through the [Pierlis](http://pierlis.com/) company, based in Paris, France.
You can also [sponsor @groue via GitHub](https://github.com/sponsors/groue).
[Contribute to the GRDB codebase]: #contribute-to-the-grdb-codebase
[Answer issues and contribute to discussions]: #answer-issues-and-contribute-to-discussions
[Financial support and sponsoring]: #financial-support-and-sponsoring
@@ -0,0 +1,4 @@
Sharing a Database in an App Group Container
============================================
This guide [has moved](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasesharing).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,404 @@
GRDB ❤️ Combine
===============
**On systems supporting the Combine framework, GRDB offers the ability to publish database values and events using Combine's publishers.**
- [Usage]
- [Demo Application]
- [Asynchronous Database Access]
- [Database Observation]
- [Combine and Data Consistency]: take care when you combine database publishers together
## Usage
To connect to the database, please refer to [Database Connections].
<details>
<summary><strong>Asynchronously read from the database</strong></summary>
This publisher reads a single value and delivers it.
```swift
// DatabasePublishers.Read<[Player]>
let players = dbQueue.readPublisher { db in
try Player.fetchAll(db)
}
```
</details>
<details>
<summary><strong>Asynchronously write in the database</strong></summary>
This publisher updates the database and delivers a single value.
```swift
// DatabasePublishers.Write<Void>
let write = dbQueue.writePublisher { db in
try Player(...).insert(db)
}
// DatabasePublishers.Write<Int>
let newPlayerCount = dbQueue.writePublisher { db -> Int in
try Player(...).insert(db)
return try Player.fetchCount(db)
}
```
</details>
<details>
<summary><strong>Asynchronously migrate the database</strong></summary>
This publisher migrates a database:
```swift
// DatabasePublishers.Migrate
let migrator: DatabaseMigrator = ...
let publisher = migrator.migratePublisher(dbQueue)
```
</details>
<details>
<summary><strong>Observe changes in database values</strong></summary>
This publisher delivers fresh values whenever the database changes:
```swift
// A publisher with output [Player] and failure Error
let publisher = ValueObservation
.tracking { db in try Player.fetchAll(db) }
.publisher(in: dbQueue)
// A publisher with output Int? and failure Error
let publisher = ValueObservation
.tracking { db in try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player") }
.publisher(in: dbQueue)
```
</details>
<details>
<summary><strong>Observe database transactions</strong></summary>
This publisher delivers database connections whenever a database transaction has impacted an observed region:
```swift
// A publisher with output Database and failure Error
let publisher = DatabaseRegionObservation
.tracking(Player.all())
.publisher(in: dbQueue)
let cancellable = publisher.sink(
receiveCompletion: { completion in ... },
receiveValue: { (db: Database) in
print("Exclusive write access to the database after players have been impacted")
})
// A publisher with output Database and failure Error
let publisher = DatabaseRegionObservation
.tracking(SQLRequest<Int>(sql: "SELECT MAX(score) FROM player"))
.publisher(in: dbQueue)
let cancellable = publisher.sink(
receiveCompletion: { completion in ... },
receiveValue: { (db: Database) in
print("Exclusive write access to the database after maximum score has been impacted")
})
```
</details>
# Asynchronous Database Access
GRDB provide publishers that perform asynchronous database accesses:
- [`readPublisher(receiveOn:value:)`]
- [`writePublisher(receiveOn:updates:)`]
- [`writePublisher(receiveOn:updates:thenRead:)`]
- [`migratePublisher(_:receiveOn:)`]
#### `DatabaseReader.readPublisher(receiveOn:value:)`
This methods returns a publisher that completes after database values have been asynchronously fetched.
```swift
// DatabasePublishers.Read<[Player]>
let players = dbQueue.readPublisher { db in
try Player.fetchAll(db)
}
```
Any attempt at modifying the database completes subscriptions with an error.
When you use a [database queue] or a [database snapshot], the read has to wait for any eventual concurrent database access performed by this queue or snapshot to complete.
When you use a [database pool], reads are generally non-blocking, unless the maximum number of concurrent reads has been reached. In this case, a read has to wait for another read to complete. That maximum number can be [configured].
This publisher can be subscribed from any thread. A new database access starts on every subscription.
The fetched value is published on the main queue, unless you provide a specific [scheduler] to the `receiveOn` argument.
#### `DatabaseWriter.writePublisher(receiveOn:updates:)`
This method returns a publisher that completes after database updates have been successfully executed inside a database transaction.
```swift
// DatabasePublishers.Write<Void>
let write = dbQueue.writePublisher { db in
try Player(...).insert(db)
}
// DatabasePublishers.Write<Int>
let newPlayerCount = dbQueue.writePublisher { db -> Int in
try Player(...).insert(db)
return try Player.fetchCount(db)
}
```
This publisher can be subscribed from any thread. A new database access starts on every subscription.
It completes on the main queue, unless you provide a specific [scheduler] to the `receiveOn` argument.
When you use a [database pool], and your app executes some database updates followed by some slow fetches, you may profit from optimized scheduling with [`writePublisher(receiveOn:updates:thenRead:)`]. See below.
#### `DatabaseWriter.writePublisher(receiveOn:updates:thenRead:)`
This method returns a publisher that completes after database updates have been successfully executed inside a database transaction, and values have been subsequently fetched:
```swift
// DatabasePublishers.Write<Int>
let newPlayerCount = dbQueue.writePublisher(
updates: { db in try Player(...).insert(db) }
thenRead: { db, _ in try Player.fetchCount(db) })
}
```
It publishes exactly the same values as [`writePublisher(receiveOn:updates:)`]:
```swift
// DatabasePublishers.Write<Int>
let newPlayerCount = dbQueue.writePublisher { db -> Int in
try Player(...).insert(db)
return try Player.fetchCount(db)
}
```
The difference is that the last fetches are performed in the `thenRead` function. This function accepts two arguments: a readonly database connection, and the result of the `updates` function. This allows you to pass information from a function to the other (it is ignored in the sample code above).
When you use a [database pool], this method applies a scheduling optimization: the `thenRead` function sees the database in the state left by the `updates` function, and yet does not block any concurrent writes. This can reduce database write contention.
When you use a [database queue], the results are guaranteed to be identical, but no scheduling optimization is applied.
This publisher can be subscribed from any thread. A new database access starts on every subscription.
It completes on the main queue, unless you provide a specific [scheduler] to the `receiveOn` argument.
# Database Observation
Database Observation publishers are based on [ValueObservation] and [DatabaseRegionObservation]. Please refer to their documentation for more information. If your application needs change notifications that are not built as Combine publishers, check the general [Database Changes Observation] chapter.
- [`ValueObservation.publisher(in:scheduling:)`]
- [`SharedValueObservation.publisher()`]
- [`DatabaseRegionObservation.publisher(in:)`]
#### `ValueObservation.publisher(in:scheduling:)`
[ValueObservation] tracks changes in database values. You can turn it into a Combine publisher:
```swift
let observation = ValueObservation.tracking { db in
try Player.fetchAll(db)
}
// A publisher with output [Player] and failure Error
let publisher = observation.publisher(in: dbQueue)
```
This publisher has the same behavior as ValueObservation:
- It notifies an initial value before the eventual changes.
- It may coalesce subsequent changes into a single notification.
- It may notify consecutive identical values. You can filter out the undesired duplicates with the `removeDuplicates()` Combine operator, but we suggest you have a look at the [removeDuplicates()](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation/removeduplicates()) GRDB operator also.
- It only completes when it is cancelled.
- By default, it notifies the initial value, as well as eventual changes and errors, on the main thread, asynchronously.
This can be configured with the `scheduling` argument. It does not accept a Combine scheduler, but a [ValueObservationScheduler](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservationscheduler).
For example, the `.immediate` scheduler makes sure the initial value is notified immediately when the publisher is subscribed. It can help your application update the user interface without having to wait for any asynchronous notifications:
```swift
// Immediate notification of the initial value
let cancellable = observation
.publisher(
in: dbQueue,
scheduling: .immediate) // <-
.sink(
receiveCompletion: { completion in ... },
receiveValue: { (players: [Player]) in print("Fresh players: \(players)") })
// <- here "fresh players" is already printed.
```
Note that the `.immediate` scheduler requires that the publisher is subscribed from the main thread. It raises a fatal error otherwise.
#### `SharedValueObservation.publisher()`
[SharedValueObservation] tracks changes in database values. You can turn it into a Combine publisher:
```swift
let sharedObservation = ValueObservation
.tracking { db in try Player.fetchAll(db) }
.shared(in: dbQueue)
// A publisher with output [Player] and failure Error
let publisher = sharedObservation.publisher()
```
This publisher has the same behavior as SharedValueObservation.
#### `DatabaseRegionObservation.publisher(in:)`
[DatabaseRegionObservation] notifies all transactions that impact a tracked database region. You can turn it into a Combine publisher:
```swift
let request = Player.all()
let observation = DatabaseRegionObservation.tracking(request)
// A publisher with output Database and failure Error
let publisher = observation.publisher(in: dbQueue)
```
This publisher can be created and subscribed from any thread. It delivers database connections in a "protected dispatch queue", serialized with all database updates. It only completes when a database error happens.
```swift
let request = Player.all()
let cancellable = DatabaseRegionObservation
.tracking(request)
.publisher(in: dbQueue)
.sink(
receiveCompletion: { completion in ... },
receiveValue: { (db: Database) in
print("Players have changed.")
})
try dbQueue.write { db in
try Player(name: "Arthur").insert(db)
try Player(name: "Barbara").insert(db)
}
// Prints "Players have changed."
try dbQueue.write { db in
try Player.deleteAll(db)
}
// Prints "Players have changed."
```
See [DatabaseRegionObservation] for more information.
## Combine and Data Consistency
When you compose database publishers together with Combine operators such as `combineLatest` or `zip`, you lose all guarantees of [data consistency](https://en.wikipedia.org/wiki/Consistency_(database_systems)).
This is because each database publisher is isolated from others: each one of them sees its own state of the database. Whenever some database change is interleaved between publisher operations, publishers will process or publish values that may not fit well together.
In other words, whenever you need to perform some database access or observation that depends on some database invariant, make sure you define one and only one database publisher instead of combining several publishers. This is how you will prevent eventual concurrent database writes from messing with your app, and introduce bugs.
To this end, remember that *all database publishers can perform several requests*.
In the example below, we are totally sure that the published `HallOfFame` values will never contain inconsistent values, because it is produced by one and only one publisher:
```swift
struct HallOfFame {
// Invariant: bestPlayers.count <= totalPlayerCount
var totalPlayerCount: Int
var bestPlayers: [Player]
}
// CORRECT: DATA CONSISTENCY GUARANTEED
let hallOfFamePublisher = ValueObservation
.tracking { db -> HallOfFame in
// 1st request
let totalPlayerCount = try Player.fetchCount(db)
// 2nd request
let bestPlayers = try Player
.order(Column("score").desc)
.limit(10)
.fetchAll(db)
// 100% guaranteed
assert(bestPlayers.count <= totalPlayerCount)
// Merge results together
return HallOfFame(
totalPlayerCount: totalPlayerCount,
bestPlayers: bestPlayers)
}
.publisher(in: dbQueue)
```
Compare with the incorrect version that combines two database publishers together:
```swift
// OK
let totalPlayerCountPublisher = ValueObservation
.tracking(Player.fetchCount)
.publisher(in: dbQueue)
// OK
let bestPlayerPublisher = ValueObservation
.tracking(Player
.order(Column("score").desc)
.limit(10)
.fetchAll)
.publisher(in: dbQueue)
// NOT OK: DATA CONSISTENCY NOT GUARANTEED
let hallOfFamePublisher = totalPlayerCountPublisher
.combineLatest(bestPlayerPublisher)
.map(HallOfFame.init(totalPlayerCount:bestPlayers))
let cancellable = hallOfFamePublisher.sink(
receiveCompletion: { completion in ... },
receiveValue: { hallOfFame in
// ASSERTION MAY FAIL if some players are deleted
// at the wrong time
assert(hallOfFame.bestPlayers.count <= hallOfFame.totalPlayerCount)
})
```
[Database Connections]: ../README.md#database-connections
[Usage]: #usage
[Asynchronous Database Access]: #asynchronous-database-access
[Combine]: https://developer.apple.com/documentation/combine
[Database Changes Observation]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseobservation
[Database Observation]: #database-observation
[Combine and Data Consistency]: #combine-and-data-consistency
[DatabaseRegionObservation]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseregionobservation
[Demo Application]: DemoApps/GRDBCombineDemo/README.md
[SQLite]: http://sqlite.org
[ValueObservation]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation
[SharedValueObservation]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/sharedvalueobservation
[`DatabaseRegionObservation.publisher(in:)`]: #databaseregionobservationpublisherin
[`ValueObservation.publisher(in:scheduling:)`]: #valueobservationpublisherinscheduling
[`SharedValueObservation.publisher()`]: #sharedvalueobservationpublisher
[`readPublisher(receiveOn:value:)`]: #databasereaderreadpublisherreceiveonvalue
[`writePublisher(receiveOn:updates:)`]: #databasewriterwritepublisherreceiveonupdates
[`writePublisher(receiveOn:updates:thenRead:)`]: #databasewriterwritepublisherreceiveonupdatesthenread
[`migratePublisher(_:receiveOn:)`]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasemigrator/migratepublisher(_:receiveon:)
[configured]: ../README.md#databasepool-configuration
[database pool]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasepool
[database queue]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasequeue
[database snapshot]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasesnapshot
[scheduler]: https://developer.apple.com/documentation/combine/scheduler
@@ -0,0 +1,414 @@
Common Table Expressions
========================
[**:fire: EXPERIMENTAL**](../README.md#what-are-experimental-features)
---
**Common table expressions** (CTEs) can generally be seen as *SQL views that you define on the fly*.
A certain level of familiarity with SQL databases is helpful before you dive into this guide. The starting point is obviously the [SQLite documentation](https://sqlite.org/lang_with.html). Many CTE tutorials exist online as well, including [this good one](https://blog.expensify.com/2015/09/25/the-simplest-sqlite-common-table-expression-tutorial/).
In this guide, you will learn how to:
- [Define Common Table Expressions]
- [Embed Common Table Expressions in Requests]
- [Fetch Values From Common Table Expressions]
- Join CTEs with [Associations to Common Table Expressions]
> **Note**: most code examples will be trivial, and not very "useful". This is because the goal of this guide is to stay focused on the GRDB support for CTEs. Rich setup would just be distracting. So bring your own good ideas with you!
## Define Common Table Expressions
You will create a `CommonTableExpression` definition first. Choose a **name**, and a **request** that provides the content of the common table expression.
The CTE name is like a regular table name: pick one that does not conflict with the names of existing tables.
The CTE request can be provided as a [query interface request]:
```swift
// WITH playerName AS (SELECT name FROM player) ...
let playerNameCTE = CommonTableExpression(
named: "playerName",
request: Player.select(Column("name")))
```
You can feed a CTE with raw SQL as well (second and third examples use [SQL Interpolation]):
```swift
let name = "O'Brien"
// WITH playerName AS (SELECT 'O''Brien') ...
let playerNameCTE = CommonTableExpression(
named: "playerName",
sql: "SELECT ?", arguments: [name])
// WITH playerName AS (SELECT 'O''Brien') ...
let playerNameCTE = CommonTableExpression(
named: "playerName",
literal: "SELECT \(name)")
// WITH playerName AS (SELECT 'O''Brien') ...
let request = SQLRequest("SELECT \(name)")
let playerNameCTE = CommonTableExpression(
named: "playerName",
request: request)
```
All CTEs can be provided with explicit column names:
```swift
// WITH pair(a, b) AS (SELECT 1, 2) ...
let pairCTE = CommonTableExpression(
named: "pair",
columns: ["a", "b"],
sql: "SELECT 1, 2")
```
Recursive CTEs need the `recursive` flag. The example below selects all integers between 1 and 1000:
```swift
// WITH RECURSIVE counter(x) AS
// (VALUES(1) UNION ALL SELECT x+1 FROM counter WHERE x<1000)
let counterCTE = CommonTableExpression(
recursive: true,
named: "counter",
columns: ["x"],
sql: """
VALUES(1)
UNION ALL
SELECT x+1 FROM counter WHERE x<1000
""")
```
> **Note**: many recursive CTEs use the `UNION ALL` SQL operator. The query interface does not provide any Swift support for it, so you'll generally have to write SQL in your definitions of recursive CTEs.
## Embed Common Table Expressions in Requests
A typical SQLite query that uses a common table expression first *defines* the CTE and then *uses* the CTE by mentioning its table name. We'll see below Swift apis that match those two steps.
We will use the (simple) query below as a target. It is the query we'll want to generate in this chapter. It defines a CTE, and uses it in a subquery:
```sql
WITH playerName AS (SELECT 'O''Brien')
SELECT * FROM player
WHERE name = (SELECT * FROM playerName)
```
We first build a `CommonTableExpression`:
```swift
let name = "O'Brien"
let playerNameCTE = CommonTableExpression(
named: "playerName",
literal: "SELECT \(name)")
```
We can then embed the definition of the CTE in a [query interface request] by calling the `with(_:)` method:
```swift
// WITH playerName AS (SELECT 'O''Brien')
// SELECT * FROM player ...
let request = Player
.with(playerNameCTE)...
```
And we can then filter the `player` table with a subquery:
```swift
// WITH playerName AS (SELECT 'O''Brien')
// SELECT * FROM player
// WHERE name = (SELECT * FROM playerName)
let request = Player
.with(playerNameCTE)
.filter(Column("name") == playerNameCTE.all())
```
> **Note**: the `with(_:)` method can be called as many times as a there are common table expressions in your request.
>
> **Note**: the `with(_:)` method can be called at any time, as all request methods: `Player.with(...).filter(...).with(...)`.
>
> **Note**: the `with(_:)` method replaces any previously embedded CTE that has the same table name. This allows you to embed the same CTE several times if you feel like it.
>
> **Note**: the `CommonTableExpression.all()` method builds a regular [query interface request] for the content of the CTE (like `SELECT * FROM <cte name>`, not to be mismatched with the request that was used to define the CTE). You can filter this request, sort it, etc, like all query interface requests:
>
> ```swift
> cte.all().select(...).filter(...).group(...).order(...)
> ```
Common table expressions can also be embedded in [SQLRequest] with [SQL Interpolation]:
```swift
// WITH playerName AS (SELECT 'O''Brien')
// SELECT * FROM player
// WHERE name = (SELECT * FROM playerName)
let request: SQLRequest<Player> = """
WITH \(definitionFor: playerNameCTE)
SELECT * FROM player
WHERE name = (SELECT * FROM \(playerNameCTE))
"""
// WITH playerName AS (SELECT 'O''Brien')
// SELECT * FROM player
// WHERE name = (SELECT * FROM playerName)
let request: SQLRequest<Player> = """
WITH \(definitionFor: playerNameCTE)
SELECT * FROM player
WHERE name = (\(playerNameCTE.all()))
"""
```
Common table expressions can also be used as subqueries, when you update or delete rows in the database:
```swift
// WITH playerName AS (SELECT 'O''Brien')
// UPDATE player SET name = (SELECT * FROM playerName)
try Player
.with(playerNameCTE)
.updateAll(db, Column("name").set(to: playerNameCTE.all()))
// WITH playerName AS (SELECT 'O''Brien')
// DELETE FROM player WHERE name = (SELECT * FROM playerName)
try Player
.with(playerNameCTE)
.filter(Column("name") == playerNameCTE.all())
.deleteAll(db)
```
## Fetch Values From Common Table Expressions
In the previous chapter, a common table expression was embedded as a subquery, with the `CommonTableExpression.all()` method.
`cte.all()` builds a regular [query interface request] that you can filter, sort, etc, like all query interface requests.
You can also fetch from `cte.all()`, as long as the request is given the definition of the CTE: `cte.all().with(cte)`. In SQL, this would give: `WITH cte AS (...) SELECT * FROM cte`:
This request, of type `QueryInterfaceRequest<Row>`, can fetch raw database [rows](../README.md#row-queries):
```swift
let cte = CommonTableExpression(...)
let request = cte.all().with(cte)
let rows = try request.fetchAll(db) // [Row]
```
In order to fetch something else, such as simple [values](../README.md#value-queries), or custom [records](../README.md#records), you have two possible options:
1. Use the `asRequest(of:)` method:
```swift
let cte = CommonTableExpression(...)
let request = cte.all().with(cte).asRequest(of: Player.self)
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
let players = try request.fetchAll(db) // [Player]
```
2. Provide the fetched type to the cte itself:
```swift
let cte = CommonTableExpression<Player>(...)
// ~~~~~~~~
let request = cte.all().with(cte)
let players = try request.fetchAll(db) // [Player]
```
## Associations to Common Table Expressions
GRDB [associations] define "to-one" and "to-many" relationships between two database tables. Here we will define associations between regular tables and common table expressions.
We recommend familiarity with the "joining methods", described in [Joining And Prefetching Associated Records]:
```swift
// SELECT parent.* FROM parent LEFT JOIN child ON ...
Parent.joining(optional: childAssociation)
// SELECT parent.* FROM parent JOIN child ON ...
Parent.joining(required: childAssociation)
// SELECT parent.*, child.* FROM parent LEFT JOIN child ON ...
Parent.including(optional: childAssociation)
// SELECT parent.*, child.* FROM parent JOIN child ON ...
Parent.including(required: childAssociation)
```
> **Note**: common table expressions currently only define "to-one" associations, so the `including(all:)` joining method is unavailable.
CTE associations are generally built with the `association(to:on:)` method, which needs:
- The two sides of the association: a `CommonTableExpression` instance, and another CTE or a type that conforms to the [TableRecord] protocol.
- A function that returns the condition that joins the two sides of the association.
The condition function plays the same role as the **foreign key** that defines regular table [associations] such as **BelongsTo** or **HasMany**. It accepts two [TableAlias], from which you can build a joining expression:
For example:
```swift
// An association from LeftRecord to rightCTE
let rightCTE = ...
let association = LeftRecord.association(
to: rightCTE,
on: { left, right in
left[Column("x")] == right[Column("y")]
})
```
Now this association can be used with a joining method:
```swift
// WITH rightCTE AS (...)
// SELECT leftRecord.*, rightCTE.*
// FROM leftRecord
// JOIN rightCTE ON leftRecord.x = rightCTE.y
LeftRecord
.with(rightCTE)
.including(required: association)
```
### CTE Association Example: a Chat App
As an example, let's build the classical main screen of a chat application: a list of all latest messages from all conversations.
The database schema of the chat app contains a `chat` and a `message` table. The application defines the following records:
```swift
struct Chat: Codable, FetchableRecord, PersistableRecord {
var id: Int64
...
}
struct Message: Codable, FetchableRecord, PersistableRecord {
var chatID: Int64
var date: Date
...
}
```
To feed the main app screen, we want to fetch a list of `ChatInfo` records:
```swift
struct ChatInfo: Decodable, FetchableRecord {
/// The chat
var chat: Chat
/// The latest chat message, if any
var latestMessage: Message?
}
```
The SQL request that we want to run is below. It uses an SQLite-specific [special processing](https://sqlite.org/lang_select.html) of `MAX()` that helps the selection of latest messages from all chats:
```sql
WITH latestMessage AS
(SELECT *, MAX(date) FROM message GROUP BY chatID)
SELECT chat.*, latestMessage.*
FROM chat
LEFT JOIN latestMessage ON chat.id = latestMessage.chatID
ORDER BY latestMessage.date DESC
```
We start by defining the CTE request, which loads the latest messages of all chats:
```swift
// SELECT *, MAX(date) FROM message GROUP BY chatID
let latestMessageRequest = Message
.annotated(with: max(Column("date")))
.group(Column("chatID"))
```
We can now define the CTE for the latest messages:
```swift
// WITH latestMessage AS
// (SELECT *, MAX(date) FROM message GROUP BY chatID)
let latestMessageCTE = CommonTableExpression(
named: "latestMessage",
request: latestMessageRequest)
```
The association from a chat to its latest message follows:
```swift
// ... JOIN latestMessage ON chat.id = latestMessage.chatID
let latestMessage = Chat.association(
to: latestMessageCTE,
on: { chat, latestMessage in
chat[Column("id")] == latestMessage[Column("chatID")]
})
.order(Column("date").desc)
```
The final request can now be defined:
```swift
// WITH latestMessage AS
// (SELECT *, MAX(date) FROM message GROUP BY chatID)
// SELECT chat.*, latestMessage.*
// FROM chat
// LEFT JOIN latestMessage ON chat.id = latestMessage.chatID
// ORDER BY latestMessage.date DESC
let request = Chat
.with(latestMessageCTE)
.including(optional: latestMessage)
.asRequest(of: ChatInfo.self)
```
And we can fetch the data that feeds our application screen:
```swift
let chatInfos: [ChatInfos] = try dbQueue.read { db in
try request.fetchAll(db)
}
```
> :bulb: **Tip**: the joining methods are generally type-safe: they won't allow you to join apples to oranges. This works when associations have a *precise* type. In this context, anonymous `CommonTableExpression` CTEs can work against type safety. When you want to define associations between several CTEs, and make sure the compiler will notice wrong uses of those associations, tag your common table expressions with an explicit type: `CommonTableExpression<SomeType>`.
>
> To do so, you can use an existing record type, or an ad-hoc enum. For example:
>
> ```swift
> enum CTE1 { }
> let cte1 = CommonTableExpression<CTE1>(...)
>
> enum CTE2 { }
> let cte2 = CommonTableExpression<CTE2>(...)
>
> let assoc1 = BaseRecord.association(to: cte1, on: ...) // from BaseRecord to CTE1
> let assoc2 = cte1.association(to: cte2, on: ...) // from CTE1 to CTE2
> let assoc3 = cte2.association(to: FarRecord.self, on: ...) // from CTE2 to FarRecord
>
> // WITH ...
> // SELECT base.* FROM base
> // JOIN cte1 ON ...
> // JOIN cte2 ON ...
> // JOIN far ON ...
> let request = BaseRecord
> .with(cte1).with(cte2)
> .joining(required: assoc1. // OK
> .joining(required: assoc2. // OK
> .joining(required: assoc3))) // OK
>
> // Compiler error
> let request = BaseRecord
> .joining(required: assoc2) // Not OK
> .joining(required: assoc3) // Not OK
> ```
[query interface request]: ../README.md#requests
[query interface requests]: ../README.md#requests
[SQLRequest]: ../README.md#custom-requests
[SQLiteral]: SQLInterpolation.md
[SQL Interpolation]: SQLInterpolation.md
[associations]: AssociationsBasics.md
[Joining And Prefetching Associated Records]: AssociationsBasics.md#joining-and-prefetching-associated-records
[Define Common Table Expressions]: #define-common-table-expressions
[Embed Common Table Expressions in Requests]: #embed-common-table-expressions-in-requests
[Fetch Values From Common Table Expressions]: #fetch-values-from-common-table-expressions
[Associations to Common Table Expressions]: #associations-to-common-table-expressions
[TableRecord]: ../README.md#tablerecord-protocol
[TableAlias]: AssociationsBasics.md#table-aliases
@@ -0,0 +1,4 @@
:twisted_rightwards_arrows: Concurrency
=======================================
This guide [has moved](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/concurrency).
@@ -0,0 +1,136 @@
Custom SQLite Builds
====================
By default, GRDB uses the version of SQLite that ships with the target operating system.
**You can build GRDB with a custom build of [SQLite 3.44.0](https://www.sqlite.org/changes.html).**
A custom SQLite build can activate extra SQLite features, and extra GRDB features as well, such as support for the [FTS5 full-text search engine](../../../#full-text-search), and [SQLite Pre-Update Hooks](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/transactionobserver).
GRDB builds SQLite with [swiftlyfalling/SQLiteLib](https://github.com/swiftlyfalling/SQLiteLib), which uses the same SQLite configuration as the one used by Apple in its operating systems, and lets you add extra compilation options that leverage the features you need.
**To install GRDB with a custom SQLite build:**
1. Clone the GRDB git repository, checkout the latest tagged version:
```sh
cd [GRDB directory]
git checkout [latest tag]
git submodule update --init SQLiteCustom/src
```
2. Choose your [extra compilation options](https://www.sqlite.org/compile.html). For example, `SQLITE_ENABLE_FTS5`, `SQLITE_ENABLE_PREUPDATE_HOOK`.
It is recommended that you enable the `SQLITE_ENABLE_SNAPSHOT` option. It allows GRDB to optimize [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation) when you use a [Database Pool](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasepool).
3. Create a folder named `GRDBCustomSQLite` somewhere in your project directory.
4. Create four files in the `GRDBCustomSQLite` folder:
- `SQLiteLib-USER.xcconfig`: this file sets the extra SQLite compilation flags.
```xcconfig
// As many -D options as there are custom SQLite compilation options
// Note: there is no space between -D and the option name.
CUSTOM_SQLLIBRARY_CFLAGS = -DSQLITE_ENABLE_SNAPSHOT -DSQLITE_ENABLE_FTS5
```
- `GRDBCustomSQLite-USER.xcconfig`: this file lets GRDB know about extra compilation flags, and enables extra GRDB APIs.
```xcconfig
// As many -D options as there are custom SQLite compilation options
// Note: there is one space between -D and the option name.
CUSTOM_OTHER_SWIFT_FLAGS = -D SQLITE_ENABLE_SNAPSHOT -D SQLITE_ENABLE_FTS5
```
- `GRDBCustomSQLite-USER.h`: this file lets your application know about extra compilation flags.
```c
// As many #define as there are custom SQLite compilation options
#define SQLITE_ENABLE_SNAPSHOT
#define SQLITE_ENABLE_FTS5
```
- `GRDBCustomSQLite-INSTALL.sh`: this file installs the three other files.
```sh
# License: MIT License
# https://github.com/swiftlyfalling/SQLiteLib/blob/master/LICENSE
#
#######################################################
# PROJECT PATHS
# !! MODIFY THESE TO MATCH YOUR PROJECT HIERARCHY !!
#######################################################
# The path to the folder containing GRDBCustom.xcodeproj:
GRDB_SOURCE_PATH="${PROJECT_DIR}/GRDB"
# The path to your custom "SQLiteLib-USER.xcconfig":
SQLITELIB_XCCONFIG_USER_PATH="${PROJECT_DIR}/GRDBCustomSQLite/SQLiteLib-USER.xcconfig"
# The path to your custom "GRDBCustomSQLite-USER.xcconfig":
CUSTOMSQLITE_XCCONFIG_USER_PATH="${PROJECT_DIR}/GRDBCustomSQLite/GRDBCustomSQLite-USER.xcconfig"
# The path to your custom "GRDBCustomSQLite-USER.h":
CUSTOMSQLITE_H_USER_PATH="${PROJECT_DIR}/GRDBCustomSQLite/GRDBCustomSQLite-USER.h"
#######################################################
#
#######################################################
if [ ! -d "$GRDB_SOURCE_PATH" ];
then
echo "error: Path to GRDB source (GRDB_SOURCE_PATH) missing/incorrect: $GRDB_SOURCE_PATH"
exit 1
fi
SyncFileChanges () {
SOURCE=$1
DESTINATIONPATH=$2
DESTINATIONFILENAME=$3
DESTINATION="${DESTINATIONPATH}/${DESTINATIONFILENAME}"
if [ ! -f "$SOURCE" ];
then
echo "error: Source file missing: $SOURCE"
exit 1
fi
rsync -a "$SOURCE" "$DESTINATION"
}
SyncFileChanges $SQLITELIB_XCCONFIG_USER_PATH "${GRDB_SOURCE_PATH}/SQLiteCustom/src" "SQLiteLib-USER.xcconfig"
SyncFileChanges $CUSTOMSQLITE_XCCONFIG_USER_PATH "${GRDB_SOURCE_PATH}/SQLiteCustom" "GRDBCustomSQLite-USER.xcconfig"
SyncFileChanges $CUSTOMSQLITE_H_USER_PATH "${GRDB_SOURCE_PATH}/SQLiteCustom" "GRDBCustomSQLite-USER.h"
echo "Finished syncing"
```
Modify the top of `GRDBCustomSQLite-INSTALL.sh` file so that it contains correct paths.
5. Embed the `GRDBCustom.xcodeproj` project in your own project.
6. Add the `GRDBCustom` target in the **Target Dependencies** section of the **Build Phases** tab of your **application target**.
7. Add the `GRDBCustom.framework` from the targeted platform to the **Embedded Binaries** section of the **General** tab of your **application target**.
8. Add a Run Script phase for your target in the **Pre-actions** section of the **Build** tab of your **application scheme**:
```sh
source "${PROJECT_DIR}/GRDBCustomSQLite/GRDBCustomSQLite-INSTALL.sh"
```
The path should be the path to your `GRDBCustomSQLite-INSTALL.sh` file.
Select your application target in the "Provide build settings from" menu.
9. Check the "Shared" checkbox of your application scheme (this lets you commit the pre-action in your Version Control System).
Now you can use GRDB with your custom SQLite build:
```swift
import GRDB
let dbQueue = try DatabaseQueue(...)
```
@@ -0,0 +1,611 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */; };
56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */; };
56519DCA274FC8E900ED16D8 /* GRDBQuery in Frameworks */ = {isa = PBXBuildFile; productRef = 56519DC9274FC8E900ED16D8 /* GRDBQuery */; };
5671723A261B23C800423B6F /* PlayerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717239261B23C800423B6F /* PlayerList.swift */; };
56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717251261B334D00423B6F /* PlayerRequestTests.swift */; };
567C3E1A2520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */; };
567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */; };
567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */; };
567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E532520B75C0011F6E9 /* Player.swift */; };
567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E542520B75C0011F6E9 /* Persistence.swift */; };
567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */; };
567C3E622520B75D0011F6E9 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5A2520B75C0011F6E9 /* AppView.swift */; };
567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */; };
567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */; };
567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E652520B7880011F6E9 /* AppDatabase.swift */; };
567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E752520BB650011F6E9 /* Localizable.stringsdict */; };
567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */; };
56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */; };
56F8A13527359A5A0011ACBE /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56F8A13427359A5A0011ACBE /* GRDB */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 567C3E0E2520B6DE0011F6E9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 567C3E152520B6DE0011F6E9;
remoteInfo = GRDBAsyncDemo;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
567C3E502520B70E0011F6E9 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBAsyncDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
56026C9C25B8A7D000D1DF3F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = "<group>"; };
56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = "<group>"; };
56717239261B23C800423B6F /* PlayerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerList.swift; sourceTree = "<group>"; };
56717251261B334D00423B6F /* PlayerRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequestTests.swift; sourceTree = "<group>"; };
567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBAsyncDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBAsyncDemoApp.swift; sourceTree = "<group>"; };
567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
567C3E222520B6DF0011F6E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
567C3E532520B75C0011F6E9 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
567C3E542520B75C0011F6E9 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
567C3E592520B75C0011F6E9 /* PlayerFormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerFormView.swift; sourceTree = "<group>"; };
567C3E5A2520B75C0011F6E9 /* AppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerCreationView.swift; sourceTree = "<group>"; };
567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionView.swift; sourceTree = "<group>"; };
567C3E652520B7880011F6E9 /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = "<group>"; };
567C3E762520BB650011F6E9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
567C3E782520BB650011F6E9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
56B6D1082619EC1B003CC455 /* PlayerRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequest.swift; sourceTree = "<group>"; };
56F8A12E27359A350011ACBE /* GRDB.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GRDB.swift; path = ../../..; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
56026C9525B8A7D000D1DF3F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
567C3E132520B6DE0011F6E9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
56519DCA274FC8E900ED16D8 /* GRDBQuery in Frameworks */,
56F8A13527359A5A0011ACBE /* GRDB in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
56026C9925B8A7D000D1DF3F /* GRDBAsyncDemoTests */ = {
isa = PBXGroup;
children = (
56026C9C25B8A7D000D1DF3F /* Info.plist */,
56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */,
56717251261B334D00423B6F /* PlayerRequestTests.swift */,
56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */,
);
path = GRDBAsyncDemoTests;
sourceTree = "<group>";
};
56185BC125B8047D00B9C30F /* Resources */ = {
isa = PBXGroup;
children = (
567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */,
567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */,
567C3E752520BB650011F6E9 /* Localizable.stringsdict */,
);
path = Resources;
sourceTree = "<group>";
};
567C3E0D2520B6DE0011F6E9 = {
isa = PBXGroup;
children = (
567C3E182520B6DE0011F6E9 /* GRDBAsyncDemo */,
56026C9925B8A7D000D1DF3F /* GRDBAsyncDemoTests */,
567C3E172520B6DE0011F6E9 /* Products */,
567C3E4D2520B70E0011F6E9 /* Frameworks */,
56F8A12E27359A350011ACBE /* GRDB.swift */,
);
sourceTree = "<group>";
};
567C3E172520B6DE0011F6E9 /* Products */ = {
isa = PBXGroup;
children = (
567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */,
56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
567C3E182520B6DE0011F6E9 /* GRDBAsyncDemo */ = {
isa = PBXGroup;
children = (
567C3E222520B6DF0011F6E9 /* Info.plist */,
567C3E652520B7880011F6E9 /* AppDatabase.swift */,
567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */,
567C3E542520B75C0011F6E9 /* Persistence.swift */,
567C3E532520B75C0011F6E9 /* Player.swift */,
56B6D1082619EC1B003CC455 /* PlayerRequest.swift */,
567C3E1F2520B6DF0011F6E9 /* Preview Content */,
56185BC125B8047D00B9C30F /* Resources */,
567C3E582520B75C0011F6E9 /* Views */,
);
path = GRDBAsyncDemo;
sourceTree = "<group>";
};
567C3E1F2520B6DF0011F6E9 /* Preview Content */ = {
isa = PBXGroup;
children = (
567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
567C3E4D2520B70E0011F6E9 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
567C3E582520B75C0011F6E9 /* Views */ = {
isa = PBXGroup;
children = (
567C3E5A2520B75C0011F6E9 /* AppView.swift */,
567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */,
567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */,
567C3E592520B75C0011F6E9 /* PlayerFormView.swift */,
56717239261B23C800423B6F /* PlayerList.swift */,
);
path = Views;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
56026C9725B8A7D000D1DF3F /* GRDBAsyncDemoTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBAsyncDemoTests" */;
buildPhases = (
56026C9425B8A7D000D1DF3F /* Sources */,
56026C9525B8A7D000D1DF3F /* Frameworks */,
56026C9625B8A7D000D1DF3F /* Resources */,
);
buildRules = (
);
dependencies = (
56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */,
);
name = GRDBAsyncDemoTests;
productName = GRDBAsyncDemoTests;
productReference = 56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */ = {
isa = PBXNativeTarget;
buildConfigurationList = 567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBAsyncDemo" */;
buildPhases = (
567C3E122520B6DE0011F6E9 /* Sources */,
567C3E132520B6DE0011F6E9 /* Frameworks */,
567C3E142520B6DE0011F6E9 /* Resources */,
567C3E502520B70E0011F6E9 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
56F8A13127359A540011ACBE /* PBXTargetDependency */,
);
name = GRDBAsyncDemo;
packageProductDependencies = (
56F8A13427359A5A0011ACBE /* GRDB */,
56519DC9274FC8E900ED16D8 /* GRDBQuery */,
);
productName = GRBCombineDemo;
productReference = 567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
567C3E0E2520B6DE0011F6E9 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1200;
TargetAttributes = {
56026C9725B8A7D000D1DF3F = {
CreatedOnToolsVersion = 12.3;
TestTargetID = 567C3E152520B6DE0011F6E9;
};
567C3E152520B6DE0011F6E9 = {
CreatedOnToolsVersion = 12.0;
};
};
};
buildConfigurationList = 567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBAsyncDemo" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 567C3E0D2520B6DE0011F6E9;
packageReferences = (
56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */,
);
productRefGroup = 567C3E172520B6DE0011F6E9 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */,
56026C9725B8A7D000D1DF3F /* GRDBAsyncDemoTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
56026C9625B8A7D000D1DF3F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
567C3E142520B6DE0011F6E9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */,
567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */,
567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */,
567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
56026C9425B8A7D000D1DF3F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */,
56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */,
56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
567C3E122520B6DE0011F6E9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */,
567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */,
56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */,
5671723A261B23C800423B6F /* PlayerList.swift in Sources */,
567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */,
567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */,
567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */,
567C3E622520B75D0011F6E9 /* AppView.swift in Sources */,
567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */,
567C3E1A2520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */;
targetProxy = 56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */;
};
56F8A13127359A540011ACBE /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 56F8A13027359A540011ACBE /* GRDB */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
567C3E752520BB650011F6E9 /* Localizable.stringsdict */ = {
isa = PBXVariantGroup;
children = (
567C3E762520BB650011F6E9 /* en */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
};
567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
567C3E782520BB650011F6E9 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
56026C9F25B8A7D000D1DF3F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = GRDBAsyncDemoTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemoTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBAsyncDemo.app/GRDBAsyncDemo";
};
name = Debug;
};
56026CA025B8A7D000D1DF3F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = GRDBAsyncDemoTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemoTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBAsyncDemo.app/GRDBAsyncDemo";
};
name = Release;
};
567C3E232520B6DF0011F6E9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_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;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
567C3E242520B6DF0011F6E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_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;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
567C3E262520B6DF0011F6E9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"GRDBAsyncDemo/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = GRDBAsyncDemo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
567C3E272520B6DF0011F6E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"GRDBAsyncDemo/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = GRDBAsyncDemo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBAsyncDemoTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
56026C9F25B8A7D000D1DF3F /* Debug */,
56026CA025B8A7D000D1DF3F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBAsyncDemo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
567C3E232520B6DF0011F6E9 /* Debug */,
567C3E242520B6DF0011F6E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBAsyncDemo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
567C3E262520B6DF0011F6E9 /* Debug */,
567C3E272520B6DF0011F6E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/groue/GRDBQuery";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.6.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
56519DC9274FC8E900ED16D8 /* GRDBQuery */ = {
isa = XCSwiftPackageProductDependency;
package = 56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */;
productName = GRDBQuery;
};
56F8A13027359A540011ACBE /* GRDB */ = {
isa = XCSwiftPackageProductDependency;
productName = GRDB;
};
56F8A13427359A5A0011ACBE /* GRDB */ = {
isa = XCSwiftPackageProductDependency;
productName = GRDB;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 567C3E0E2520B6DE0011F6E9 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "grdbquery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/groue/GRDBQuery",
"state" : {
"revision" : "a6c46dd38ecf11a5c37732870dc03a384d582fba",
"version" : "0.9.0"
}
}
],
"version" : 2
}
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
BuildableName = "GRDBAsyncDemo.app"
BlueprintName = "GRDBAsyncDemo"
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "56026C9725B8A7D000D1DF3F"
BuildableName = "GRDBAsyncDemoTests.xctest"
BlueprintName = "GRDBAsyncDemoTests"
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
BuildableName = "GRDBAsyncDemo.app"
BlueprintName = "GRDBAsyncDemo"
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "SQL_TRACE"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
BuildableName = "GRDBAsyncDemo.app"
BlueprintName = "GRDBAsyncDemo"
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,235 @@
import Foundation
import GRDB
import os.log
/// A database of players.
///
/// You create an `AppDatabase` with a connection to an SQLite database
/// (see <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>).
///
/// Create those connections with a configuration returned from
/// `AppDatabase/makeConfiguration(_:)`.
///
/// For example:
///
/// ```swift
/// // Create an in-memory AppDatabase
/// let config = AppDatabase.makeConfiguration()
/// let dbQueue = try DatabaseQueue(configuration: config)
/// let appDatabase = try AppDatabase(dbQueue)
/// ```
struct AppDatabase {
/// Creates an `AppDatabase`, and makes sure the database schema
/// is ready.
///
/// - important: Create the `DatabaseWriter` with a configuration
/// returned by ``makeConfiguration(_:)``.
init(_ dbWriter: any DatabaseWriter) throws {
self.dbWriter = dbWriter
try migrator.migrate(dbWriter)
}
/// Provides access to the database.
///
/// Application can use a `DatabasePool`, while SwiftUI previews and tests
/// can use a fast in-memory `DatabaseQueue`.
///
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>
private let dbWriter: any DatabaseWriter
}
// MARK: - Database Configuration
extension AppDatabase {
private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL")
/// Returns a database configuration suited for `PlayerRepository`.
///
/// SQL statements are logged if the `SQL_TRACE` environment variable
/// is set.
///
/// - parameter base: A base configuration.
public static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration {
var config = base
// An opportunity to add required custom SQL functions or
// collations, if needed:
// config.prepareDatabase { db in
// db.add(function: ...)
// }
// Log SQL statements if the `SQL_TRACE` environment variable is set.
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/database/trace(options:_:)>
if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil {
config.prepareDatabase { db in
db.trace {
// It's ok to log statements publicly. Sensitive
// information (statement arguments) are not logged
// unless config.publicStatementArguments is set
// (see below).
os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0))
}
}
}
#if DEBUG
// Protect sensitive information by enabling verbose debugging in
// DEBUG builds only.
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/configuration/publicstatementarguments>
config.publicStatementArguments = true
#endif
return config
}
}
// MARK: - Database Migrations
extension AppDatabase {
/// The DatabaseMigrator that defines the database schema.
///
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations>
private var migrator: DatabaseMigrator {
var migrator = DatabaseMigrator()
#if DEBUG
// Speed up development by nuking the database when migrations change
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations>
migrator.eraseDatabaseOnSchemaChange = true
#endif
migrator.registerMigration("createPlayer") { db in
// Create a table
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseschema>
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer).notNull()
}
}
// Migrations for future application versions will be inserted here:
// migrator.registerMigration(...) { db in
// ...
// }
return migrator
}
}
// MARK: - Database Access: Writes
// The write methods execute invariant-preserving database transactions.
extension AppDatabase {
/// A validation error that prevents some players from being saved into
/// the database.
enum ValidationError: LocalizedError {
case missingName
var errorDescription: String? {
switch self {
case .missingName:
return "Please provide a name"
}
}
}
/// Saves (inserts or updates) a player. When the method returns, the
/// player is present in the database, and its id is not nil.
func savePlayer(_ player: inout Player) async throws {
if player.name.isEmpty {
throw ValidationError.missingName
}
player = try await dbWriter.write { [player] db in
try player.saved(db)
}
}
/// Delete the specified players
func deletePlayers(ids: [Int64]) async throws {
try await dbWriter.write { db in
_ = try Player.deleteAll(db, ids: ids)
}
}
/// Delete all players
func deleteAllPlayers() async throws {
try await dbWriter.write { db in
_ = try Player.deleteAll(db)
}
}
/// Refresh all players (by performing some random changes, for demo purpose).
func refreshPlayers() async throws {
try await dbWriter.write { db in
if try Player.all().isEmpty(db) {
// When database is empty, insert new random players
try createRandomPlayers(db)
} else {
// Insert a player
if Bool.random() {
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
}
// Delete a random player
if Bool.random() {
try Player.order(sql: "RANDOM()").limit(1).deleteAll(db)
}
// Update some players
for var player in try Player.fetchAll(db) where Bool.random() {
try player.updateChanges(db) {
$0.score = Player.randomScore()
}
}
}
}
}
/// Create random players if the database is empty.
func createRandomPlayersIfEmpty() throws {
try dbWriter.write { db in
if try Player.all().isEmpty(db) {
try createRandomPlayers(db)
}
}
}
private static let uiTestPlayers = [
Player(id: nil, name: "Arthur", score: 5),
Player(id: nil, name: "Barbara", score: 6),
Player(id: nil, name: "Craig", score: 8),
Player(id: nil, name: "David", score: 4),
Player(id: nil, name: "Elena", score: 1),
Player(id: nil, name: "Frederik", score: 2),
Player(id: nil, name: "Gilbert", score: 7),
Player(id: nil, name: "Henriette", score: 3)]
func createPlayersForUITests() throws {
try dbWriter.write { db in
try AppDatabase.uiTestPlayers.forEach { player in
_ = try player.inserted(db) // insert but ignore inserted id
}
}
}
/// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`.
private func createRandomPlayers(_ db: Database) throws {
for _ in 0..<8 {
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
}
}
}
// MARK: - Database Access: Reads
// This demo app does not provide any specific reading method, and instead
// gives an unrestricted read-only access to the rest of the application.
// In your app, you are free to choose another path, and define focused
// reading methods.
extension AppDatabase {
/// Provides a read-only access to the database
var reader: DatabaseReader {
dbWriter
}
}
@@ -0,0 +1,32 @@
import GRDBQuery
import SwiftUI
@main
struct GRDBAsyncDemoApp: App {
var body: some Scene {
WindowGroup {
AppView().appDatabase(.shared)
}
}
}
// MARK: - Give SwiftUI access to the database
private struct AppDatabaseKey: EnvironmentKey {
static var defaultValue: AppDatabase { .empty() }
}
extension EnvironmentValues {
var appDatabase: AppDatabase {
get { self[AppDatabaseKey.self] }
set { self[AppDatabaseKey.self] = newValue }
}
}
extension View {
func appDatabase(_ appDatabase: AppDatabase) -> some View {
self
.environment(\.appDatabase, appDatabase)
.databaseContext(.readOnly { appDatabase.reader })
}
}
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -0,0 +1,77 @@
import Foundation
import GRDB
extension AppDatabase {
/// The database for the application
static let shared = makeShared()
private static func makeShared() -> AppDatabase {
do {
// Apply recommendations from
// <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>
//
// Create the "Application Support/Database" directory if needed
let fileManager = FileManager.default
let appSupportURL = try fileManager.url(
for: .applicationSupportDirectory, in: .userDomainMask,
appropriateFor: nil, create: true)
let directoryURL = appSupportURL.appendingPathComponent("Database", isDirectory: true)
// Support for tests: delete the database if requested
if CommandLine.arguments.contains("-reset") {
try? fileManager.removeItem(at: directoryURL)
}
// Create the database folder if needed
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
// Open or create the database
let databaseURL = directoryURL.appendingPathComponent("db.sqlite")
NSLog("Database stored at \(databaseURL.path)")
let dbPool = try DatabasePool(
path: databaseURL.path,
// Use default AppDatabase configuration
configuration: AppDatabase.makeConfiguration())
// Create the AppDatabase
let appDatabase = try AppDatabase(dbPool)
// Prepare the database with test fixtures if requested
if CommandLine.arguments.contains("-fixedTestData") {
try appDatabase.createPlayersForUITests()
} else {
// Otherwise, populate the database if it is empty, for better
// demo purpose.
try appDatabase.createRandomPlayersIfEmpty()
}
return appDatabase
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
//
// Typical reasons for an error here include:
// * The parent directory cannot be created, or disallows writing.
// * The database is not accessible, due to permissions or data protection when the device is locked.
// * The device is out of space.
// * The database could not be migrated to its latest schema version.
// Check the error message to determine what the actual problem was.
fatalError("Unresolved error \(error)")
}
}
/// Creates an empty database for SwiftUI previews
static func empty() -> AppDatabase {
// Connect to an in-memory database
// See https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections
let dbQueue = try! DatabaseQueue(configuration: AppDatabase.makeConfiguration())
return try! AppDatabase(dbQueue)
}
/// Creates a database full of random players for SwiftUI previews
static func random() -> AppDatabase {
let appDatabase = empty()
try! appDatabase.createRandomPlayersIfEmpty()
return appDatabase
}
}
@@ -0,0 +1,105 @@
import GRDB
/// The Player struct.
///
/// Identifiable conformance supports SwiftUI list animations, and type-safe
/// GRDB primary key methods.
/// Equatable conformance supports tests.
struct Player: Identifiable, Equatable {
/// The player id.
///
/// Int64 is the recommended type for auto-incremented database ids.
/// Use nil for players that are not inserted yet in the database.
var id: Int64?
var name: String
var score: Int
}
extension Player {
private static let names = [
"Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David",
"Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette",
"Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl",
"Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole",
"Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul",
"Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain",
"Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann",
"Zazie", "Zoé"]
/// Creates a new player with empty name and zero score
static func new() -> Player {
Player(id: nil, name: "", score: 0)
}
/// Creates a new player with random name and random score
static func makeRandom() -> Player {
Player(id: nil, name: randomName(), score: randomScore())
}
/// Returns a random name
static func randomName() -> String {
names.randomElement()!
}
/// Returns a random score
static func randomScore() -> Int {
10 * Int.random(in: 0...100)
}
}
// MARK: - Persistence
/// Make Player a Codable Record.
///
/// See <https://github.com/groue/GRDB.swift/blob/master/README.md#records>
extension Player: Codable, FetchableRecord, MutablePersistableRecord {
// Define database columns from CodingKeys
fileprivate enum Columns {
static let name = Column(CodingKeys.name)
static let score = Column(CodingKeys.score)
}
/// Updates a player id after it has been inserted in the database.
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
// MARK: - Player Database Requests
/// Define some player requests used by the application.
///
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices>
extension DerivableRequest<Player> {
/// A request of players ordered by name.
///
/// For example:
///
/// let players: [Player] = try dbWriter.read { db in
/// try Player.all().orderedByName().fetchAll(db)
/// }
func orderedByName() -> Self {
// Sort by name in a localized case insensitive fashion
// See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison
order(Player.Columns.name.collating(.localizedCaseInsensitiveCompare))
}
/// A request of players ordered by score.
///
/// For example:
///
/// let players: [Player] = try dbWriter.read { db in
/// try Player.all().orderedByScore().fetchAll(db)
/// }
/// let bestPlayer: Player? = try dbWriter.read { db in
/// try Player.all().orderedByScore().fetchOne(db)
/// }
func orderedByScore() -> Self {
// Sort by descending score, and then by name, in a
// localized case insensitive fashion
// See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison
order(
Player.Columns.score.desc,
Player.Columns.name.collating(.localizedCaseInsensitiveCompare))
}
}
@@ -0,0 +1,35 @@
import GRDB
import GRDBQuery
/// A player request can be used with the `@Query` property wrapper in order to
/// feed a view with a list of players.
///
/// For example:
///
/// struct MyView: View {
/// @Query(PlayerRequest(ordering: .byName)) private var players: [Player]
///
/// var body: some View {
/// List(players) { player in ... )
/// }
/// }
struct PlayerRequest: ValueObservationQueryable {
enum Ordering {
case byScore
case byName
}
static var defaultValue: [Player] { [] }
/// The ordering used by the player request.
var ordering: Ordering
func fetch(_ db: Database) throws -> [Player] {
switch ordering {
case .byScore:
return try Player.all().orderedByScore().fetchAll(db)
case .byName:
return try Player.all().orderedByName().fetchAll(db)
}
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,116 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "icon_20pt@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "icon_20pt@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon_29pt@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon_29pt@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "icon_40pt@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "icon_40pt@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon_60pt@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon_60pt@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "icon_20pt.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "icon_20pt@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "icon_29pt.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "icon_29pt@2x-1.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "icon_40pt.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "icon_40pt@2x-1.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon_76pt.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon_76pt@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "icon_83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "LaunchIcon.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchIcon" translatesAutoresizingMaskIntoConstraints="NO" id="Sgi-te-PKu">
<rect key="frame" x="123.5" y="334.5" width="167" height="237"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="Sgi-te-PKu" firstAttribute="centerY" secondItem="6Tk-OE-BBY" secondAttribute="centerY" id="KT7-xd-gV4"/>
<constraint firstItem="Sgi-te-PKu" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="feL-Vs-SeN"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchIcon" width="167" height="237"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>%lld Players</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@VARIABLE@</string>
<key>VARIABLE</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>zero</key>
<string>No Player</string>
<key>one</key>
<string>1 Player</string>
<key>other</key>
<string>%lld Players</string>
</dict>
</dict>
<key>%lld points</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@VARIABLE@</string>
<key>VARIABLE</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>zero</key>
<string>0 point</string>
<key>one</key>
<string>1 point</string>
<key>other</key>
<string>%lld points</string>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,154 @@
import GRDBQuery
import SwiftUI
/// The main application view
struct AppView: View {
/// Write access to the database
@Environment(\.appDatabase) private var appDatabase
/// The `players` property is automatically updated when the database changes
@Query(PlayerRequest(ordering: .byScore)) private var players: [Player]
/// We'll need to leave edit mode in several occasions.
@State private var editMode = EditMode.inactive
/// Tracks the presentation of the player creation sheet.
@State private var newPlayerIsPresented = false
// If you want to define the query on initialization, you will prefer:
//
// @Query<PlayerRequest> private var players: [Player]
//
// init(initialOrdering: PlayerRequest.Ordering) {
// _players = Query(PlayerRequest(ordering: initialOrdering))
// }
var body: some View {
NavigationView {
PlayerList(players: players)
.navigationBarTitle(Text("\(players.count) Players"))
.navigationBarItems(
leading: HStack {
EditButton()
newPlayerButton
},
trailing: ToggleOrderingButton(
ordering: $players.ordering,
willChange: {
// onChange(of: $players.wrappedValue.ordering)
// is not able to leave the editing mode during
// the animation of the list content.
// Workaround: stop editing before the ordering
// is changed, and the list content is updated.
stopEditing()
}))
.toolbar { toolbarContent }
.onChange(of: players) {
if players.isEmpty {
stopEditing()
}
}
.environment(\.editMode, $editMode)
}
}
private var toolbarContent: some ToolbarContent {
ToolbarItemGroup(placement: .bottomBar) {
Button {
// Don't stopEditing() here because this is
// performed `onChange(of: players)`
Task {
try? await appDatabase.deleteAllPlayers()
}
} label: {
Image(systemName: "trash").imageScale(.large)
}
Spacer()
Button {
stopEditing()
Task {
try? await appDatabase.refreshPlayers()
}
} label: {
Image(systemName: "arrow.clockwise").imageScale(.large)
}
Spacer()
Button {
stopEditing()
// Perform 50 refreshes in parallel
Task {
try? await withThrowingTaskGroup(of: Void.self) { group in
for _ in 0..<50 {
_ = group.addTaskUnlessCancelled {
try await appDatabase.refreshPlayers()
}
}
try await group.waitForAll()
}
}
} label: {
Image(systemName: "tornado").imageScale(.large)
}
}
}
/// The button that presents the player creation sheet.
private var newPlayerButton: some View {
Button {
stopEditing()
newPlayerIsPresented = true
} label: {
Image(systemName: "plus")
}
.accessibility(label: Text("New Player"))
.sheet(isPresented: $newPlayerIsPresented) {
PlayerCreationView()
}
}
private func stopEditing() {
withAnimation {
editMode = .inactive
}
}
}
private struct ToggleOrderingButton: View {
@Binding var ordering: PlayerRequest.Ordering
let willChange: () -> Void
var body: some View {
switch ordering {
case .byName:
Button {
willChange()
ordering = .byScore
} label: {
Label("Name", systemImage: "arrowtriangle.up.fill").labelStyle(.titleAndIcon)
}
case .byScore:
Button {
willChange()
ordering = .byName
} label: {
Label("Score", systemImage: "arrowtriangle.down.fill").labelStyle(.titleAndIcon)
}
}
}
}
// MARK: - Previews
#Preview("Empty") {
// Preview the default, empty database
AppView()
}
#Preview("Populated") {
// Preview a database of random players
AppView().appDatabase(.random())
}
@@ -0,0 +1,50 @@
import SwiftUI
/// The view that creates a new player.
struct PlayerCreationView: View {
/// Write access to the database
@Environment(\.appDatabase) private var appDatabase
@Environment(\.dismiss) private var dismiss
@State private var form = PlayerForm(name: "", score: "")
@State private var errorAlertIsPresented = false
@State private var errorAlertTitle = ""
var body: some View {
NavigationView {
PlayerFormView(form: $form)
.alert(
isPresented: $errorAlertIsPresented,
content: { Alert(title: Text(errorAlertTitle)) })
.navigationBarTitle("New Player")
.navigationBarItems(
leading: Button(role: .cancel) {
dismiss()
} label: {
Text("Cancel")
},
trailing: Button {
Task { await save() }
} label: {
Text("Save")
})
}
}
private func save() async {
do {
var player = Player(id: nil, name: "", score: 0)
form.apply(to: &player)
try await appDatabase.savePlayer(&player)
dismiss()
} catch {
errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
errorAlertIsPresented = true
}
}
}
// MARK: - Previews
#Preview {
PlayerCreationView()
}
@@ -0,0 +1,40 @@
import SwiftUI
/// The view that edits an existing player.
struct PlayerEditionView: View {
/// Write access to the database
@Environment(\.appDatabase) private var appDatabase
@Environment(\.isPresented) private var isPresented
private let player: Player
@State private var form: PlayerForm
init(player: Player) {
self.player = player
self.form = PlayerForm(player)
}
var body: some View {
PlayerFormView(form: $form)
.onChange(of: isPresented) {
// Save when back button is pressed
if !isPresented {
Task {
var savedPlayer = player
form.apply(to: &savedPlayer)
// Ignore error because I don't know how to cancel the
// back button and present the error
try? await appDatabase.savePlayer(&savedPlayer)
}
}
}
}
}
// MARK: - Previews
#Preview {
NavigationView {
PlayerEditionView(player: Player.makeRandom())
.navigationBarTitle("Player Edition")
}
}
@@ -0,0 +1,48 @@
import SwiftUI
/// The Player editing form, embedded in both
/// `PlayerCreationView` and `PlayerEditionView`.
struct PlayerFormView: View {
@Binding var form: PlayerForm
var body: some View {
List {
TextField("Name", text: $form.name)
.accessibility(label: Text("Player Name"))
TextField("Score", text: $form.score).keyboardType(.numberPad)
.accessibility(label: Text("Player Score"))
}
.listStyle(InsetGroupedListStyle())
}
}
struct PlayerForm {
var name: String
var score: String
}
extension PlayerForm {
init(_ player: Player) {
self.name = player.name
self.score = "\(player.score)"
}
func apply(to player: inout Player) {
player.name = name
player.score = Int(score) ?? 0
}
}
// MARK: - Previews
#Preview("Empty") {
PlayerFormView(form: .constant(PlayerForm(
name: "",
score: "")))
}
#Preview("Prefilled") {
PlayerFormView(form: .constant(PlayerForm(
name: Player.randomName(),
score: "\(Player.randomScore())")))
}
@@ -0,0 +1,59 @@
import SwiftUI
struct PlayerList: View {
/// Write access to the database
@Environment(\.appDatabase) private var appDatabase
/// The players in the list
var players: [Player]
var body: some View {
List {
ForEach(players) { player in
NavigationLink(destination: editionView(for: player)) {
PlayerRow(player: player)
// Don't animate player update
.animation(nil, value: player)
}
}
.onDelete { offsets in
let playerIds = offsets.compactMap { players[$0].id }
Task {
try? await appDatabase.deletePlayers(ids: playerIds)
}
}
}
// Animate list updates
.animation(.default, value: players)
.listStyle(.plain)
}
/// The view that edits a player in the list.
private func editionView(for player: Player) -> some View {
PlayerEditionView(player: player).navigationBarTitle(player.name)
}
}
private struct PlayerRow: View {
var player: Player
var body: some View {
HStack {
Text(player.name)
Spacer()
Text("\(player.score) points").foregroundColor(.gray)
}
}
}
// MARK: - Previews
#Preview {
NavigationView {
PlayerList(players: [
Player(id: 1, name: "Arthur", score: 100),
Player(id: 2, name: "Barbara", score: 1000),
])
.navigationTitle("Preview")
}
}
@@ -0,0 +1,144 @@
import XCTest
import GRDB
@testable import GRDBAsyncDemo
class AppDatabaseTests: XCTestCase {
func test_database_schema() throws {
// Given an empty database
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
// When we instantiate an AppDatabase
_ = try AppDatabase(dbQueue)
// Then the player table exists, with id, name & score columns
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"])
}
}
func test_savePlayer_inserts() async throws {
// Given an empty players database
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
// When we save a new player
var player = Player(id: nil, name: "Arthur", score: 100)
try await appDatabase.savePlayer(&player)
// Then the player exists in the database
let playerExists = try await dbQueue.read { [player] in try player.exists($0) }
XCTAssertTrue(playerExists)
}
func test_savePlayer_updates() async throws {
// Given a players database that contains a player
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
var player = try await dbQueue.write { db in
try Player(id: nil, name: "Arthur", score: 100).inserted(db)
}
// When we modify and save the player
player.name = "Barbara"
player.score = 1000
try await appDatabase.savePlayer(&player)
// Then the player has been updated in the database
let fetchedPlayer = try await dbQueue.read { [player] db in
try XCTUnwrap(Player.fetchOne(db, key: player.id))
}
XCTAssertEqual(fetchedPlayer, player)
}
func test_deletePlayers() async throws {
// Given a players database that contains four players
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
let playerIds: [Int64] = try await dbQueue.write { db in
_ = try Player(id: nil, name: "Arthur", score: 100).inserted(db)
_ = try Player(id: nil, name: "Barbara", score: 200).inserted(db)
_ = try Player(id: nil, name: "Craig", score: 150).inserted(db)
_ = try Player(id: nil, name: "David", score: 120).inserted(db)
return try Player.selectPrimaryKey().fetchAll(db)
}
// When we delete two players
let deletedId1 = playerIds[0]
let deletedId2 = playerIds[2]
try await appDatabase.deletePlayers(ids: [deletedId1, deletedId2])
// Then the deleted players no longer exist
try await dbQueue.read { db in
try XCTAssertFalse(Player.exists(db, id: deletedId1))
try XCTAssertFalse(Player.exists(db, id: deletedId2))
}
// Then the database still contains two players
let count = try await dbQueue.read { try Player.fetchCount($0) }
XCTAssertEqual(count, 2)
}
func test_deleteAllPlayers() async throws {
// Given a players database that contains players
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
try await dbQueue.write { db in
_ = try Player(id: nil, name: "Arthur", score: 100).inserted(db)
_ = try Player(id: nil, name: "Barbara", score: 200).inserted(db)
_ = try Player(id: nil, name: "Craig", score: 150).inserted(db)
_ = try Player(id: nil, name: "David", score: 120).inserted(db)
}
// When we delete all players
try await appDatabase.deleteAllPlayers()
// Then the database does not contain any player
let count = try await dbQueue.read { try Player.fetchCount($0) }
XCTAssertEqual(count, 0)
}
func test_refreshPlayers_populates_an_empty_database() async throws {
// Given an empty players database
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
// When we refresh players
try await appDatabase.refreshPlayers()
// Then the database is not empty
let count = try await dbQueue.read { try Player.fetchCount($0) }
XCTAssert(count > 0)
}
func test_createRandomPlayersIfEmpty_populates_an_empty_database() throws {
// Given an empty players database
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
// When we create random players
try appDatabase.createRandomPlayersIfEmpty()
// Then the database is not empty
try XCTAssert(dbQueue.read(Player.fetchCount) > 0)
}
func test_createRandomPlayersIfEmpty_does_not_modify_a_non_empty_database() throws {
// Given a players database that contains one player
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
var player = Player(id: nil, name: "Arthur", score: 100)
try dbQueue.write { db in
try player.insert(db)
}
// When we create random players
try appDatabase.createRandomPlayersIfEmpty()
// Then the database still only contains the original player
let players = try dbQueue.read(Player.fetchAll)
XCTAssertEqual(players, [player])
}
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
@@ -0,0 +1,43 @@
import XCTest
import GRDB
@testable import GRDBAsyncDemo
class PlayerRequestTests: XCTestCase {
func test_PlayerRequest_byName_fetches_well_ordered_players() throws {
// Given a players database that contains two players
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
_ = try AppDatabase(dbQueue)
var player1 = Player(id: nil, name: "Arthur", score: 100)
var player2 = Player(id: nil, name: "Barbara", score: 1000)
try dbQueue.write { db in
try player1.insert(db)
try player2.insert(db)
}
// When we fetch players ordered by name
let playerRequest = PlayerRequest(ordering: .byName)
let players = try dbQueue.read(playerRequest.fetch)
// Then the players are the two players ordered by name
XCTAssertEqual(players, [player1, player2])
}
func test_PlayerRequest_byScore_fetches_well_ordered_players() throws {
// Given a players database that contains two players
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
_ = try AppDatabase(dbQueue)
var player1 = Player(id: nil, name: "Arthur", score: 100)
var player2 = Player(id: nil, name: "Barbara", score: 1000)
try dbQueue.write { db in
try player1.insert(db)
try player2.insert(db)
}
// When we fetch players ordered by score
let playerRequest = PlayerRequest(ordering: .byScore)
let players = try dbQueue.read(playerRequest.fetch)
// Then the players are the two players ordered by score descending
XCTAssertEqual(players, [player2, player1])
}
}
@@ -0,0 +1,108 @@
import XCTest
import GRDB
@testable import GRDBAsyncDemo
class PlayerTests: XCTestCase {
// MARK: - CRUD
// Test that our Player type properly talks to GRDB.
func testInsert() throws {
// Given an empty players database
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
_ = try AppDatabase(dbQueue)
// When we insert a player
var player = Player(id: nil, name: "Arthur", score: 100)
try dbQueue.write { db in
try player.insert(db)
}
// Then the player gets a non-nil id
XCTAssertNotNil(player.id)
}
func testRoundtrip() throws {
// Given an empty players database
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
_ = try AppDatabase(dbQueue)
// When we insert a player and fetch the player with the same id
var insertedPlayer = Player(id: nil, name: "Arthur", score: 100)
let fetchedPlayer: Player? = try dbQueue.write { db in
try insertedPlayer.insert(db)
return try Player.fetchOne(db, key: insertedPlayer.id)
}
// Then the fetched player is equal to the inserted player
XCTAssertEqual(insertedPlayer, fetchedPlayer)
}
// MARK: - Requests
// Test that requests defined on the Player type behave as expected.
func testOrderedByScore() throws {
// Given a players database that contains players with distinct scores
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
_ = try AppDatabase(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: 120)
try dbQueue.write { db in
try player1.insert(db)
try player2.insert(db)
try player3.insert(db)
try player4.insert(db)
}
// When we fetch players ordered by score
let players = try dbQueue.read(Player.all().orderedByScore().fetchAll)
// Then fetched players are ordered by score descending
XCTAssertEqual(players, [player2, player3, player4, player1])
}
func testOrderedByScoreSortsIdenticalScoresByName() throws {
// Given a players database that contains players with common scores
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
_ = try AppDatabase(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: 200)
var player4 = Player(id: 4, name: "David", score: 200)
try dbQueue.write { db in
try player1.insert(db)
try player2.insert(db)
try player3.insert(db)
try player4.insert(db)
}
// When we fetch players ordered by score
let players = try dbQueue.read(Player.all().orderedByScore().fetchAll)
// Then fetched players are ordered by score descending and by name
XCTAssertEqual(players, [player2, player3, player4, player1])
}
func testOrderedByName() throws {
// Given a players database that contains players with distinct names
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
_ = try AppDatabase(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: 120)
try dbQueue.write { db in
try player1.insert(db)
try player2.insert(db)
try player3.insert(db)
try player4.insert(db)
}
// When we fetch players ordered by name
let players = try dbQueue.read(Player.all().orderedByName().fetchAll)
// Then fetched players are ordered by name
XCTAssertEqual(players, [player1, player2, player3, player4])
}
}
@@ -0,0 +1,52 @@
Async/Await + SwiftUI Demo Application
======================================
<img align="right" src="https://github.com/groue/GRDB.swift/raw/master/Documentation/DemoApps/GRDBCombineDemo/Screenshot.png" width="50%">
**This demo application is an Async/Await + SwiftUI application.** For a demo application that uses UIKit, see [GRDBDemoiOS](../GRDBDemoiOS/README.md), and for Combine + SwiftUI, see [GRDBCombineDemo](../GRDBCombineDemo/README.md).
**Requirements**: iOS 15.0+ / Xcode 13.1+
> **Note**: This demo app is not a project template. Do not copy it as a starting point for your application. Instead, create a new project, choose a GRDB [installation method](../../../README.md#installation), and use the demo as an inspiration.
The topics covered in this demo are:
- How to setup a database in an iOS app.
- How to define a simple [Codable Record](../../../README.md#codable-records).
- How to track database changes and animate a SwiftUI List with [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation) Combine publishers.
- How to apply the recommendations of [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices).
- How to perform `async` database accesses.
- How to feed SwiftUI previews with a transient database.
**Files of interest:**
- [GRDBAsyncDemoApp.swift](GRDBAsyncDemo/GRDBAsyncDemoApp.swift)
`GRDBAsyncDemoApp` feeds the app views with a database, through the SwiftUI environment.
- [AppDatabase.swift](GRDBAsyncDemo/AppDatabase.swift)
`AppDatabase` is the type that grants database access. It uses [DatabaseMigrator](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasemigrator) in order to setup the database schema.
- [Persistence.swift](GRDBAsyncDemo/Persistence.swift)
This file instantiates various `AppDatabase` for the various projects needs: one database on disk for the application, and in-memory databases for SwiftUI previews.
- [Player.swift](GRDBAsyncDemo/Player.swift)
`Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records).
- [PlayerRequest.swift](GRDBAsyncDemo/PlayerRequest.swift), [AppView.swift](GRDBAsyncDemo/Views/AppView.swift)
`PlayerRequest` defines the player requests used by the app (sorted by score, or by name).
`PlayerRequest` feeds the `@Query` property wrapper (`@Query`, defined in [GRDBQuery](https://github.com/groue/GRDBQuery), allows SwiftUI views to display up-to-date database content).
`AppView` is the SwiftUI view that uses `@Query` in order to feed its player list.
- [GRDBAsyncDemoTests](GRDBAsyncDemoTests)
- Test the database schema
- Test the `Player` record and its requests
- Test the `PlayerRequest` methods that feed the list of players.
- Test the `AppDatabase` methods that let the app access the database.
Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

@@ -0,0 +1,611 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */; };
56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */; };
56519DC7274FC85600ED16D8 /* GRDBQuery in Frameworks */ = {isa = PBXBuildFile; productRef = 56519DC6274FC85600ED16D8 /* GRDBQuery */; };
5671723A261B23C800423B6F /* PlayerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717239261B23C800423B6F /* PlayerList.swift */; };
56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717251261B334D00423B6F /* PlayerRequestTests.swift */; };
567C3E1A2520B6DE0011F6E9 /* GRDBCombineDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */; };
567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */; };
567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */; };
567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E532520B75C0011F6E9 /* Player.swift */; };
567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E542520B75C0011F6E9 /* Persistence.swift */; };
567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */; };
567C3E622520B75D0011F6E9 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5A2520B75C0011F6E9 /* AppView.swift */; };
567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */; };
567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */; };
567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E652520B7880011F6E9 /* AppDatabase.swift */; };
567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E752520BB650011F6E9 /* Localizable.stringsdict */; };
567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */; };
56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */; };
56F8A1202735989D0011ACBE /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56F8A11F2735989D0011ACBE /* GRDB */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 567C3E0E2520B6DE0011F6E9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 567C3E152520B6DE0011F6E9;
remoteInfo = GRDBCombineDemo;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
567C3E502520B70E0011F6E9 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
56026C9825B8A7D000D1DF3F /* GRDBCombineDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBCombineDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
56026C9C25B8A7D000D1DF3F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = "<group>"; };
56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = "<group>"; };
56717239261B23C800423B6F /* PlayerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerList.swift; sourceTree = "<group>"; };
56717251261B334D00423B6F /* PlayerRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequestTests.swift; sourceTree = "<group>"; };
567C3E162520B6DE0011F6E9 /* GRDBCombineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBCombineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBCombineDemoApp.swift; sourceTree = "<group>"; };
567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
567C3E222520B6DF0011F6E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
567C3E532520B75C0011F6E9 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
567C3E542520B75C0011F6E9 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
567C3E592520B75C0011F6E9 /* PlayerFormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerFormView.swift; sourceTree = "<group>"; };
567C3E5A2520B75C0011F6E9 /* AppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerCreationView.swift; sourceTree = "<group>"; };
567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionView.swift; sourceTree = "<group>"; };
567C3E652520B7880011F6E9 /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = "<group>"; };
567C3E762520BB650011F6E9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
567C3E782520BB650011F6E9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
56B6D1082619EC1B003CC455 /* PlayerRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequest.swift; sourceTree = "<group>"; };
56F8A11C2735988F0011ACBE /* GRDB.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GRDB.swift; path = ../../..; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
56026C9525B8A7D000D1DF3F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
567C3E132520B6DE0011F6E9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
56519DC7274FC85600ED16D8 /* GRDBQuery in Frameworks */,
56F8A1202735989D0011ACBE /* GRDB in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
56026C9925B8A7D000D1DF3F /* GRDBCombineDemoTests */ = {
isa = PBXGroup;
children = (
56026C9C25B8A7D000D1DF3F /* Info.plist */,
56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */,
56717251261B334D00423B6F /* PlayerRequestTests.swift */,
56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */,
);
path = GRDBCombineDemoTests;
sourceTree = "<group>";
};
56185BC125B8047D00B9C30F /* Resources */ = {
isa = PBXGroup;
children = (
567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */,
567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */,
567C3E752520BB650011F6E9 /* Localizable.stringsdict */,
);
path = Resources;
sourceTree = "<group>";
};
567C3E0D2520B6DE0011F6E9 = {
isa = PBXGroup;
children = (
567C3E182520B6DE0011F6E9 /* GRDBCombineDemo */,
56026C9925B8A7D000D1DF3F /* GRDBCombineDemoTests */,
567C3E172520B6DE0011F6E9 /* Products */,
567C3E4D2520B70E0011F6E9 /* Frameworks */,
56F8A11C2735988F0011ACBE /* GRDB.swift */,
);
sourceTree = "<group>";
};
567C3E172520B6DE0011F6E9 /* Products */ = {
isa = PBXGroup;
children = (
567C3E162520B6DE0011F6E9 /* GRDBCombineDemo.app */,
56026C9825B8A7D000D1DF3F /* GRDBCombineDemoTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
567C3E182520B6DE0011F6E9 /* GRDBCombineDemo */ = {
isa = PBXGroup;
children = (
567C3E222520B6DF0011F6E9 /* Info.plist */,
567C3E652520B7880011F6E9 /* AppDatabase.swift */,
567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */,
567C3E542520B75C0011F6E9 /* Persistence.swift */,
567C3E532520B75C0011F6E9 /* Player.swift */,
56B6D1082619EC1B003CC455 /* PlayerRequest.swift */,
567C3E1F2520B6DF0011F6E9 /* Preview Content */,
56185BC125B8047D00B9C30F /* Resources */,
567C3E582520B75C0011F6E9 /* Views */,
);
path = GRDBCombineDemo;
sourceTree = "<group>";
};
567C3E1F2520B6DF0011F6E9 /* Preview Content */ = {
isa = PBXGroup;
children = (
567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
567C3E4D2520B70E0011F6E9 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
567C3E582520B75C0011F6E9 /* Views */ = {
isa = PBXGroup;
children = (
567C3E5A2520B75C0011F6E9 /* AppView.swift */,
567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */,
567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */,
567C3E592520B75C0011F6E9 /* PlayerFormView.swift */,
56717239261B23C800423B6F /* PlayerList.swift */,
);
path = Views;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
56026C9725B8A7D000D1DF3F /* GRDBCombineDemoTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBCombineDemoTests" */;
buildPhases = (
56026C9425B8A7D000D1DF3F /* Sources */,
56026C9525B8A7D000D1DF3F /* Frameworks */,
56026C9625B8A7D000D1DF3F /* Resources */,
);
buildRules = (
);
dependencies = (
56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */,
);
name = GRDBCombineDemoTests;
productName = GRDBCombineDemoTests;
productReference = 56026C9825B8A7D000D1DF3F /* GRDBCombineDemoTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
567C3E152520B6DE0011F6E9 /* GRDBCombineDemo */ = {
isa = PBXNativeTarget;
buildConfigurationList = 567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBCombineDemo" */;
buildPhases = (
567C3E122520B6DE0011F6E9 /* Sources */,
567C3E132520B6DE0011F6E9 /* Frameworks */,
567C3E142520B6DE0011F6E9 /* Resources */,
567C3E502520B70E0011F6E9 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
56F8A11E273598960011ACBE /* PBXTargetDependency */,
);
name = GRDBCombineDemo;
packageProductDependencies = (
56F8A11F2735989D0011ACBE /* GRDB */,
56519DC6274FC85600ED16D8 /* GRDBQuery */,
);
productName = GRBCombineDemo;
productReference = 567C3E162520B6DE0011F6E9 /* GRDBCombineDemo.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
567C3E0E2520B6DE0011F6E9 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1200;
TargetAttributes = {
56026C9725B8A7D000D1DF3F = {
CreatedOnToolsVersion = 12.3;
TestTargetID = 567C3E152520B6DE0011F6E9;
};
567C3E152520B6DE0011F6E9 = {
CreatedOnToolsVersion = 12.0;
};
};
};
buildConfigurationList = 567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBCombineDemo" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 567C3E0D2520B6DE0011F6E9;
packageReferences = (
56519DC5274FC85600ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */,
);
productRefGroup = 567C3E172520B6DE0011F6E9 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
567C3E152520B6DE0011F6E9 /* GRDBCombineDemo */,
56026C9725B8A7D000D1DF3F /* GRDBCombineDemoTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
56026C9625B8A7D000D1DF3F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
567C3E142520B6DE0011F6E9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */,
567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */,
567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */,
567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
56026C9425B8A7D000D1DF3F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */,
56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */,
56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
567C3E122520B6DE0011F6E9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */,
567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */,
56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */,
5671723A261B23C800423B6F /* PlayerList.swift in Sources */,
567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */,
567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */,
567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */,
567C3E622520B75D0011F6E9 /* AppView.swift in Sources */,
567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */,
567C3E1A2520B6DE0011F6E9 /* GRDBCombineDemoApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 567C3E152520B6DE0011F6E9 /* GRDBCombineDemo */;
targetProxy = 56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */;
};
56F8A11E273598960011ACBE /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 56F8A11D273598960011ACBE /* GRDB */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
567C3E752520BB650011F6E9 /* Localizable.stringsdict */ = {
isa = PBXVariantGroup;
children = (
567C3E762520BB650011F6E9 /* en */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
};
567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
567C3E782520BB650011F6E9 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
56026C9F25B8A7D000D1DF3F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = GRDBCombineDemoTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemoTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBCombineDemo.app/GRDBCombineDemo";
};
name = Debug;
};
56026CA025B8A7D000D1DF3F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = GRDBCombineDemoTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemoTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBCombineDemo.app/GRDBCombineDemo";
};
name = Release;
};
567C3E232520B6DF0011F6E9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_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;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
567C3E242520B6DF0011F6E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_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;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
567C3E262520B6DF0011F6E9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"GRDBCombineDemo/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = GRDBCombineDemo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
567C3E272520B6DF0011F6E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"GRDBCombineDemo/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = GRDBCombineDemo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBCombineDemoTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
56026C9F25B8A7D000D1DF3F /* Debug */,
56026CA025B8A7D000D1DF3F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBCombineDemo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
567C3E232520B6DF0011F6E9 /* Debug */,
567C3E242520B6DF0011F6E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBCombineDemo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
567C3E262520B6DF0011F6E9 /* Debug */,
567C3E272520B6DF0011F6E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
56519DC5274FC85600ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/groue/GRDBQuery";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.6.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
56519DC6274FC85600ED16D8 /* GRDBQuery */ = {
isa = XCSwiftPackageProductDependency;
package = 56519DC5274FC85600ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */;
productName = GRDBQuery;
};
56F8A11D273598960011ACBE /* GRDB */ = {
isa = XCSwiftPackageProductDependency;
productName = GRDB;
};
56F8A11F2735989D0011ACBE /* GRDB */ = {
isa = XCSwiftPackageProductDependency;
productName = GRDB;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 567C3E0E2520B6DE0011F6E9 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "grdbquery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/groue/GRDBQuery",
"state" : {
"revision" : "a6c46dd38ecf11a5c37732870dc03a384d582fba",
"version" : "0.9.0"
}
}
],
"version" : 2
}
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
BuildableName = "GRDBCombineDemo.app"
BlueprintName = "GRDBCombineDemo"
ReferencedContainer = "container:GRDBCombineDemo.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "56026C9725B8A7D000D1DF3F"
BuildableName = "GRDBCombineDemoTests.xctest"
BlueprintName = "GRDBCombineDemoTests"
ReferencedContainer = "container:GRDBCombineDemo.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
BuildableName = "GRDBCombineDemo.app"
BlueprintName = "GRDBCombineDemo"
ReferencedContainer = "container:GRDBCombineDemo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "SQL_TRACE"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
BuildableName = "GRDBCombineDemo.app"
BlueprintName = "GRDBCombineDemo"
ReferencedContainer = "container:GRDBCombineDemo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,235 @@
import Foundation
import GRDB
import os.log
/// A database of players.
///
/// You create an `AppDatabase` with a connection to an SQLite database
/// (see <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>).
///
/// Create those connections with a configuration returned from
/// `AppDatabase/makeConfiguration(_:)`.
///
/// For example:
///
/// ```swift
/// // Create an in-memory AppDatabase
/// let config = AppDatabase.makeConfiguration()
/// let dbQueue = try DatabaseQueue(configuration: config)
/// let appDatabase = try AppDatabase(dbQueue)
/// ```
struct AppDatabase {
/// Creates an `AppDatabase`, and makes sure the database schema
/// is ready.
///
/// - important: Create the `DatabaseWriter` with a configuration
/// returned by ``makeConfiguration(_:)``.
init(_ dbWriter: any DatabaseWriter) throws {
self.dbWriter = dbWriter
try migrator.migrate(dbWriter)
}
/// Provides access to the database.
///
/// Application can use a `DatabasePool`, while SwiftUI previews and tests
/// can use a fast in-memory `DatabaseQueue`.
///
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>
private let dbWriter: any DatabaseWriter
}
// MARK: - Database Configuration
extension AppDatabase {
private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL")
/// Returns a database configuration suited for `PlayerRepository`.
///
/// SQL statements are logged if the `SQL_TRACE` environment variable
/// is set.
///
/// - parameter base: A base configuration.
public static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration {
var config = base
// An opportunity to add required custom SQL functions or
// collations, if needed:
// config.prepareDatabase { db in
// db.add(function: ...)
// }
// Log SQL statements if the `SQL_TRACE` environment variable is set.
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/database/trace(options:_:)>
if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil {
config.prepareDatabase { db in
db.trace {
// It's ok to log statements publicly. Sensitive
// information (statement arguments) are not logged
// unless config.publicStatementArguments is set
// (see below).
os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0))
}
}
}
#if DEBUG
// Protect sensitive information by enabling verbose debugging in
// DEBUG builds only.
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/configuration/publicstatementarguments>
config.publicStatementArguments = true
#endif
return config
}
}
// MARK: - Database Migrations
extension AppDatabase {
/// The DatabaseMigrator that defines the database schema.
///
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations>
private var migrator: DatabaseMigrator {
var migrator = DatabaseMigrator()
#if DEBUG
// Speed up development by nuking the database when migrations change
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations>
migrator.eraseDatabaseOnSchemaChange = true
#endif
migrator.registerMigration("createPlayer") { db in
// Create a table
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseschema>
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer).notNull()
}
}
// Migrations for future application versions will be inserted here:
// migrator.registerMigration(...) { db in
// ...
// }
return migrator
}
}
// MARK: - Database Access: Writes
// The write methods execute invariant-preserving database transactions.
extension AppDatabase {
/// A validation error that prevents some players from being saved into
/// the database.
enum ValidationError: LocalizedError {
case missingName
var errorDescription: String? {
switch self {
case .missingName:
return "Please provide a name"
}
}
}
/// Saves (inserts or updates) a player. When the method returns, the
/// player is present in the database, and its id is not nil.
func savePlayer(_ player: inout Player) throws {
if player.name.isEmpty {
throw ValidationError.missingName
}
try dbWriter.write { db in
try player.save(db)
}
}
/// Delete the specified players
func deletePlayers(ids: [Int64]) throws {
try dbWriter.write { db in
_ = try Player.deleteAll(db, ids: ids)
}
}
/// Delete all players
func deleteAllPlayers() throws {
try dbWriter.write { db in
_ = try Player.deleteAll(db)
}
}
/// Refresh all players (by performing some random changes, for demo purpose).
func refreshPlayers() throws {
try dbWriter.write { db in
if try Player.all().isEmpty(db) {
// When database is empty, insert new random players
try createRandomPlayers(db)
} else {
// Insert a player
if Bool.random() {
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
}
// Delete a random player
if Bool.random() {
try Player.order(sql: "RANDOM()").limit(1).deleteAll(db)
}
// Update some players
for var player in try Player.fetchAll(db) where Bool.random() {
try player.updateChanges(db) {
$0.score = Player.randomScore()
}
}
}
}
}
/// Create random players if the database is empty.
func createRandomPlayersIfEmpty() throws {
try dbWriter.write { db in
if try Player.all().isEmpty(db) {
try createRandomPlayers(db)
}
}
}
private static let uiTestPlayers = [
Player(id: nil, name: "Arthur", score: 5),
Player(id: nil, name: "Barbara", score: 6),
Player(id: nil, name: "Craig", score: 8),
Player(id: nil, name: "David", score: 4),
Player(id: nil, name: "Elena", score: 1),
Player(id: nil, name: "Frederik", score: 2),
Player(id: nil, name: "Gilbert", score: 7),
Player(id: nil, name: "Henriette", score: 3)]
func createPlayersForUITests() throws {
try dbWriter.write { db in
try AppDatabase.uiTestPlayers.forEach { player in
_ = try player.inserted(db) // insert but ignore inserted id
}
}
}
/// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`.
private func createRandomPlayers(_ db: Database) throws {
for _ in 0..<8 {
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
}
}
}
// MARK: - Database Access: Reads
// This demo app does not provide any specific reading method, and instead
// gives an unrestricted read-only access to the rest of the application.
// In your app, you are free to choose another path, and define focused
// reading methods.
extension AppDatabase {
/// Provides a read-only access to the database
var reader: DatabaseReader {
dbWriter
}
}
@@ -0,0 +1,32 @@
import GRDBQuery
import SwiftUI
@main
struct GRDBCombineDemoApp: App {
var body: some Scene {
WindowGroup {
AppView().appDatabase(.shared)
}
}
}
// MARK: - Give SwiftUI access to the database
private struct AppDatabaseKey: EnvironmentKey {
static var defaultValue: AppDatabase { .empty() }
}
extension EnvironmentValues {
var appDatabase: AppDatabase {
get { self[AppDatabaseKey.self] }
set { self[AppDatabaseKey.self] = newValue }
}
}
extension View {
func appDatabase(_ appDatabase: AppDatabase) -> some View {
self
.environment(\.appDatabase, appDatabase)
.databaseContext(.readOnly { appDatabase.reader })
}
}
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -0,0 +1,77 @@
import Foundation
import GRDB
extension AppDatabase {
/// The database for the application
static let shared = makeShared()
private static func makeShared() -> AppDatabase {
do {
// Apply recommendations from
// <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>
//
// Create the "Application Support/Database" directory if needed
let fileManager = FileManager.default
let appSupportURL = try fileManager.url(
for: .applicationSupportDirectory, in: .userDomainMask,
appropriateFor: nil, create: true)
let directoryURL = appSupportURL.appendingPathComponent("Database", isDirectory: true)
// Support for tests: delete the database if requested
if CommandLine.arguments.contains("-reset") {
try? fileManager.removeItem(at: directoryURL)
}
// Create the database folder if needed
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
// Open or create the database
let databaseURL = directoryURL.appendingPathComponent("db.sqlite")
NSLog("Database stored at \(databaseURL.path)")
let dbPool = try DatabasePool(
path: databaseURL.path,
// Use default AppDatabase configuration
configuration: AppDatabase.makeConfiguration())
// Create the AppDatabase
let appDatabase = try AppDatabase(dbPool)
// Prepare the database with test fixtures if requested
if CommandLine.arguments.contains("-fixedTestData") {
try appDatabase.createPlayersForUITests()
} else {
// Otherwise, populate the database if it is empty, for better
// demo purpose.
try appDatabase.createRandomPlayersIfEmpty()
}
return appDatabase
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
//
// Typical reasons for an error here include:
// * The parent directory cannot be created, or disallows writing.
// * The database is not accessible, due to permissions or data protection when the device is locked.
// * The device is out of space.
// * The database could not be migrated to its latest schema version.
// Check the error message to determine what the actual problem was.
fatalError("Unresolved error \(error)")
}
}
/// Creates an empty database for SwiftUI previews
static func empty() -> AppDatabase {
// Connect to an in-memory database
// See https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections
let dbQueue = try! DatabaseQueue(configuration: AppDatabase.makeConfiguration())
return try! AppDatabase(dbQueue)
}
/// Creates a database full of random players for SwiftUI previews
static func random() -> AppDatabase {
let appDatabase = empty()
try! appDatabase.createRandomPlayersIfEmpty()
return appDatabase
}
}
@@ -0,0 +1,105 @@
import GRDB
/// The Player struct.
///
/// Identifiable conformance supports SwiftUI list animations, and type-safe
/// GRDB primary key methods.
/// Equatable conformance supports tests.
struct Player: Identifiable, Equatable {
/// The player id.
///
/// Int64 is the recommended type for auto-incremented database ids.
/// Use nil for players that are not inserted yet in the database.
var id: Int64?
var name: String
var score: Int
}
extension Player {
private static let names = [
"Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David",
"Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette",
"Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl",
"Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole",
"Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul",
"Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain",
"Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann",
"Zazie", "Zoé"]
/// Creates a new player with empty name and zero score
static func new() -> Player {
Player(id: nil, name: "", score: 0)
}
/// Creates a new player with random name and random score
static func makeRandom() -> Player {
Player(id: nil, name: randomName(), score: randomScore())
}
/// Returns a random name
static func randomName() -> String {
names.randomElement()!
}
/// Returns a random score
static func randomScore() -> Int {
10 * Int.random(in: 0...100)
}
}
// MARK: - Persistence
/// Make Player a Codable Record.
///
/// See <https://github.com/groue/GRDB.swift/blob/master/README.md#records>
extension Player: Codable, FetchableRecord, MutablePersistableRecord {
// Define database columns from CodingKeys
fileprivate enum Columns {
static let name = Column(CodingKeys.name)
static let score = Column(CodingKeys.score)
}
/// Updates a player id after it has been inserted in the database.
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
// MARK: - Player Database Requests
/// Define some player requests used by the application.
///
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices>
extension DerivableRequest<Player> {
/// A request of players ordered by name.
///
/// For example:
///
/// let players: [Player] = try dbWriter.read { db in
/// try Player.all().orderedByName().fetchAll(db)
/// }
func orderedByName() -> Self {
// Sort by name in a localized case insensitive fashion
// See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison
order(Player.Columns.name.collating(.localizedCaseInsensitiveCompare))
}
/// A request of players ordered by score.
///
/// For example:
///
/// let players: [Player] = try dbWriter.read { db in
/// try Player.all().orderedByScore().fetchAll(db)
/// }
/// let bestPlayer: Player? = try dbWriter.read { db in
/// try Player.all().orderedByScore().fetchOne(db)
/// }
func orderedByScore() -> Self {
// Sort by descending score, and then by name, in a
// localized case insensitive fashion
// See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison
order(
Player.Columns.score.desc,
Player.Columns.name.collating(.localizedCaseInsensitiveCompare))
}
}
@@ -0,0 +1,35 @@
import GRDB
import GRDBQuery
/// A player request can be used with the `@Query` property wrapper in order to
/// feed a view with a list of players.
///
/// For example:
///
/// struct MyView: View {
/// @Query(PlayerRequest(ordering: .byName)) private var players: [Player]
///
/// var body: some View {
/// List(players) { player in ... )
/// }
/// }
struct PlayerRequest: ValueObservationQueryable {
enum Ordering {
case byScore
case byName
}
static var defaultValue: [Player] { [] }
/// The ordering used by the player request.
var ordering: Ordering
func fetch(_ db: Database) throws -> [Player] {
switch ordering {
case .byScore:
return try Player.all().orderedByScore().fetchAll(db)
case .byName:
return try Player.all().orderedByName().fetchAll(db)
}
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,116 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "icon_20pt@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "icon_20pt@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon_29pt@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "icon_29pt@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "icon_40pt@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "icon_40pt@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon_60pt@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon_60pt@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "icon_20pt.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "icon_20pt@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "icon_29pt.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "icon_29pt@2x-1.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "icon_40pt.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "icon_40pt@2x-1.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon_76pt.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon_76pt@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "icon_83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Some files were not shown because too many files have changed in this diff Show More