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,18 @@
{
"images" : [
{
"idiom" : "watch",
"screenWidth" : "{130,145}",
"scale" : "2x"
},
{
"idiom" : "watch",
"screenWidth" : "{146,165}",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,28 @@
{
"assets" : [
{
"idiom" : "watch",
"filename" : "Circular.imageset",
"role" : "circular"
},
{
"idiom" : "watch",
"filename" : "Extra Large.imageset",
"role" : "extra-large"
},
{
"idiom" : "watch",
"filename" : "Modular.imageset",
"role" : "modular"
},
{
"idiom" : "watch",
"filename" : "Utilitarian.imageset",
"role" : "utilitarian"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,18 @@
{
"images" : [
{
"idiom" : "watch",
"screenWidth" : "{130,145}",
"scale" : "2x"
},
{
"idiom" : "watch",
"screenWidth" : "{146,165}",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,18 @@
{
"images" : [
{
"idiom" : "watch",
"screenWidth" : "{130,145}",
"scale" : "2x"
},
{
"idiom" : "watch",
"screenWidth" : "{146,165}",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,18 @@
{
"images" : [
{
"idiom" : "watch",
"screenWidth" : "{130,145}",
"scale" : "2x"
},
{
"idiom" : "watch",
"screenWidth" : "{146,165}",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,48 @@
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once youre done.
backgroundTask.setTaskCompletedWithSnapshot(false)
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once youre done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once youre done.
urlSessionTask.setTaskCompletedWithSnapshot(false)
case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
// Be sure to complete the relevant-shortcut task once you're done.
relevantShortcutTask.setTaskCompletedWithSnapshot(false)
case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
// Be sure to complete the intent-did-run task once you're done.
intentDidRunTask.setTaskCompletedWithSnapshot(false)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(false)
}
}
}
}
@@ -0,0 +1,36 @@
<?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>en</string>
<key>CFBundleDisplayName</key>
<string>GRDBDemoWatchOS Extension</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>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>com.github.groue.GRDBDemoiOS2.watchkitapp</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
</dict>
<key>WKExtensionDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
</dict>
</plist>
@@ -0,0 +1,15 @@
import WatchKit
import Foundation
import GRDB
class InterfaceController: WKInterfaceController {
@IBOutlet var versionLabel: WKInterfaceLabel!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
let sqliteVersion = String(cString: sqlite3_libversion(), encoding: .utf8)
versionLabel.setText(sqliteVersion)
}
}
@@ -0,0 +1,55 @@
{
"images" : [
{
"size" : "24x24",
"idiom" : "watch",
"scale" : "2x",
"role" : "notificationCenter",
"subtype" : "38mm"
},
{
"size" : "27.5x27.5",
"idiom" : "watch",
"scale" : "2x",
"role" : "notificationCenter",
"subtype" : "42mm"
},
{
"size" : "29x29",
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "watch",
"scale" : "2x",
"role" : "appLauncher",
"subtype" : "38mm"
},
{
"size" : "86x86",
"idiom" : "watch",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "38mm"
},
{
"size" : "98x98",
"idiom" : "watch",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "42mm"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder.WatchKit.Storyboard" version="3.0" toolsVersion="12118" systemVersion="16E195" targetRuntime="watchKit" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="AgC-eL-Hgc">
<device id="watch38" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="watchOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12086"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBWatchKitPlugin" version="12029"/>
</dependencies>
<scenes>
<!--Interface Controller-->
<scene sceneID="aou-V4-d1y">
<objects>
<controller id="AgC-eL-Hgc" customClass="InterfaceController" customModule="GRDBDemoWatchOS" customModuleProvider="target">
<items>
<group width="1" alignment="left" verticalAlignment="center" layout="vertical" id="tkk-aG-QQF">
<items>
<label alignment="center" text="SQLite version" id="dGA-xX-x8V"/>
<label alignment="center" text="Label" id="whi-N6-h8U"/>
</items>
</group>
</items>
<connections>
<outlet property="versionLabel" destination="whi-N6-h8U" id="5xN-Gj-yNt"/>
</connections>
</controller>
</objects>
</scene>
</scenes>
</document>
@@ -0,0 +1,33 @@
<?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>en</string>
<key>CFBundleDisplayName</key>
<string>GRDBDemoiOS</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>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>WKCompanionAppBundleIdentifier</key>
<string>com.github.groue.GRDBDemoiOS2</string>
<key>WKWatchKitApp</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,912 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
56185BBF25B8036100B9C30F /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56185BBE25B8036100B9C30F /* Persistence.swift */; };
56185BF325B80B8900B9C30F /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56185BF225B80B8900B9C30F /* AppDatabaseTests.swift */; };
56185C0725B80CEC00B9C30F /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56185C0625B80CEC00B9C30F /* PlayerTests.swift */; };
567940C528BA33D2004A0298 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56606D0A2355931F00185962 /* GRDB.framework */; };
567940C628BA33D2004A0298 /* GRDB.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 56606D0A2355931F00185962 /* GRDB.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
567940C728BA33DE004A0298 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56606D0A2355931F00185962 /* GRDB.framework */; };
567940C828BA33DE004A0298 /* GRDB.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 56606D0A2355931F00185962 /* GRDB.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
568E5FC31E926430002582E0 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 568E5FC11E926430002582E0 /* Interface.storyboard */; };
568E5FC51E926430002582E0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 568E5FC41E926430002582E0 /* Assets.xcassets */; };
568E5FCC1E926430002582E0 /* GRDBDemoWatchOS Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 568E5FCB1E926430002582E0 /* GRDBDemoWatchOS Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
568E5FD11E926430002582E0 /* InterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568E5FD01E926430002582E0 /* InterfaceController.swift */; };
568E5FD31E926430002582E0 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568E5FD21E926430002582E0 /* ExtensionDelegate.swift */; };
568E5FD51E926430002582E0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 568E5FD41E926430002582E0 /* Assets.xcassets */; };
568E5FD91E926430002582E0 /* GRDBDemoWatchOS.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 568E5FBF1E926430002582E0 /* GRDBDemoWatchOS.app */; };
56B036071E8D9EBE003B6DA4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B036061E8D9EBE003B6DA4 /* AppDelegate.swift */; };
56B0360C1E8D9EBE003B6DA4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 56B0360A1E8D9EBE003B6DA4 /* Main.storyboard */; };
56B0360E1E8D9EBE003B6DA4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56B0360D1E8D9EBE003B6DA4 /* Assets.xcassets */; };
56B036111E8D9EBE003B6DA4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 56B0360F1E8D9EBE003B6DA4 /* LaunchScreen.storyboard */; };
56B0361B1E8D9F38003B6DA4 /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B036191E8D9F38003B6DA4 /* AppDatabase.swift */; };
56B0361C1E8D9F38003B6DA4 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B0361A1E8D9F38003B6DA4 /* Player.swift */; };
56B036231E8D9F4C003B6DA4 /* PlayerEditionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */; };
56B036241E8D9F4C003B6DA4 /* PlayerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */; };
56FE6F2624A90CE400711EDF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FE6F2524A90CE400711EDF /* SceneDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
56185BF525B80B8900B9C30F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 56B035FB1E8D9EBE003B6DA4 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 56B036021E8D9EBE003B6DA4;
remoteInfo = GRDBDemoiOS;
};
5642252F29A2390800D714BF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = DC3773F219C8CBB3004FCF85;
remoteInfo = GRDB;
};
5642253329A2391900D714BF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = DC3773F219C8CBB3004FCF85;
remoteInfo = GRDB;
};
56606D092355931F00185962 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = DC3773F319C8CBB3004FCF85;
remoteInfo = GRDBOSX;
};
56606D0B2355931F00185962 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 56E5D7F91B4D422D00430942;
remoteInfo = GRDBOSXTests;
};
568E5FCD1E926430002582E0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 56B035FB1E8D9EBE003B6DA4 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 568E5FCA1E926430002582E0;
remoteInfo = "GRDBDemoWatchOS Extension";
};
568E5FD71E926430002582E0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 56B035FB1E8D9EBE003B6DA4 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 568E5FBE1E926430002582E0;
remoteInfo = GRDBDemoWatchOS;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
568E5FDD1E926430002582E0 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
568E5FCC1E926430002582E0 /* GRDBDemoWatchOS Extension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
568E5FE11E926430002582E0 /* Embed Watch Content */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
dstSubfolderSpec = 16;
files = (
568E5FD91E926430002582E0 /* GRDBDemoWatchOS.app in Embed Watch Content */,
);
name = "Embed Watch Content";
runOnlyForDeploymentPostprocessing = 0;
};
568E5FE81E926547002582E0 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
567940C828BA33DE004A0298 /* GRDB.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
56B036621E8D9FB1003B6DA4 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
567940C628BA33D2004A0298 /* GRDB.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
56185BBE25B8036100B9C30F /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
56185BF025B80B8900B9C30F /* GRDBDemoiOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBDemoiOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
56185BF225B80B8900B9C30F /* AppDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = "<group>"; };
56185BF425B80B8900B9C30F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
56185C0625B80CEC00B9C30F /* PlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = "<group>"; };
568E5FBF1E926430002582E0 /* GRDBDemoWatchOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBDemoWatchOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
568E5FC21E926430002582E0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
568E5FC41E926430002582E0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
568E5FC61E926430002582E0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
568E5FCB1E926430002582E0 /* GRDBDemoWatchOS Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "GRDBDemoWatchOS Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
568E5FD01E926430002582E0 /* InterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceController.swift; sourceTree = "<group>"; };
568E5FD21E926430002582E0 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = "<group>"; };
568E5FD41E926430002582E0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
568E5FD61E926430002582E0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
56B036031E8D9EBE003B6DA4 /* GRDBDemoiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBDemoiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
56B036061E8D9EBE003B6DA4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
56B0360B1E8D9EBE003B6DA4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
56B0360D1E8D9EBE003B6DA4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
56B036101E8D9EBE003B6DA4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
56B036121E8D9EBE003B6DA4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
56B036191E8D9F38003B6DA4 /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = "<group>"; };
56B0361A1E8D9F38003B6DA4 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionViewController.swift; sourceTree = "<group>"; };
56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerListViewController.swift; sourceTree = "<group>"; };
56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GRDB.xcodeproj; path = ../../../GRDB.xcodeproj; sourceTree = "<group>"; };
56FE6F2524A90CE400711EDF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
56185BED25B80B8900B9C30F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
568E5FC81E926430002582E0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
567940C728BA33DE004A0298 /* GRDB.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
56B036001E8D9EBE003B6DA4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
567940C528BA33D2004A0298 /* GRDB.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
56185BF125B80B8900B9C30F /* GRDBDemoiOSTests */ = {
isa = PBXGroup;
children = (
56185BF425B80B8900B9C30F /* Info.plist */,
56185BF225B80B8900B9C30F /* AppDatabaseTests.swift */,
56185C0625B80CEC00B9C30F /* PlayerTests.swift */,
);
path = GRDBDemoiOSTests;
sourceTree = "<group>";
};
56606CFD2355931F00185962 /* Products */ = {
isa = PBXGroup;
children = (
56606D0A2355931F00185962 /* GRDB.framework */,
56606D0C2355931F00185962 /* GRDBTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
56606D1F2355938A00185962 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
568E5FC01E926430002582E0 /* GRDBDemoWatchOS */ = {
isa = PBXGroup;
children = (
568E5FC11E926430002582E0 /* Interface.storyboard */,
568E5FC41E926430002582E0 /* Assets.xcassets */,
568E5FC61E926430002582E0 /* Info.plist */,
);
path = GRDBDemoWatchOS;
sourceTree = "<group>";
};
568E5FCF1E926430002582E0 /* GRDBDemoWatchOS Extension */ = {
isa = PBXGroup;
children = (
568E5FD01E926430002582E0 /* InterfaceController.swift */,
568E5FD21E926430002582E0 /* ExtensionDelegate.swift */,
568E5FD41E926430002582E0 /* Assets.xcassets */,
568E5FD61E926430002582E0 /* Info.plist */,
);
path = "GRDBDemoWatchOS Extension";
sourceTree = "<group>";
};
56B035FA1E8D9EBE003B6DA4 = {
isa = PBXGroup;
children = (
56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */,
56B036051E8D9EBE003B6DA4 /* GRDBDemoiOS */,
568E5FC01E926430002582E0 /* GRDBDemoWatchOS */,
568E5FCF1E926430002582E0 /* GRDBDemoWatchOS Extension */,
56185BF125B80B8900B9C30F /* GRDBDemoiOSTests */,
56B036041E8D9EBE003B6DA4 /* Products */,
56606D1F2355938A00185962 /* Frameworks */,
);
sourceTree = "<group>";
};
56B036041E8D9EBE003B6DA4 /* Products */ = {
isa = PBXGroup;
children = (
56B036031E8D9EBE003B6DA4 /* GRDBDemoiOS.app */,
568E5FBF1E926430002582E0 /* GRDBDemoWatchOS.app */,
568E5FCB1E926430002582E0 /* GRDBDemoWatchOS Extension.appex */,
56185BF025B80B8900B9C30F /* GRDBDemoiOSTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
56B036051E8D9EBE003B6DA4 /* GRDBDemoiOS */ = {
isa = PBXGroup;
children = (
56B036121E8D9EBE003B6DA4 /* Info.plist */,
56B036191E8D9F38003B6DA4 /* AppDatabase.swift */,
56B036061E8D9EBE003B6DA4 /* AppDelegate.swift */,
56185BBE25B8036100B9C30F /* Persistence.swift */,
56B0361A1E8D9F38003B6DA4 /* Player.swift */,
56FE6F2524A90CE400711EDF /* SceneDelegate.swift */,
56FE6F4E24A9112900711EDF /* Resources */,
56FE6F4F24A9114200711EDF /* ViewControllers */,
);
path = GRDBDemoiOS;
sourceTree = "<group>";
};
56FE6F4E24A9112900711EDF /* Resources */ = {
isa = PBXGroup;
children = (
56B0360D1E8D9EBE003B6DA4 /* Assets.xcassets */,
56B0360F1E8D9EBE003B6DA4 /* LaunchScreen.storyboard */,
56B0360A1E8D9EBE003B6DA4 /* Main.storyboard */,
);
path = Resources;
sourceTree = "<group>";
};
56FE6F4F24A9114200711EDF /* ViewControllers */ = {
isa = PBXGroup;
children = (
56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */,
56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
56185BEF25B80B8900B9C30F /* GRDBDemoiOSTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 56185BFF25B80B8900B9C30F /* Build configuration list for PBXNativeTarget "GRDBDemoiOSTests" */;
buildPhases = (
56185BEC25B80B8900B9C30F /* Sources */,
56185BED25B80B8900B9C30F /* Frameworks */,
56185BEE25B80B8900B9C30F /* Resources */,
);
buildRules = (
);
dependencies = (
56185BF625B80B8900B9C30F /* PBXTargetDependency */,
);
name = GRDBDemoiOSTests;
productName = GRDBDemoiOSTests;
productReference = 56185BF025B80B8900B9C30F /* GRDBDemoiOSTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
568E5FBE1E926430002582E0 /* GRDBDemoWatchOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 568E5FDE1E926430002582E0 /* Build configuration list for PBXNativeTarget "GRDBDemoWatchOS" */;
buildPhases = (
568E5FBD1E926430002582E0 /* Resources */,
568E5FDD1E926430002582E0 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
568E5FCE1E926430002582E0 /* PBXTargetDependency */,
);
name = GRDBDemoWatchOS;
productName = GRDBDemoWatchOS;
productReference = 568E5FBF1E926430002582E0 /* GRDBDemoWatchOS.app */;
productType = "com.apple.product-type.application.watchapp2";
};
568E5FCA1E926430002582E0 /* GRDBDemoWatchOS Extension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 568E5FDA1E926430002582E0 /* Build configuration list for PBXNativeTarget "GRDBDemoWatchOS Extension" */;
buildPhases = (
568E5FC71E926430002582E0 /* Sources */,
568E5FC81E926430002582E0 /* Frameworks */,
568E5FC91E926430002582E0 /* Resources */,
568E5FE81E926547002582E0 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
5642253429A2391900D714BF /* PBXTargetDependency */,
);
name = "GRDBDemoWatchOS Extension";
productName = "GRDBDemoWatchOS Extension";
productReference = 568E5FCB1E926430002582E0 /* GRDBDemoWatchOS Extension.appex */;
productType = "com.apple.product-type.watchkit2-extension";
};
56B036021E8D9EBE003B6DA4 /* GRDBDemoiOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 56B036151E8D9EBE003B6DA4 /* Build configuration list for PBXNativeTarget "GRDBDemoiOS" */;
buildPhases = (
56B035FF1E8D9EBE003B6DA4 /* Sources */,
56B036001E8D9EBE003B6DA4 /* Frameworks */,
56B036011E8D9EBE003B6DA4 /* Resources */,
56B036621E8D9FB1003B6DA4 /* Embed Frameworks */,
568E5FE11E926430002582E0 /* Embed Watch Content */,
);
buildRules = (
);
dependencies = (
5642253029A2390800D714BF /* PBXTargetDependency */,
568E5FD81E926430002582E0 /* PBXTargetDependency */,
);
name = GRDBDemoiOS;
productName = GRDBDemoiOS;
productReference = 56B036031E8D9EBE003B6DA4 /* GRDBDemoiOS.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
56B035FB1E8D9EBE003B6DA4 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1230;
LastUpgradeCheck = 1400;
ORGANIZATIONNAME = "Gwendal Roué";
TargetAttributes = {
56185BEF25B80B8900B9C30F = {
CreatedOnToolsVersion = 12.3;
DevelopmentTeam = AMD8W895CT;
ProvisioningStyle = Automatic;
TestTargetID = 56B036021E8D9EBE003B6DA4;
};
568E5FBE1E926430002582E0 = {
CreatedOnToolsVersion = 8.3;
DevelopmentTeam = AMD8W895CT;
ProvisioningStyle = Automatic;
};
568E5FCA1E926430002582E0 = {
CreatedOnToolsVersion = 8.3;
DevelopmentTeam = AMD8W895CT;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
};
56B036021E8D9EBE003B6DA4 = {
CreatedOnToolsVersion = 8.3;
DevelopmentTeam = AMD8W895CT;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 56B035FE1E8D9EBE003B6DA4 /* Build configuration list for PBXProject "GRDBDemoiOS" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 56B035FA1E8D9EBE003B6DA4;
productRefGroup = 56B036041E8D9EBE003B6DA4 /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 56606CFD2355931F00185962 /* Products */;
ProjectRef = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */;
},
);
projectRoot = "";
targets = (
56B036021E8D9EBE003B6DA4 /* GRDBDemoiOS */,
568E5FBE1E926430002582E0 /* GRDBDemoWatchOS */,
568E5FCA1E926430002582E0 /* GRDBDemoWatchOS Extension */,
56185BEF25B80B8900B9C30F /* GRDBDemoiOSTests */,
);
};
/* End PBXProject section */
/* Begin PBXReferenceProxy section */
56606D0A2355931F00185962 /* GRDB.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = GRDB.framework;
remoteRef = 56606D092355931F00185962 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
56606D0C2355931F00185962 /* GRDBTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = GRDBTests.xctest;
remoteRef = 56606D0B2355931F00185962 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
56185BEE25B80B8900B9C30F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
568E5FBD1E926430002582E0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
568E5FC51E926430002582E0 /* Assets.xcassets in Resources */,
568E5FC31E926430002582E0 /* Interface.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
568E5FC91E926430002582E0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
568E5FD51E926430002582E0 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
56B036011E8D9EBE003B6DA4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
56B036111E8D9EBE003B6DA4 /* LaunchScreen.storyboard in Resources */,
56B0360E1E8D9EBE003B6DA4 /* Assets.xcassets in Resources */,
56B0360C1E8D9EBE003B6DA4 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
56185BEC25B80B8900B9C30F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
56185C0725B80CEC00B9C30F /* PlayerTests.swift in Sources */,
56185BF325B80B8900B9C30F /* AppDatabaseTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
568E5FC71E926430002582E0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
568E5FD31E926430002582E0 /* ExtensionDelegate.swift in Sources */,
568E5FD11E926430002582E0 /* InterfaceController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
56B035FF1E8D9EBE003B6DA4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
56FE6F2624A90CE400711EDF /* SceneDelegate.swift in Sources */,
56B0361C1E8D9F38003B6DA4 /* Player.swift in Sources */,
56B0361B1E8D9F38003B6DA4 /* AppDatabase.swift in Sources */,
56185BBF25B8036100B9C30F /* Persistence.swift in Sources */,
56B036071E8D9EBE003B6DA4 /* AppDelegate.swift in Sources */,
56B036241E8D9F4C003B6DA4 /* PlayerListViewController.swift in Sources */,
56B036231E8D9F4C003B6DA4 /* PlayerEditionViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
56185BF625B80B8900B9C30F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 56B036021E8D9EBE003B6DA4 /* GRDBDemoiOS */;
targetProxy = 56185BF525B80B8900B9C30F /* PBXContainerItemProxy */;
};
5642253029A2390800D714BF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = GRDB;
targetProxy = 5642252F29A2390800D714BF /* PBXContainerItemProxy */;
};
5642253429A2391900D714BF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = GRDB;
targetProxy = 5642253329A2391900D714BF /* PBXContainerItemProxy */;
};
568E5FCE1E926430002582E0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 568E5FCA1E926430002582E0 /* GRDBDemoWatchOS Extension */;
targetProxy = 568E5FCD1E926430002582E0 /* PBXContainerItemProxy */;
};
568E5FD81E926430002582E0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 568E5FBE1E926430002582E0 /* GRDBDemoWatchOS */;
targetProxy = 568E5FD71E926430002582E0 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
568E5FC11E926430002582E0 /* Interface.storyboard */ = {
isa = PBXVariantGroup;
children = (
568E5FC21E926430002582E0 /* Base */,
);
name = Interface.storyboard;
sourceTree = "<group>";
};
56B0360A1E8D9EBE003B6DA4 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
56B0360B1E8D9EBE003B6DA4 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
56B0360F1E8D9EBE003B6DA4 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
56B036101E8D9EBE003B6DA4 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
56185BF725B80B8900B9C30F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = AMD8W895CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = GRDBDemoiOSTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOSTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBDemoiOS.app/GRDBDemoiOS";
};
name = Debug;
};
56185BF825B80B8900B9C30F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = AMD8W895CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = GRDBDemoiOSTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOSTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBDemoiOS.app/GRDBDemoiOS";
};
name = Release;
};
568E5FDB1E926430002582E0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
CLANG_ENABLE_OBJC_WEAK = YES;
DEVELOPMENT_TEAM = AMD8W895CT;
INFOPLIST_FILE = "GRDBDemoWatchOS Extension/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp.watchkitextension;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = watchos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 4;
};
name = Debug;
};
568E5FDC1E926430002582E0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
CLANG_ENABLE_OBJC_WEAK = YES;
DEVELOPMENT_TEAM = AMD8W895CT;
INFOPLIST_FILE = "GRDBDemoWatchOS Extension/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp.watchkitextension;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = watchos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 4;
};
name = Release;
};
568E5FDF1E926430002582E0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_WEAK = YES;
DEVELOPMENT_TEAM = AMD8W895CT;
IBSC_MODULE = GRDBDemoWatchOS_Extension;
INFOPLIST_FILE = GRDBDemoWatchOS/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 4;
};
name = Debug;
};
568E5FE01E926430002582E0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_WEAK = YES;
DEVELOPMENT_TEAM = AMD8W895CT;
IBSC_MODULE = GRDBDemoWatchOS_Extension;
INFOPLIST_FILE = GRDBDemoWatchOS/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 4;
};
name = Release;
};
56B036131E8D9EBE003B6DA4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
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 = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
WATCHOS_DEPLOYMENT_TARGET = 8.0;
};
name = Debug;
};
56B036141E8D9EBE003B6DA4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
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;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES;
WATCHOS_DEPLOYMENT_TARGET = 8.0;
};
name = Release;
};
56B036161E8D9EBE003B6DA4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_WEAK = YES;
DEVELOPMENT_TEAM = AMD8W895CT;
INFOPLIST_FILE = GRDBDemoiOS/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
56B036171E8D9EBE003B6DA4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_WEAK = YES;
DEVELOPMENT_TEAM = AMD8W895CT;
INFOPLIST_FILE = GRDBDemoiOS/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
56185BFF25B80B8900B9C30F /* Build configuration list for PBXNativeTarget "GRDBDemoiOSTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
56185BF725B80B8900B9C30F /* Debug */,
56185BF825B80B8900B9C30F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
568E5FDA1E926430002582E0 /* Build configuration list for PBXNativeTarget "GRDBDemoWatchOS Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
568E5FDB1E926430002582E0 /* Debug */,
568E5FDC1E926430002582E0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
568E5FDE1E926430002582E0 /* Build configuration list for PBXNativeTarget "GRDBDemoWatchOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
568E5FDF1E926430002582E0 /* Debug */,
568E5FE01E926430002582E0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
56B035FE1E8D9EBE003B6DA4 /* Build configuration list for PBXProject "GRDBDemoiOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
56B036131E8D9EBE003B6DA4 /* Debug */,
56B036141E8D9EBE003B6DA4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
56B036151E8D9EBE003B6DA4 /* Build configuration list for PBXNativeTarget "GRDBDemoiOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
56B036161E8D9EBE003B6DA4 /* Debug */,
56B036171E8D9EBE003B6DA4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 56B035FB1E8D9EBE003B6DA4 /* 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,116 @@
<?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 = "568E5FBE1E926430002582E0"
BuildableName = "GRDBDemoWatchOS.app"
BlueprintName = "GRDBDemoWatchOS"
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "56B036021E8D9EBE003B6DA4"
BuildableName = "GRDBDemoiOS.app"
BlueprintName = "GRDBDemoiOS"
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "568E5FCA1E926430002582E0"
BuildableName = "GRDBDemoWatchOS Extension.appex"
BlueprintName = "GRDBDemoWatchOS Extension"
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "56185BEF25B80B8900B9C30F"
BuildableName = "GRDBDemoiOSTests.xctest"
BlueprintName = "GRDBDemoiOSTests"
ReferencedContainer = "container:GRDBDemoiOS.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 = "568E5FBE1E926430002582E0"
BuildableName = "GRDBDemoWatchOS.app"
BlueprintName = "GRDBDemoWatchOS"
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "568E5FBE1E926430002582E0"
BuildableName = "GRDBDemoWatchOS.app"
BlueprintName = "GRDBDemoWatchOS"
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -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 = "56B036021E8D9EBE003B6DA4"
BuildableName = "GRDBDemoiOS.app"
BlueprintName = "GRDBDemoiOS"
ReferencedContainer = "container:GRDBDemoiOS.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 = "56185BEF25B80B8900B9C30F"
BuildableName = "GRDBDemoiOSTests.xctest"
BlueprintName = "GRDBDemoiOSTests"
ReferencedContainer = "container:GRDBDemoiOS.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 = "56B036021E8D9EBE003B6DA4"
BuildableName = "GRDBDemoiOS.app"
BlueprintName = "GRDBDemoiOS"
ReferencedContainer = "container:GRDBDemoiOS.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 = "56B036021E8D9EBE003B6DA4"
BuildableName = "GRDBDemoiOS.app"
BlueprintName = "GRDBDemoiOS"
ReferencedContainer = "container:GRDBDemoiOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,200 @@
import GRDB
import os.log
/// `AppDatabase` lets the application access the database.
///
/// 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`, 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 {
/// 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 {
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)
}
}
}
/// 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,22 @@
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
@@ -0,0 +1,62 @@
<?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>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</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,49 @@
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)
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)
// 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)")
}
}
}
@@ -0,0 +1,104 @@
import GRDB
/// The Player struct.
///
/// Identifiable conformance supports type-safe GRDB primary key methods.
/// Hashable conformance supports table view updates
struct Player: Identifiable, Hashable {
/// The player id.
///
/// Int64 is the recommended type for auto-incremented database ids.
/// Use nil for players that are not inserted yet in the database.
var id: Int64?
var name: String
var score: Int
}
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,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,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" 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="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.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="10v-qH-Qer">
<rect key="frame" x="123.5" y="334.5" width="167" height="237"/>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="10v-qH-Qer" firstAttribute="centerX" secondItem="5Q0-b7-anu" secondAttribute="centerX" id="CgU-Ob-iCK"/>
<constraint firstItem="10v-qH-Qer" firstAttribute="centerY" secondItem="5Q0-b7-anu" secondAttribute="centerY" id="l2h-yP-koC"/>
</constraints>
<viewLayoutGuide key="safeArea" id="5Q0-b7-anu"/>
</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"/>
</resources>
</document>
@@ -0,0 +1,213 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="UAh-8N-CM8">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Player Edition View Controller-->
<scene sceneID="Juk-Ad-tZ2">
<objects>
<tableViewController id="iNr-Oe-muq" customClass="PlayerEditionViewController" customModule="GRDBDemoiOS" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" dataMode="static" style="insetGrouped" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="-1" estimatedSectionHeaderHeight="-1" sectionFooterHeight="-1" estimatedSectionFooterHeight="-1" id="vlV-lc-eQA">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<sections>
<tableViewSection headerTitle="Name" id="Nzq-eO-jT3">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" id="2gb-gB-918">
<rect key="frame" x="20" y="32" width="374" height="40.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="2gb-gB-918" id="iyM-TJ-JB4">
<rect key="frame" x="0.0" y="0.0" width="374" height="40.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" horizontalCompressionResistancePriority="250" contentHorizontalAlignment="left" contentVerticalAlignment="center" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="m5m-Zs-j0z">
<rect key="frame" x="20" y="11" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" spellCheckingType="no" returnKeyType="next"/>
<connections>
<action selector="textFieldDidChange:" destination="iNr-Oe-muq" eventType="editingChanged" id="EmC-Rp-f6S"/>
<outlet property="delegate" destination="iNr-Oe-muq" id="K2p-7d-teD"/>
</connections>
</textField>
</subviews>
<constraints>
<constraint firstItem="m5m-Zs-j0z" firstAttribute="leading" secondItem="iyM-TJ-JB4" secondAttribute="leadingMargin" id="9cK-nj-70V"/>
<constraint firstAttribute="trailingMargin" secondItem="m5m-Zs-j0z" secondAttribute="trailing" id="JsE-8v-XAS"/>
<constraint firstAttribute="bottomMargin" secondItem="m5m-Zs-j0z" secondAttribute="bottom" id="KbI-Yl-Las"/>
<constraint firstItem="m5m-Zs-j0z" firstAttribute="top" secondItem="iyM-TJ-JB4" secondAttribute="topMargin" id="v2S-en-21g"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Score" id="Urb-uF-sj1">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" id="vp3-Ft-Gth">
<rect key="frame" x="20" y="122" width="374" height="40.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="vp3-Ft-Gth" id="ccO-0O-u72">
<rect key="frame" x="0.0" y="0.0" width="374" height="40.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" horizontalCompressionResistancePriority="250" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="0" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="rZ9-Fa-cyr">
<rect key="frame" x="20" y="11" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="numberPad" returnKeyType="done"/>
<connections>
<action selector="textFieldDidChange:" destination="iNr-Oe-muq" eventType="editingChanged" id="ebo-2z-Z9P"/>
</connections>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="bottomMargin" secondItem="rZ9-Fa-cyr" secondAttribute="bottom" id="3Ya-Po-TQ3"/>
<constraint firstAttribute="trailingMargin" secondItem="rZ9-Fa-cyr" secondAttribute="trailing" id="Gso-FU-Ct1"/>
<constraint firstItem="rZ9-Fa-cyr" firstAttribute="leading" secondItem="ccO-0O-u72" secondAttribute="leadingMargin" id="WrY-St-72v"/>
<constraint firstItem="rZ9-Fa-cyr" firstAttribute="top" secondItem="ccO-0O-u72" secondAttribute="topMargin" id="mmE-f2-e9r"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="iNr-Oe-muq" id="67O-XE-MeV"/>
<outlet property="delegate" destination="iNr-Oe-muq" id="vLD-QQ-zR2"/>
</connections>
</tableView>
<toolbarItems/>
<navigationItem key="navigationItem" id="U4g-xG-W8b">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="VIN-Hi-Twt">
<connections>
<segue destination="lTz-K0-aRY" kind="unwind" unwindAction="cancelPlayerEdition:" id="fOs-ol-qJL"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="2um-u5-BhV">
<connections>
<segue destination="lTz-K0-aRY" kind="unwind" identifier="Commit" unwindAction="commitPlayerEdition:" id="Y1E-dI-wcl"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="cancelButtonItem" destination="VIN-Hi-Twt" id="dET-mF-ZXe"/>
<outlet property="nameCell" destination="2gb-gB-918" id="ch2-MB-uRm"/>
<outlet property="nameTextField" destination="m5m-Zs-j0z" id="A6a-pw-q8s"/>
<outlet property="saveButtonItem" destination="2um-u5-BhV" id="Jym-us-zdP"/>
<outlet property="scoreCell" destination="vp3-Ft-Gth" id="b8A-42-8Dw"/>
<outlet property="scoreTextField" destination="rZ9-Fa-cyr" id="4kw-gV-uih"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="fNC-Ls-uRw" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="lTz-K0-aRY" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="1134" y="2891"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="2KX-IU-Z5Q">
<objects>
<navigationController id="ein-Sr-i8w" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" id="zIT-dB-Mfo">
<rect key="frame" x="0.0" y="0.0" width="414" height="56"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="iNr-Oe-muq" kind="relationship" relationship="rootViewController" destinationCreationSelector="makePlayerCreationViewController:" id="e5b-3U-9Vz"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kNr-vk-I5l" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1134" y="2071"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="N1X-4j-Re4">
<objects>
<navigationController id="UAh-8N-CM8" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="U7d-1C-GnL">
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="PJp-Eq-ZjA" kind="relationship" relationship="rootViewController" id="klT-ZP-gtX"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="sVt-e9-ghQ" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-459" y="2071"/>
</scene>
<!--Hall Of Fame-->
<scene sceneID="9su-ms-vdJ">
<objects>
<tableViewController id="PJp-Eq-ZjA" customClass="PlayerListViewController" customModule="GRDBDemoiOS" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="-1" estimatedSectionHeaderHeight="-1" sectionFooterHeight="-1" estimatedSectionFooterHeight="-1" id="tX0-ON-WSI">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Player" textLabel="G8y-VE-YRu" detailTextLabel="NNc-J4-xiE" style="IBUITableViewCellStyleValue1" id="bFj-Hd-fwd">
<rect key="frame" x="0.0" y="44.5" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="bFj-Hd-fwd" id="kDf-TZ-xhb">
<rect key="frame" x="0.0" y="0.0" width="384.5" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="G8y-VE-YRu">
<rect key="frame" x="20" y="14" width="28.5" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="NNc-J4-xiE">
<rect key="frame" x="339" y="14" width="37.5" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="iNr-Oe-muq" kind="show" identifier="" destinationCreationSelector="makePlayerEditionViewController:" id="Lgu-Yd-HSy"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="PJp-Eq-ZjA" id="MkQ-WQ-sTh"/>
<outlet property="delegate" destination="PJp-Eq-ZjA" id="quj-Sp-DuL"/>
</connections>
</tableView>
<toolbarItems>
<barButtonItem title="Item" id="hMF-Rv-dAe"/>
</toolbarItems>
<navigationItem key="navigationItem" title="Hall Of Fame" id="QIv-M6-kT1">
<barButtonItem key="leftBarButtonItem" systemItem="add" id="2bK-Fl-55Q">
<connections>
<segue destination="ein-Sr-i8w" kind="presentation" identifier="" id="ehP-ZN-Nt4"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="newPlayerButtonItem" destination="2bK-Fl-55Q" id="O9m-8s-oRL"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="af5-Qo-AkD" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="348" y="2071"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="Lgu-Yd-HSy"/>
</inferredMetricsTieBreakers>
<resources>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
@@ -0,0 +1,40 @@
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
@@ -0,0 +1,128 @@
import UIKit
class PlayerEditionViewController: UITableViewController {
enum Mode {
/// Edition ends with the "Commit" unwind segue.
case creation
/// Edition ends when user hits the back button.
case edition
}
/// The edited player
private(set) var player: Player
/// The presentation mode
let mode: Mode
@IBOutlet private weak var cancelButtonItem: UIBarButtonItem!
@IBOutlet private weak var saveButtonItem: UIBarButtonItem!
@IBOutlet private weak var nameCell: UITableViewCell!
@IBOutlet private weak var nameTextField: UITextField!
@IBOutlet private weak var scoreCell: UITableViewCell!
@IBOutlet private weak var scoreTextField: UITextField!
init?(_ coder: NSCoder, mode: Mode, player: Player) {
self.mode = mode
self.player = player
super.init(coder: coder)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
configureNavigationItem()
configureForm()
}
}
// MARK: - Navigation
extension PlayerEditionViewController {
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
// Force keyboard to dismiss early
view.endEditing(true)
return true
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "Commit" {
saveChanges()
}
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if mode == .edition, parent == nil {
// Self is popping from its navigation controller
saveChanges()
}
}
private func configureNavigationItem() {
switch mode {
case .creation:
navigationItem.title = "New Player"
navigationItem.leftBarButtonItem = cancelButtonItem
navigationItem.rightBarButtonItem = saveButtonItem
case .edition:
navigationItem.title = player.name
navigationItem.leftBarButtonItem = nil
navigationItem.rightBarButtonItem = nil
}
}
}
// MARK: - Form
extension PlayerEditionViewController: UITextFieldDelegate {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
nameTextField.becomeFirstResponder()
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
let cell = tableView.cellForRow(at: indexPath)
if cell === nameCell {
nameTextField.becomeFirstResponder()
} else if cell === scoreCell {
scoreTextField.becomeFirstResponder()
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == nameTextField {
scoreTextField.becomeFirstResponder()
}
return false
}
@IBAction func textFieldDidChange(_ textField: UITextField) {
// User has edited the player: prevent interactive dismissal
isModalInPresentation = true
}
private func configureForm() {
nameTextField.text = player.name
if player.score == 0 && player.id == nil {
scoreTextField.text = ""
} else {
scoreTextField.text = "\(player.score)"
}
}
private func saveChanges() {
var player = self.player
player.name = nameTextField.text ?? ""
player.score = scoreTextField.text.flatMap { Int($0) } ?? 0
try! AppDatabase.shared.savePlayer(&player)
self.player = player
}
}
@@ -0,0 +1,194 @@
import UIKit
import GRDB
/// PlayerListViewController displays the list of players.
class PlayerListViewController: UITableViewController {
private enum PlayerOrdering {
case byName
case byScore
}
@IBOutlet private weak var newPlayerButtonItem: UIBarButtonItem!
private var dataSource: PlayerDataSource!
private var playersCancellable: DatabaseCancellable?
private var playerOrdering: PlayerOrdering = .byScore {
didSet {
configureOrderingBarButtonItem()
observePlayers()
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureToolbar()
configureNavigationItem()
configureDataSource()
observePlayers()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isToolbarHidden = false
}
private func configureToolbar() {
toolbarItems = [
UIBarButtonItem(systemItem: .trash, primaryAction: UIAction { [unowned self] _ in
setEditing(false, animated: true)
try! AppDatabase.shared.deleteAllPlayers()
}),
UIBarButtonItem(systemItem: .flexibleSpace),
UIBarButtonItem(systemItem: .refresh, primaryAction: UIAction { [unowned self] _ in
setEditing(false, animated: true)
try! AppDatabase.shared.refreshPlayers()
}),
UIBarButtonItem(systemItem: .flexibleSpace),
UIBarButtonItem(image: UIImage(systemName: "tornado"), primaryAction: UIAction { [unowned self] _ in
setEditing(false, animated: true)
for _ in 0..<50 {
DispatchQueue.global().async {
try! AppDatabase.shared.refreshPlayers()
}
}
}),
]
}
private func configureNavigationItem() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Players")
navigationItem.leftBarButtonItems = [editButtonItem, newPlayerButtonItem]
configureOrderingBarButtonItem()
}
private func configureOrderingBarButtonItem() {
switch playerOrdering {
case .byScore:
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "Score ▼",
primaryAction: UIAction { [unowned self] _ in
setEditing(false, animated: true)
playerOrdering = .byName
})
case .byName:
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "Name ▲",
primaryAction: UIAction { [unowned self] _ in
setEditing(false, animated: true)
playerOrdering = .byScore
})
}
}
private func configureDataSource() {
dataSource = PlayerDataSource(tableView: tableView) { (tableView, indexPath, player) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Player", for: indexPath)
if player.name.isEmpty {
cell.textLabel?.text = "(anonymous)"
} else {
cell.textLabel?.text = player.name
}
cell.detailTextLabel?.text = abs(player.score) > 1 ? "\(player.score) points" : "0 point"
return cell
}
dataSource.defaultRowAnimation = .fade
tableView.dataSource = dataSource
}
private func configureTitle(from players: [Player]) {
switch players.count {
case 0:
navigationItem.title = "No Player"
case 1:
navigationItem.title = "1 Player"
case let count:
navigationItem.title = "\(count) Players"
}
}
private func configureDataSource(from players: [Player]) {
var snapshot = NSDiffableDataSourceSnapshot<Int, Player>()
snapshot.appendSections([0])
snapshot.appendItems(players, toSection: 0)
// Remember selection
let selectedPlayerId = tableView.indexPathForSelectedRow.flatMap {
dataSource.itemIdentifier(for: $0)?.id
}
// Avoid a UIKit warning; don't animate when popping from edition
let animated = view.window != nil
dataSource.apply(snapshot, animatingDifferences: animated, completion: {
// Restore selection
if let index = players.firstIndex(where: { $0.id == selectedPlayerId }) {
self.tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none)
}
})
}
private func observePlayers() {
let request: QueryInterfaceRequest<Player>
switch playerOrdering {
case .byName:
request = Player.all().orderedByName()
case .byScore:
request = Player.all().orderedByScore()
}
playersCancellable = ValueObservation
.tracking(request.fetchAll(_:))
.start(
in: AppDatabase.shared.reader,
// Immediate scheduling feeds the data source right on subscription,
// and avoids an undesired animation when the application starts.
scheduling: .immediate,
onError: { error in fatalError("Unexpected error: \(error)") },
onChange: { [weak self] players in
guard let self else { return }
self.configureTitle(from: players)
self.configureDataSource(from: players)
})
}
}
// MARK: - Navigation
extension PlayerListViewController {
@IBSegueAction func makePlayerEditionViewController(_ coder: NSCoder) -> PlayerEditionViewController? {
guard let indexPath = tableView.indexPathForSelectedRow,
let player = dataSource.itemIdentifier(for: indexPath)
else { return nil }
return PlayerEditionViewController(coder, mode: .edition, player: player)
}
@IBSegueAction func makePlayerCreationViewController(_ coder: NSCoder) -> PlayerEditionViewController? {
let player = Player(id: nil, name: "", score: 0)
return PlayerEditionViewController(coder, mode: .creation, player: player)
}
@IBAction func cancelPlayerEdition(_ segue: UIStoryboardSegue) {
// Player creation cancelled
}
@IBAction func commitPlayerEdition(_ segue: UIStoryboardSegue) {
// Player creation committed
}
}
// MARK: - UITableViewDataSource
/// Subclass of UITableViewDiffableDataSource that supports row deletion
private class PlayerDataSource: UITableViewDiffableDataSource<Int, Player> {
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
// Delete the player
if let player = itemIdentifier(for: indexPath), let id = player.id {
try! AppDatabase.shared.deletePlayers(ids: [id])
}
}
}
@@ -0,0 +1,146 @@
import XCTest
import GRDB
@testable import GRDBDemoiOS
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() 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 appDatabase.savePlayer(&player)
// Then the player exists in the database
try XCTAssertTrue(dbQueue.read(player.exists))
}
func test_savePlayer_updates() throws {
// Given a players database that contains a 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 modify and save the player
player.name = "Barbara"
player.score = 1000
try appDatabase.savePlayer(&player)
// Then the player has been updated in the database
let fetchedPlayer = try dbQueue.read { db in
try XCTUnwrap(Player.fetchOne(db, key: player.id))
}
XCTAssertEqual(fetchedPlayer, player)
}
func test_deletePlayers() throws {
// Given a players database that contains four players
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
var player1 = Player(id: nil, name: "Arthur", score: 100)
var player2 = Player(id: nil, name: "Barbara", score: 200)
var player3 = Player(id: nil, name: "Craig", score: 150)
var player4 = Player(id: nil, 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 delete two players
try appDatabase.deletePlayers(ids: [player1.id!, player3.id!])
// Then the deleted players no longer exist
try dbQueue.read { db in
try XCTAssertFalse(player1.exists(db))
try XCTAssertFalse(player3.exists(db))
}
// Then the database still contains two players
try XCTAssertEqual(dbQueue.read(Player.fetchCount), 2)
}
func test_deleteAllPlayers() throws {
// Given a players database that contains players
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
var player1 = Player(id: nil, name: "Arthur", score: 100)
var player2 = Player(id: nil, name: "Barbara", score: 200)
var player3 = Player(id: nil, name: "Craig", score: 150)
var player4 = Player(id: nil, 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 delete all players
try appDatabase.deleteAllPlayers()
// Then the database does not contain any player
try XCTAssertEqual(dbQueue.read(Player.fetchCount), 0)
}
func test_refreshPlayers_populates_an_empty_database() throws {
// Given an empty players database
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
let appDatabase = try AppDatabase(dbQueue)
// When we refresh players
try appDatabase.refreshPlayers()
// Then the database is not empty
try XCTAssert(dbQueue.read(Player.fetchCount) > 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,108 @@
import XCTest
import GRDB
@testable import GRDBDemoiOS
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,45 @@
UIKit Demo Application
======================
<img align="right" src="https://github.com/groue/GRDB.swift/raw/master/Documentation/DemoApps/GRDBDemoiOS/Screenshot.png" width="50%">
**This demo application is a storyboard-based UIKit application.** For a demo application that uses Combine + SwiftUI, see [GRDBCombineDemo](../GRDBCombineDemo/README.md), and for Async/Await + SwiftUI, see [GRDBAsyncDemo](../GRDBAsyncDemo/README.md).
**Requirements**: iOS 15.0+ / Xcode 12+
> **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 table view with [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation).
- How to apply the recommendations of [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices).
**Files of interest:**
- [AppDatabase.swift](GRDBDemoiOS/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, and [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation) in order to let the application observe database changes.
- [Persistence.swift](GRDBDemoiOS/Persistence.swift)
This file defines the `AppDatabase` instance used by the application.
- [Player.swift](GRDBDemoiOS/Player.swift)
`Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records). It defines the database requests used by the application.
- [PlayerListViewController.swift](GRDBDemoiOS/ViewControllers/PlayerListViewController.swift)
`PlayerListViewController` displays the list of players.
- [PlayerEditionViewController.swift](GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift)
`PlayerEditionViewController` can create or edit a player, and save it in the database.
- [GRDBDemoiOSTests](GRDBDemoiOSTests)
- Test the database schema
- Test the `Player` record and its requests
- Test the `AppDatabase` methods that let the app access the database.
Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB