add swiftUI code
@@ -0,0 +1,611 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */; };
|
||||
56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */; };
|
||||
56519DCA274FC8E900ED16D8 /* GRDBQuery in Frameworks */ = {isa = PBXBuildFile; productRef = 56519DC9274FC8E900ED16D8 /* GRDBQuery */; };
|
||||
5671723A261B23C800423B6F /* PlayerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717239261B23C800423B6F /* PlayerList.swift */; };
|
||||
56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717251261B334D00423B6F /* PlayerRequestTests.swift */; };
|
||||
567C3E1A2520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */; };
|
||||
567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */; };
|
||||
567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */; };
|
||||
567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E532520B75C0011F6E9 /* Player.swift */; };
|
||||
567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E542520B75C0011F6E9 /* Persistence.swift */; };
|
||||
567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */; };
|
||||
567C3E622520B75D0011F6E9 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5A2520B75C0011F6E9 /* AppView.swift */; };
|
||||
567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */; };
|
||||
567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */; };
|
||||
567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E652520B7880011F6E9 /* AppDatabase.swift */; };
|
||||
567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E752520BB650011F6E9 /* Localizable.stringsdict */; };
|
||||
567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */; };
|
||||
56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */; };
|
||||
56F8A13527359A5A0011ACBE /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56F8A13427359A5A0011ACBE /* GRDB */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 567C3E0E2520B6DE0011F6E9 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 567C3E152520B6DE0011F6E9;
|
||||
remoteInfo = GRDBAsyncDemo;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
567C3E502520B70E0011F6E9 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBAsyncDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
56026C9C25B8A7D000D1DF3F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = "<group>"; };
|
||||
56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = "<group>"; };
|
||||
56717239261B23C800423B6F /* PlayerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerList.swift; sourceTree = "<group>"; };
|
||||
56717251261B334D00423B6F /* PlayerRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequestTests.swift; sourceTree = "<group>"; };
|
||||
567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBAsyncDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBAsyncDemoApp.swift; sourceTree = "<group>"; };
|
||||
567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
567C3E222520B6DF0011F6E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
567C3E532520B75C0011F6E9 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
|
||||
567C3E542520B75C0011F6E9 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
|
||||
567C3E592520B75C0011F6E9 /* PlayerFormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerFormView.swift; sourceTree = "<group>"; };
|
||||
567C3E5A2520B75C0011F6E9 /* AppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
|
||||
567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerCreationView.swift; sourceTree = "<group>"; };
|
||||
567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionView.swift; sourceTree = "<group>"; };
|
||||
567C3E652520B7880011F6E9 /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = "<group>"; };
|
||||
567C3E762520BB650011F6E9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
567C3E782520BB650011F6E9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
56B6D1082619EC1B003CC455 /* PlayerRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequest.swift; sourceTree = "<group>"; };
|
||||
56F8A12E27359A350011ACBE /* GRDB.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GRDB.swift; path = ../../..; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
56026C9525B8A7D000D1DF3F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
567C3E132520B6DE0011F6E9 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56519DCA274FC8E900ED16D8 /* GRDBQuery in Frameworks */,
|
||||
56F8A13527359A5A0011ACBE /* GRDB in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
56026C9925B8A7D000D1DF3F /* GRDBAsyncDemoTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56026C9C25B8A7D000D1DF3F /* Info.plist */,
|
||||
56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */,
|
||||
56717251261B334D00423B6F /* PlayerRequestTests.swift */,
|
||||
56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */,
|
||||
);
|
||||
path = GRDBAsyncDemoTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
56185BC125B8047D00B9C30F /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */,
|
||||
567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */,
|
||||
567C3E752520BB650011F6E9 /* Localizable.stringsdict */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567C3E0D2520B6DE0011F6E9 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
567C3E182520B6DE0011F6E9 /* GRDBAsyncDemo */,
|
||||
56026C9925B8A7D000D1DF3F /* GRDBAsyncDemoTests */,
|
||||
567C3E172520B6DE0011F6E9 /* Products */,
|
||||
567C3E4D2520B70E0011F6E9 /* Frameworks */,
|
||||
56F8A12E27359A350011ACBE /* GRDB.swift */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567C3E172520B6DE0011F6E9 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */,
|
||||
56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567C3E182520B6DE0011F6E9 /* GRDBAsyncDemo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
567C3E222520B6DF0011F6E9 /* Info.plist */,
|
||||
567C3E652520B7880011F6E9 /* AppDatabase.swift */,
|
||||
567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */,
|
||||
567C3E542520B75C0011F6E9 /* Persistence.swift */,
|
||||
567C3E532520B75C0011F6E9 /* Player.swift */,
|
||||
56B6D1082619EC1B003CC455 /* PlayerRequest.swift */,
|
||||
567C3E1F2520B6DF0011F6E9 /* Preview Content */,
|
||||
56185BC125B8047D00B9C30F /* Resources */,
|
||||
567C3E582520B75C0011F6E9 /* Views */,
|
||||
);
|
||||
path = GRDBAsyncDemo;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567C3E1F2520B6DF0011F6E9 /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567C3E4D2520B70E0011F6E9 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567C3E582520B75C0011F6E9 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
567C3E5A2520B75C0011F6E9 /* AppView.swift */,
|
||||
567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */,
|
||||
567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */,
|
||||
567C3E592520B75C0011F6E9 /* PlayerFormView.swift */,
|
||||
56717239261B23C800423B6F /* PlayerList.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
56026C9725B8A7D000D1DF3F /* GRDBAsyncDemoTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBAsyncDemoTests" */;
|
||||
buildPhases = (
|
||||
56026C9425B8A7D000D1DF3F /* Sources */,
|
||||
56026C9525B8A7D000D1DF3F /* Frameworks */,
|
||||
56026C9625B8A7D000D1DF3F /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */,
|
||||
);
|
||||
name = GRDBAsyncDemoTests;
|
||||
productName = GRDBAsyncDemoTests;
|
||||
productReference = 56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBAsyncDemo" */;
|
||||
buildPhases = (
|
||||
567C3E122520B6DE0011F6E9 /* Sources */,
|
||||
567C3E132520B6DE0011F6E9 /* Frameworks */,
|
||||
567C3E142520B6DE0011F6E9 /* Resources */,
|
||||
567C3E502520B70E0011F6E9 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
56F8A13127359A540011ACBE /* PBXTargetDependency */,
|
||||
);
|
||||
name = GRDBAsyncDemo;
|
||||
packageProductDependencies = (
|
||||
56F8A13427359A5A0011ACBE /* GRDB */,
|
||||
56519DC9274FC8E900ED16D8 /* GRDBQuery */,
|
||||
);
|
||||
productName = GRBCombineDemo;
|
||||
productReference = 567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
567C3E0E2520B6DE0011F6E9 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1250;
|
||||
LastUpgradeCheck = 1200;
|
||||
TargetAttributes = {
|
||||
56026C9725B8A7D000D1DF3F = {
|
||||
CreatedOnToolsVersion = 12.3;
|
||||
TestTargetID = 567C3E152520B6DE0011F6E9;
|
||||
};
|
||||
567C3E152520B6DE0011F6E9 = {
|
||||
CreatedOnToolsVersion = 12.0;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBAsyncDemo" */;
|
||||
compatibilityVersion = "Xcode 12.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 567C3E0D2520B6DE0011F6E9;
|
||||
packageReferences = (
|
||||
56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */,
|
||||
);
|
||||
productRefGroup = 567C3E172520B6DE0011F6E9 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */,
|
||||
56026C9725B8A7D000D1DF3F /* GRDBAsyncDemoTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
56026C9625B8A7D000D1DF3F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
567C3E142520B6DE0011F6E9 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */,
|
||||
567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */,
|
||||
567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */,
|
||||
567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
56026C9425B8A7D000D1DF3F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */,
|
||||
56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */,
|
||||
56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
567C3E122520B6DE0011F6E9 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */,
|
||||
567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */,
|
||||
56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */,
|
||||
5671723A261B23C800423B6F /* PlayerList.swift in Sources */,
|
||||
567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */,
|
||||
567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */,
|
||||
567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */,
|
||||
567C3E622520B75D0011F6E9 /* AppView.swift in Sources */,
|
||||
567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */,
|
||||
567C3E1A2520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */;
|
||||
targetProxy = 56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */;
|
||||
};
|
||||
56F8A13127359A540011ACBE /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
productRef = 56F8A13027359A540011ACBE /* GRDB */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
567C3E752520BB650011F6E9 /* Localizable.stringsdict */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
567C3E762520BB650011F6E9 /* en */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
567C3E782520BB650011F6E9 /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
56026C9F25B8A7D000D1DF3F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = GRDBAsyncDemoTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemoTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBAsyncDemo.app/GRDBAsyncDemo";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
56026CA025B8A7D000D1DF3F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = GRDBAsyncDemoTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemoTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBAsyncDemo.app/GRDBAsyncDemo";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
567C3E232520B6DF0011F6E9 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
567C3E242520B6DF0011F6E9 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
567C3E262520B6DF0011F6E9 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"GRDBAsyncDemo/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = GRDBAsyncDemo/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
567C3E272520B6DF0011F6E9 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"GRDBAsyncDemo/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = GRDBAsyncDemo/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBAsyncDemoTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
56026C9F25B8A7D000D1DF3F /* Debug */,
|
||||
56026CA025B8A7D000D1DF3F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBAsyncDemo" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
567C3E232520B6DF0011F6E9 /* Debug */,
|
||||
567C3E242520B6DF0011F6E9 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBAsyncDemo" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
567C3E262520B6DF0011F6E9 /* Debug */,
|
||||
567C3E272520B6DF0011F6E9 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/groue/GRDBQuery";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.6.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
56519DC9274FC8E900ED16D8 /* GRDBQuery */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */;
|
||||
productName = GRDBQuery;
|
||||
};
|
||||
56F8A13027359A540011ACBE /* GRDB */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = GRDB;
|
||||
};
|
||||
56F8A13427359A5A0011ACBE /* GRDB */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = GRDB;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 567C3E0E2520B6DE0011F6E9 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "grdbquery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/groue/GRDBQuery",
|
||||
"state" : {
|
||||
"revision" : "a6c46dd38ecf11a5c37732870dc03a384d582fba",
|
||||
"version" : "0.9.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
|
||||
BuildableName = "GRDBAsyncDemo.app"
|
||||
BlueprintName = "GRDBAsyncDemo"
|
||||
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "56026C9725B8A7D000D1DF3F"
|
||||
BuildableName = "GRDBAsyncDemoTests.xctest"
|
||||
BlueprintName = "GRDBAsyncDemoTests"
|
||||
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
|
||||
BuildableName = "GRDBAsyncDemo.app"
|
||||
BlueprintName = "GRDBAsyncDemo"
|
||||
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "SQL_TRACE"
|
||||
value = "1"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "567C3E152520B6DE0011F6E9"
|
||||
BuildableName = "GRDBAsyncDemo.app"
|
||||
BlueprintName = "GRDBAsyncDemo"
|
||||
ReferencedContainer = "container:GRDBAsyncDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,235 @@
|
||||
import Foundation
|
||||
import GRDB
|
||||
import os.log
|
||||
|
||||
/// A database of players.
|
||||
///
|
||||
/// You create an `AppDatabase` with a connection to an SQLite database
|
||||
/// (see <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>).
|
||||
///
|
||||
/// Create those connections with a configuration returned from
|
||||
/// `AppDatabase/makeConfiguration(_:)`.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Create an in-memory AppDatabase
|
||||
/// let config = AppDatabase.makeConfiguration()
|
||||
/// let dbQueue = try DatabaseQueue(configuration: config)
|
||||
/// let appDatabase = try AppDatabase(dbQueue)
|
||||
/// ```
|
||||
struct AppDatabase {
|
||||
/// Creates an `AppDatabase`, and makes sure the database schema
|
||||
/// is ready.
|
||||
///
|
||||
/// - important: Create the `DatabaseWriter` with a configuration
|
||||
/// returned by ``makeConfiguration(_:)``.
|
||||
init(_ dbWriter: any DatabaseWriter) throws {
|
||||
self.dbWriter = dbWriter
|
||||
try migrator.migrate(dbWriter)
|
||||
}
|
||||
|
||||
/// Provides access to the database.
|
||||
///
|
||||
/// Application can use a `DatabasePool`, while SwiftUI previews and tests
|
||||
/// can use a fast in-memory `DatabaseQueue`.
|
||||
///
|
||||
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>
|
||||
private let dbWriter: any DatabaseWriter
|
||||
}
|
||||
|
||||
// MARK: - Database Configuration
|
||||
|
||||
extension AppDatabase {
|
||||
private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL")
|
||||
|
||||
/// Returns a database configuration suited for `PlayerRepository`.
|
||||
///
|
||||
/// SQL statements are logged if the `SQL_TRACE` environment variable
|
||||
/// is set.
|
||||
///
|
||||
/// - parameter base: A base configuration.
|
||||
public static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration {
|
||||
var config = base
|
||||
|
||||
// An opportunity to add required custom SQL functions or
|
||||
// collations, if needed:
|
||||
// config.prepareDatabase { db in
|
||||
// db.add(function: ...)
|
||||
// }
|
||||
|
||||
// Log SQL statements if the `SQL_TRACE` environment variable is set.
|
||||
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/database/trace(options:_:)>
|
||||
if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil {
|
||||
config.prepareDatabase { db in
|
||||
db.trace {
|
||||
// It's ok to log statements publicly. Sensitive
|
||||
// information (statement arguments) are not logged
|
||||
// unless config.publicStatementArguments is set
|
||||
// (see below).
|
||||
os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// Protect sensitive information by enabling verbose debugging in
|
||||
// DEBUG builds only.
|
||||
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/configuration/publicstatementarguments>
|
||||
config.publicStatementArguments = true
|
||||
#endif
|
||||
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Database Migrations
|
||||
|
||||
extension AppDatabase {
|
||||
/// The DatabaseMigrator that defines the database schema.
|
||||
///
|
||||
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations>
|
||||
private var migrator: DatabaseMigrator {
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
#if DEBUG
|
||||
// Speed up development by nuking the database when migrations change
|
||||
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations>
|
||||
migrator.eraseDatabaseOnSchemaChange = true
|
||||
#endif
|
||||
|
||||
migrator.registerMigration("createPlayer") { db in
|
||||
// Create a table
|
||||
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseschema>
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer).notNull()
|
||||
}
|
||||
}
|
||||
|
||||
// Migrations for future application versions will be inserted here:
|
||||
// migrator.registerMigration(...) { db in
|
||||
// ...
|
||||
// }
|
||||
|
||||
return migrator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Database Access: Writes
|
||||
// The write methods execute invariant-preserving database transactions.
|
||||
|
||||
extension AppDatabase {
|
||||
/// A validation error that prevents some players from being saved into
|
||||
/// the database.
|
||||
enum ValidationError: LocalizedError {
|
||||
case missingName
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .missingName:
|
||||
return "Please provide a name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves (inserts or updates) a player. When the method returns, the
|
||||
/// player is present in the database, and its id is not nil.
|
||||
func savePlayer(_ player: inout Player) async throws {
|
||||
if player.name.isEmpty {
|
||||
throw ValidationError.missingName
|
||||
}
|
||||
player = try await dbWriter.write { [player] db in
|
||||
try player.saved(db)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the specified players
|
||||
func deletePlayers(ids: [Int64]) async throws {
|
||||
try await dbWriter.write { db in
|
||||
_ = try Player.deleteAll(db, ids: ids)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete all players
|
||||
func deleteAllPlayers() async throws {
|
||||
try await dbWriter.write { db in
|
||||
_ = try Player.deleteAll(db)
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh all players (by performing some random changes, for demo purpose).
|
||||
func refreshPlayers() async throws {
|
||||
try await dbWriter.write { db in
|
||||
if try Player.all().isEmpty(db) {
|
||||
// When database is empty, insert new random players
|
||||
try createRandomPlayers(db)
|
||||
} else {
|
||||
// Insert a player
|
||||
if Bool.random() {
|
||||
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
|
||||
}
|
||||
|
||||
// Delete a random player
|
||||
if Bool.random() {
|
||||
try Player.order(sql: "RANDOM()").limit(1).deleteAll(db)
|
||||
}
|
||||
|
||||
// Update some players
|
||||
for var player in try Player.fetchAll(db) where Bool.random() {
|
||||
try player.updateChanges(db) {
|
||||
$0.score = Player.randomScore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create random players if the database is empty.
|
||||
func createRandomPlayersIfEmpty() throws {
|
||||
try dbWriter.write { db in
|
||||
if try Player.all().isEmpty(db) {
|
||||
try createRandomPlayers(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static let uiTestPlayers = [
|
||||
Player(id: nil, name: "Arthur", score: 5),
|
||||
Player(id: nil, name: "Barbara", score: 6),
|
||||
Player(id: nil, name: "Craig", score: 8),
|
||||
Player(id: nil, name: "David", score: 4),
|
||||
Player(id: nil, name: "Elena", score: 1),
|
||||
Player(id: nil, name: "Frederik", score: 2),
|
||||
Player(id: nil, name: "Gilbert", score: 7),
|
||||
Player(id: nil, name: "Henriette", score: 3)]
|
||||
|
||||
func createPlayersForUITests() throws {
|
||||
try dbWriter.write { db in
|
||||
try AppDatabase.uiTestPlayers.forEach { player in
|
||||
_ = try player.inserted(db) // insert but ignore inserted id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`.
|
||||
private func createRandomPlayers(_ db: Database) throws {
|
||||
for _ in 0..<8 {
|
||||
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Database Access: Reads
|
||||
|
||||
// This demo app does not provide any specific reading method, and instead
|
||||
// gives an unrestricted read-only access to the rest of the application.
|
||||
// In your app, you are free to choose another path, and define focused
|
||||
// reading methods.
|
||||
extension AppDatabase {
|
||||
/// Provides a read-only access to the database
|
||||
var reader: DatabaseReader {
|
||||
dbWriter
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import GRDBQuery
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct GRDBAsyncDemoApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
AppView().appDatabase(.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Give SwiftUI access to the database
|
||||
|
||||
private struct AppDatabaseKey: EnvironmentKey {
|
||||
static var defaultValue: AppDatabase { .empty() }
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var appDatabase: AppDatabase {
|
||||
get { self[AppDatabaseKey.self] }
|
||||
set { self[AppDatabaseKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func appDatabase(_ appDatabase: AppDatabase) -> some View {
|
||||
self
|
||||
.environment(\.appDatabase, appDatabase)
|
||||
.databaseContext(.readOnly { appDatabase.reader })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
extension AppDatabase {
|
||||
/// The database for the application
|
||||
static let shared = makeShared()
|
||||
|
||||
private static func makeShared() -> AppDatabase {
|
||||
do {
|
||||
// Apply recommendations from
|
||||
// <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>
|
||||
//
|
||||
// Create the "Application Support/Database" directory if needed
|
||||
let fileManager = FileManager.default
|
||||
let appSupportURL = try fileManager.url(
|
||||
for: .applicationSupportDirectory, in: .userDomainMask,
|
||||
appropriateFor: nil, create: true)
|
||||
let directoryURL = appSupportURL.appendingPathComponent("Database", isDirectory: true)
|
||||
|
||||
// Support for tests: delete the database if requested
|
||||
if CommandLine.arguments.contains("-reset") {
|
||||
try? fileManager.removeItem(at: directoryURL)
|
||||
}
|
||||
|
||||
// Create the database folder if needed
|
||||
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||
|
||||
// Open or create the database
|
||||
let databaseURL = directoryURL.appendingPathComponent("db.sqlite")
|
||||
NSLog("Database stored at \(databaseURL.path)")
|
||||
let dbPool = try DatabasePool(
|
||||
path: databaseURL.path,
|
||||
// Use default AppDatabase configuration
|
||||
configuration: AppDatabase.makeConfiguration())
|
||||
|
||||
// Create the AppDatabase
|
||||
let appDatabase = try AppDatabase(dbPool)
|
||||
|
||||
// Prepare the database with test fixtures if requested
|
||||
if CommandLine.arguments.contains("-fixedTestData") {
|
||||
try appDatabase.createPlayersForUITests()
|
||||
} else {
|
||||
// Otherwise, populate the database if it is empty, for better
|
||||
// demo purpose.
|
||||
try appDatabase.createRandomPlayersIfEmpty()
|
||||
}
|
||||
|
||||
return appDatabase
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate.
|
||||
//
|
||||
// Typical reasons for an error here include:
|
||||
// * The parent directory cannot be created, or disallows writing.
|
||||
// * The database is not accessible, due to permissions or data protection when the device is locked.
|
||||
// * The device is out of space.
|
||||
// * The database could not be migrated to its latest schema version.
|
||||
// Check the error message to determine what the actual problem was.
|
||||
fatalError("Unresolved error \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an empty database for SwiftUI previews
|
||||
static func empty() -> AppDatabase {
|
||||
// Connect to an in-memory database
|
||||
// See https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections
|
||||
let dbQueue = try! DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
return try! AppDatabase(dbQueue)
|
||||
}
|
||||
|
||||
/// Creates a database full of random players for SwiftUI previews
|
||||
static func random() -> AppDatabase {
|
||||
let appDatabase = empty()
|
||||
try! appDatabase.createRandomPlayersIfEmpty()
|
||||
return appDatabase
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import GRDB
|
||||
|
||||
/// The Player struct.
|
||||
///
|
||||
/// Identifiable conformance supports SwiftUI list animations, and type-safe
|
||||
/// GRDB primary key methods.
|
||||
/// Equatable conformance supports tests.
|
||||
struct Player: Identifiable, Equatable {
|
||||
/// The player id.
|
||||
///
|
||||
/// Int64 is the recommended type for auto-incremented database ids.
|
||||
/// Use nil for players that are not inserted yet in the database.
|
||||
var id: Int64?
|
||||
var name: String
|
||||
var score: Int
|
||||
}
|
||||
|
||||
extension Player {
|
||||
private static let names = [
|
||||
"Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David",
|
||||
"Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette",
|
||||
"Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl",
|
||||
"Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole",
|
||||
"Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul",
|
||||
"Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain",
|
||||
"Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann",
|
||||
"Zazie", "Zoé"]
|
||||
|
||||
/// Creates a new player with empty name and zero score
|
||||
static func new() -> Player {
|
||||
Player(id: nil, name: "", score: 0)
|
||||
}
|
||||
|
||||
/// Creates a new player with random name and random score
|
||||
static func makeRandom() -> Player {
|
||||
Player(id: nil, name: randomName(), score: randomScore())
|
||||
}
|
||||
|
||||
/// Returns a random name
|
||||
static func randomName() -> String {
|
||||
names.randomElement()!
|
||||
}
|
||||
|
||||
/// Returns a random score
|
||||
static func randomScore() -> Int {
|
||||
10 * Int.random(in: 0...100)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Persistence
|
||||
|
||||
/// Make Player a Codable Record.
|
||||
///
|
||||
/// See <https://github.com/groue/GRDB.swift/blob/master/README.md#records>
|
||||
extension Player: Codable, FetchableRecord, MutablePersistableRecord {
|
||||
// Define database columns from CodingKeys
|
||||
fileprivate enum Columns {
|
||||
static let name = Column(CodingKeys.name)
|
||||
static let score = Column(CodingKeys.score)
|
||||
}
|
||||
|
||||
/// Updates a player id after it has been inserted in the database.
|
||||
mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
id = inserted.rowID
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Player Database Requests
|
||||
|
||||
/// Define some player requests used by the application.
|
||||
///
|
||||
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices>
|
||||
extension DerivableRequest<Player> {
|
||||
/// A request of players ordered by name.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let players: [Player] = try dbWriter.read { db in
|
||||
/// try Player.all().orderedByName().fetchAll(db)
|
||||
/// }
|
||||
func orderedByName() -> Self {
|
||||
// Sort by name in a localized case insensitive fashion
|
||||
// See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison
|
||||
order(Player.Columns.name.collating(.localizedCaseInsensitiveCompare))
|
||||
}
|
||||
|
||||
/// A request of players ordered by score.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let players: [Player] = try dbWriter.read { db in
|
||||
/// try Player.all().orderedByScore().fetchAll(db)
|
||||
/// }
|
||||
/// let bestPlayer: Player? = try dbWriter.read { db in
|
||||
/// try Player.all().orderedByScore().fetchOne(db)
|
||||
/// }
|
||||
func orderedByScore() -> Self {
|
||||
// Sort by descending score, and then by name, in a
|
||||
// localized case insensitive fashion
|
||||
// See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison
|
||||
order(
|
||||
Player.Columns.score.desc,
|
||||
Player.Columns.name.collating(.localizedCaseInsensitiveCompare))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import GRDB
|
||||
import GRDBQuery
|
||||
|
||||
/// A player request can be used with the `@Query` property wrapper in order to
|
||||
/// feed a view with a list of players.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// struct MyView: View {
|
||||
/// @Query(PlayerRequest(ordering: .byName)) private var players: [Player]
|
||||
///
|
||||
/// var body: some View {
|
||||
/// List(players) { player in ... )
|
||||
/// }
|
||||
/// }
|
||||
struct PlayerRequest: ValueObservationQueryable {
|
||||
enum Ordering {
|
||||
case byScore
|
||||
case byName
|
||||
}
|
||||
|
||||
static var defaultValue: [Player] { [] }
|
||||
|
||||
/// The ordering used by the player request.
|
||||
var ordering: Ordering
|
||||
|
||||
func fetch(_ db: Database) throws -> [Player] {
|
||||
switch ordering {
|
||||
case .byScore:
|
||||
return try Player.all().orderedByScore().fetchAll(db)
|
||||
case .byName:
|
||||
return try Player.all().orderedByName().fetchAll(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon_20pt@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon_20pt@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon_29pt@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon_29pt@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon_40pt@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon_40pt@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon_60pt@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon_60pt@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_20pt.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_20pt@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_29pt.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_29pt@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_40pt.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_40pt@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_76pt.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_76pt@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon_83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 329 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "LaunchIcon.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchIcon" translatesAutoresizingMaskIntoConstraints="NO" id="Sgi-te-PKu">
|
||||
<rect key="frame" x="123.5" y="334.5" width="167" height="237"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Sgi-te-PKu" firstAttribute="centerY" secondItem="6Tk-OE-BBY" secondAttribute="centerY" id="KT7-xd-gV4"/>
|
||||
<constraint firstItem="Sgi-te-PKu" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="feL-Vs-SeN"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchIcon" width="167" height="237"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>%lld Players</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@VARIABLE@</string>
|
||||
<key>VARIABLE</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>lld</string>
|
||||
<key>zero</key>
|
||||
<string>No Player</string>
|
||||
<key>one</key>
|
||||
<string>1 Player</string>
|
||||
<key>other</key>
|
||||
<string>%lld Players</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>%lld points</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@VARIABLE@</string>
|
||||
<key>VARIABLE</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>lld</string>
|
||||
<key>zero</key>
|
||||
<string>0 point</string>
|
||||
<key>one</key>
|
||||
<string>1 point</string>
|
||||
<key>other</key>
|
||||
<string>%lld points</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,154 @@
|
||||
import GRDBQuery
|
||||
import SwiftUI
|
||||
|
||||
/// The main application view
|
||||
struct AppView: View {
|
||||
/// Write access to the database
|
||||
@Environment(\.appDatabase) private var appDatabase
|
||||
|
||||
/// The `players` property is automatically updated when the database changes
|
||||
@Query(PlayerRequest(ordering: .byScore)) private var players: [Player]
|
||||
|
||||
/// We'll need to leave edit mode in several occasions.
|
||||
@State private var editMode = EditMode.inactive
|
||||
|
||||
/// Tracks the presentation of the player creation sheet.
|
||||
@State private var newPlayerIsPresented = false
|
||||
|
||||
// If you want to define the query on initialization, you will prefer:
|
||||
//
|
||||
// @Query<PlayerRequest> private var players: [Player]
|
||||
//
|
||||
// init(initialOrdering: PlayerRequest.Ordering) {
|
||||
// _players = Query(PlayerRequest(ordering: initialOrdering))
|
||||
// }
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
PlayerList(players: players)
|
||||
.navigationBarTitle(Text("\(players.count) Players"))
|
||||
.navigationBarItems(
|
||||
leading: HStack {
|
||||
EditButton()
|
||||
newPlayerButton
|
||||
},
|
||||
trailing: ToggleOrderingButton(
|
||||
ordering: $players.ordering,
|
||||
willChange: {
|
||||
// onChange(of: $players.wrappedValue.ordering)
|
||||
// is not able to leave the editing mode during
|
||||
// the animation of the list content.
|
||||
// Workaround: stop editing before the ordering
|
||||
// is changed, and the list content is updated.
|
||||
stopEditing()
|
||||
}))
|
||||
.toolbar { toolbarContent }
|
||||
.onChange(of: players) {
|
||||
if players.isEmpty {
|
||||
stopEditing()
|
||||
}
|
||||
}
|
||||
.environment(\.editMode, $editMode)
|
||||
}
|
||||
}
|
||||
|
||||
private var toolbarContent: some ToolbarContent {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
Button {
|
||||
// Don't stopEditing() here because this is
|
||||
// performed `onChange(of: players)`
|
||||
Task {
|
||||
try? await appDatabase.deleteAllPlayers()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "trash").imageScale(.large)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
stopEditing()
|
||||
Task {
|
||||
try? await appDatabase.refreshPlayers()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise").imageScale(.large)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
stopEditing()
|
||||
// Perform 50 refreshes in parallel
|
||||
Task {
|
||||
try? await withThrowingTaskGroup(of: Void.self) { group in
|
||||
for _ in 0..<50 {
|
||||
_ = group.addTaskUnlessCancelled {
|
||||
try await appDatabase.refreshPlayers()
|
||||
}
|
||||
}
|
||||
try await group.waitForAll()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "tornado").imageScale(.large)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The button that presents the player creation sheet.
|
||||
private var newPlayerButton: some View {
|
||||
Button {
|
||||
stopEditing()
|
||||
newPlayerIsPresented = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.accessibility(label: Text("New Player"))
|
||||
.sheet(isPresented: $newPlayerIsPresented) {
|
||||
PlayerCreationView()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopEditing() {
|
||||
withAnimation {
|
||||
editMode = .inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ToggleOrderingButton: View {
|
||||
@Binding var ordering: PlayerRequest.Ordering
|
||||
let willChange: () -> Void
|
||||
|
||||
var body: some View {
|
||||
switch ordering {
|
||||
case .byName:
|
||||
Button {
|
||||
willChange()
|
||||
ordering = .byScore
|
||||
} label: {
|
||||
Label("Name", systemImage: "arrowtriangle.up.fill").labelStyle(.titleAndIcon)
|
||||
}
|
||||
case .byScore:
|
||||
Button {
|
||||
willChange()
|
||||
ordering = .byName
|
||||
} label: {
|
||||
Label("Score", systemImage: "arrowtriangle.down.fill").labelStyle(.titleAndIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Empty") {
|
||||
// Preview the default, empty database
|
||||
AppView()
|
||||
}
|
||||
|
||||
#Preview("Populated") {
|
||||
// Preview a database of random players
|
||||
AppView().appDatabase(.random())
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The view that creates a new player.
|
||||
struct PlayerCreationView: View {
|
||||
/// Write access to the database
|
||||
@Environment(\.appDatabase) private var appDatabase
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var form = PlayerForm(name: "", score: "")
|
||||
@State private var errorAlertIsPresented = false
|
||||
@State private var errorAlertTitle = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
PlayerFormView(form: $form)
|
||||
.alert(
|
||||
isPresented: $errorAlertIsPresented,
|
||||
content: { Alert(title: Text(errorAlertTitle)) })
|
||||
.navigationBarTitle("New Player")
|
||||
.navigationBarItems(
|
||||
leading: Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
},
|
||||
trailing: Button {
|
||||
Task { await save() }
|
||||
} label: {
|
||||
Text("Save")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func save() async {
|
||||
do {
|
||||
var player = Player(id: nil, name: "", score: 0)
|
||||
form.apply(to: &player)
|
||||
try await appDatabase.savePlayer(&player)
|
||||
dismiss()
|
||||
} catch {
|
||||
errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
|
||||
errorAlertIsPresented = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview {
|
||||
PlayerCreationView()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The view that edits an existing player.
|
||||
struct PlayerEditionView: View {
|
||||
/// Write access to the database
|
||||
@Environment(\.appDatabase) private var appDatabase
|
||||
@Environment(\.isPresented) private var isPresented
|
||||
private let player: Player
|
||||
@State private var form: PlayerForm
|
||||
|
||||
init(player: Player) {
|
||||
self.player = player
|
||||
self.form = PlayerForm(player)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
PlayerFormView(form: $form)
|
||||
.onChange(of: isPresented) {
|
||||
// Save when back button is pressed
|
||||
if !isPresented {
|
||||
Task {
|
||||
var savedPlayer = player
|
||||
form.apply(to: &savedPlayer)
|
||||
// Ignore error because I don't know how to cancel the
|
||||
// back button and present the error
|
||||
try? await appDatabase.savePlayer(&savedPlayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
PlayerEditionView(player: Player.makeRandom())
|
||||
.navigationBarTitle("Player Edition")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The Player editing form, embedded in both
|
||||
/// `PlayerCreationView` and `PlayerEditionView`.
|
||||
struct PlayerFormView: View {
|
||||
@Binding var form: PlayerForm
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
TextField("Name", text: $form.name)
|
||||
.accessibility(label: Text("Player Name"))
|
||||
TextField("Score", text: $form.score).keyboardType(.numberPad)
|
||||
.accessibility(label: Text("Player Score"))
|
||||
}
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayerForm {
|
||||
var name: String
|
||||
var score: String
|
||||
}
|
||||
|
||||
extension PlayerForm {
|
||||
init(_ player: Player) {
|
||||
self.name = player.name
|
||||
self.score = "\(player.score)"
|
||||
}
|
||||
|
||||
func apply(to player: inout Player) {
|
||||
player.name = name
|
||||
player.score = Int(score) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Empty") {
|
||||
PlayerFormView(form: .constant(PlayerForm(
|
||||
name: "",
|
||||
score: "")))
|
||||
}
|
||||
|
||||
#Preview("Prefilled") {
|
||||
PlayerFormView(form: .constant(PlayerForm(
|
||||
name: Player.randomName(),
|
||||
score: "\(Player.randomScore())")))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PlayerList: View {
|
||||
/// Write access to the database
|
||||
@Environment(\.appDatabase) private var appDatabase
|
||||
|
||||
/// The players in the list
|
||||
var players: [Player]
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(players) { player in
|
||||
NavigationLink(destination: editionView(for: player)) {
|
||||
PlayerRow(player: player)
|
||||
// Don't animate player update
|
||||
.animation(nil, value: player)
|
||||
}
|
||||
}
|
||||
.onDelete { offsets in
|
||||
let playerIds = offsets.compactMap { players[$0].id }
|
||||
Task {
|
||||
try? await appDatabase.deletePlayers(ids: playerIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Animate list updates
|
||||
.animation(.default, value: players)
|
||||
.listStyle(.plain)
|
||||
}
|
||||
|
||||
/// The view that edits a player in the list.
|
||||
private func editionView(for player: Player) -> some View {
|
||||
PlayerEditionView(player: player).navigationBarTitle(player.name)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PlayerRow: View {
|
||||
var player: Player
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(player.name)
|
||||
Spacer()
|
||||
Text("\(player.score) points").foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
PlayerList(players: [
|
||||
Player(id: 1, name: "Arthur", score: 100),
|
||||
Player(id: 2, name: "Barbara", score: 1000),
|
||||
])
|
||||
.navigationTitle("Preview")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
@testable import GRDBAsyncDemo
|
||||
|
||||
class AppDatabaseTests: XCTestCase {
|
||||
func test_database_schema() throws {
|
||||
// Given an empty database
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
|
||||
// When we instantiate an AppDatabase
|
||||
_ = try AppDatabase(dbQueue)
|
||||
|
||||
// Then the player table exists, with id, name & score columns
|
||||
try dbQueue.read { db in
|
||||
try XCTAssert(db.tableExists("player"))
|
||||
let columns = try db.columns(in: "player")
|
||||
let columnNames = Set(columns.map { $0.name })
|
||||
XCTAssertEqual(columnNames, ["id", "name", "score"])
|
||||
}
|
||||
}
|
||||
|
||||
func test_savePlayer_inserts() async throws {
|
||||
// Given an empty players database
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
let appDatabase = try AppDatabase(dbQueue)
|
||||
|
||||
// When we save a new player
|
||||
var player = Player(id: nil, name: "Arthur", score: 100)
|
||||
try await appDatabase.savePlayer(&player)
|
||||
|
||||
// Then the player exists in the database
|
||||
let playerExists = try await dbQueue.read { [player] in try player.exists($0) }
|
||||
XCTAssertTrue(playerExists)
|
||||
}
|
||||
|
||||
func test_savePlayer_updates() async throws {
|
||||
// Given a players database that contains a player
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
let appDatabase = try AppDatabase(dbQueue)
|
||||
var player = try await dbQueue.write { db in
|
||||
try Player(id: nil, name: "Arthur", score: 100).inserted(db)
|
||||
}
|
||||
|
||||
// When we modify and save the player
|
||||
player.name = "Barbara"
|
||||
player.score = 1000
|
||||
try await appDatabase.savePlayer(&player)
|
||||
|
||||
// Then the player has been updated in the database
|
||||
let fetchedPlayer = try await dbQueue.read { [player] db in
|
||||
try XCTUnwrap(Player.fetchOne(db, key: player.id))
|
||||
}
|
||||
XCTAssertEqual(fetchedPlayer, player)
|
||||
}
|
||||
|
||||
func test_deletePlayers() async throws {
|
||||
// Given a players database that contains four players
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
let appDatabase = try AppDatabase(dbQueue)
|
||||
let playerIds: [Int64] = try await dbQueue.write { db in
|
||||
_ = try Player(id: nil, name: "Arthur", score: 100).inserted(db)
|
||||
_ = try Player(id: nil, name: "Barbara", score: 200).inserted(db)
|
||||
_ = try Player(id: nil, name: "Craig", score: 150).inserted(db)
|
||||
_ = try Player(id: nil, name: "David", score: 120).inserted(db)
|
||||
return try Player.selectPrimaryKey().fetchAll(db)
|
||||
}
|
||||
|
||||
// When we delete two players
|
||||
let deletedId1 = playerIds[0]
|
||||
let deletedId2 = playerIds[2]
|
||||
try await appDatabase.deletePlayers(ids: [deletedId1, deletedId2])
|
||||
|
||||
// Then the deleted players no longer exist
|
||||
try await dbQueue.read { db in
|
||||
try XCTAssertFalse(Player.exists(db, id: deletedId1))
|
||||
try XCTAssertFalse(Player.exists(db, id: deletedId2))
|
||||
}
|
||||
|
||||
// Then the database still contains two players
|
||||
let count = try await dbQueue.read { try Player.fetchCount($0) }
|
||||
XCTAssertEqual(count, 2)
|
||||
}
|
||||
|
||||
func test_deleteAllPlayers() async throws {
|
||||
// Given a players database that contains players
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
let appDatabase = try AppDatabase(dbQueue)
|
||||
try await dbQueue.write { db in
|
||||
_ = try Player(id: nil, name: "Arthur", score: 100).inserted(db)
|
||||
_ = try Player(id: nil, name: "Barbara", score: 200).inserted(db)
|
||||
_ = try Player(id: nil, name: "Craig", score: 150).inserted(db)
|
||||
_ = try Player(id: nil, name: "David", score: 120).inserted(db)
|
||||
}
|
||||
|
||||
// When we delete all players
|
||||
try await appDatabase.deleteAllPlayers()
|
||||
|
||||
// Then the database does not contain any player
|
||||
let count = try await dbQueue.read { try Player.fetchCount($0) }
|
||||
XCTAssertEqual(count, 0)
|
||||
}
|
||||
|
||||
func test_refreshPlayers_populates_an_empty_database() async throws {
|
||||
// Given an empty players database
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
let appDatabase = try AppDatabase(dbQueue)
|
||||
|
||||
// When we refresh players
|
||||
try await appDatabase.refreshPlayers()
|
||||
|
||||
// Then the database is not empty
|
||||
let count = try await dbQueue.read { try Player.fetchCount($0) }
|
||||
XCTAssert(count > 0)
|
||||
}
|
||||
|
||||
func test_createRandomPlayersIfEmpty_populates_an_empty_database() throws {
|
||||
// Given an empty players database
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
let appDatabase = try AppDatabase(dbQueue)
|
||||
|
||||
// When we create random players
|
||||
try appDatabase.createRandomPlayersIfEmpty()
|
||||
|
||||
// Then the database is not empty
|
||||
try XCTAssert(dbQueue.read(Player.fetchCount) > 0)
|
||||
}
|
||||
|
||||
func test_createRandomPlayersIfEmpty_does_not_modify_a_non_empty_database() throws {
|
||||
// Given a players database that contains one player
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
let appDatabase = try AppDatabase(dbQueue)
|
||||
var player = Player(id: nil, name: "Arthur", score: 100)
|
||||
try dbQueue.write { db in
|
||||
try player.insert(db)
|
||||
}
|
||||
|
||||
// When we create random players
|
||||
try appDatabase.createRandomPlayersIfEmpty()
|
||||
|
||||
// Then the database still only contains the original player
|
||||
let players = try dbQueue.read(Player.fetchAll)
|
||||
XCTAssertEqual(players, [player])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,43 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
@testable import GRDBAsyncDemo
|
||||
|
||||
class PlayerRequestTests: XCTestCase {
|
||||
func test_PlayerRequest_byName_fetches_well_ordered_players() throws {
|
||||
// Given a players database that contains two players
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
_ = try AppDatabase(dbQueue)
|
||||
var player1 = Player(id: nil, name: "Arthur", score: 100)
|
||||
var player2 = Player(id: nil, name: "Barbara", score: 1000)
|
||||
try dbQueue.write { db in
|
||||
try player1.insert(db)
|
||||
try player2.insert(db)
|
||||
}
|
||||
|
||||
// When we fetch players ordered by name
|
||||
let playerRequest = PlayerRequest(ordering: .byName)
|
||||
let players = try dbQueue.read(playerRequest.fetch)
|
||||
|
||||
// Then the players are the two players ordered by name
|
||||
XCTAssertEqual(players, [player1, player2])
|
||||
}
|
||||
|
||||
func test_PlayerRequest_byScore_fetches_well_ordered_players() throws {
|
||||
// Given a players database that contains two players
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
_ = try AppDatabase(dbQueue)
|
||||
var player1 = Player(id: nil, name: "Arthur", score: 100)
|
||||
var player2 = Player(id: nil, name: "Barbara", score: 1000)
|
||||
try dbQueue.write { db in
|
||||
try player1.insert(db)
|
||||
try player2.insert(db)
|
||||
}
|
||||
|
||||
// When we fetch players ordered by score
|
||||
let playerRequest = PlayerRequest(ordering: .byScore)
|
||||
let players = try dbQueue.read(playerRequest.fetch)
|
||||
|
||||
// Then the players are the two players ordered by score descending
|
||||
XCTAssertEqual(players, [player2, player1])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
@testable import GRDBAsyncDemo
|
||||
|
||||
class PlayerTests: XCTestCase {
|
||||
// MARK: - CRUD
|
||||
// Test that our Player type properly talks to GRDB.
|
||||
|
||||
func testInsert() throws {
|
||||
// Given an empty players database
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
_ = try AppDatabase(dbQueue)
|
||||
|
||||
// When we insert a player
|
||||
var player = Player(id: nil, name: "Arthur", score: 100)
|
||||
try dbQueue.write { db in
|
||||
try player.insert(db)
|
||||
}
|
||||
|
||||
// Then the player gets a non-nil id
|
||||
XCTAssertNotNil(player.id)
|
||||
}
|
||||
|
||||
func testRoundtrip() throws {
|
||||
// Given an empty players database
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
_ = try AppDatabase(dbQueue)
|
||||
|
||||
// When we insert a player and fetch the player with the same id
|
||||
var insertedPlayer = Player(id: nil, name: "Arthur", score: 100)
|
||||
let fetchedPlayer: Player? = try dbQueue.write { db in
|
||||
try insertedPlayer.insert(db)
|
||||
return try Player.fetchOne(db, key: insertedPlayer.id)
|
||||
}
|
||||
|
||||
// Then the fetched player is equal to the inserted player
|
||||
XCTAssertEqual(insertedPlayer, fetchedPlayer)
|
||||
}
|
||||
|
||||
// MARK: - Requests
|
||||
// Test that requests defined on the Player type behave as expected.
|
||||
|
||||
func testOrderedByScore() throws {
|
||||
// Given a players database that contains players with distinct scores
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
_ = try AppDatabase(dbQueue)
|
||||
var player1 = Player(id: 1, name: "Arthur", score: 100)
|
||||
var player2 = Player(id: 2, name: "Barbara", score: 200)
|
||||
var player3 = Player(id: 3, name: "Craig", score: 150)
|
||||
var player4 = Player(id: 4, name: "David", score: 120)
|
||||
try dbQueue.write { db in
|
||||
try player1.insert(db)
|
||||
try player2.insert(db)
|
||||
try player3.insert(db)
|
||||
try player4.insert(db)
|
||||
}
|
||||
|
||||
// When we fetch players ordered by score
|
||||
let players = try dbQueue.read(Player.all().orderedByScore().fetchAll)
|
||||
|
||||
// Then fetched players are ordered by score descending
|
||||
XCTAssertEqual(players, [player2, player3, player4, player1])
|
||||
}
|
||||
|
||||
func testOrderedByScoreSortsIdenticalScoresByName() throws {
|
||||
// Given a players database that contains players with common scores
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
_ = try AppDatabase(dbQueue)
|
||||
var player1 = Player(id: 1, name: "Arthur", score: 100)
|
||||
var player2 = Player(id: 2, name: "Barbara", score: 200)
|
||||
var player3 = Player(id: 3, name: "Craig", score: 200)
|
||||
var player4 = Player(id: 4, name: "David", score: 200)
|
||||
try dbQueue.write { db in
|
||||
try player1.insert(db)
|
||||
try player2.insert(db)
|
||||
try player3.insert(db)
|
||||
try player4.insert(db)
|
||||
}
|
||||
|
||||
// When we fetch players ordered by score
|
||||
let players = try dbQueue.read(Player.all().orderedByScore().fetchAll)
|
||||
|
||||
// Then fetched players are ordered by score descending and by name
|
||||
XCTAssertEqual(players, [player2, player3, player4, player1])
|
||||
}
|
||||
|
||||
func testOrderedByName() throws {
|
||||
// Given a players database that contains players with distinct names
|
||||
let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration())
|
||||
_ = try AppDatabase(dbQueue)
|
||||
var player1 = Player(id: 1, name: "Arthur", score: 100)
|
||||
var player2 = Player(id: 2, name: "Barbara", score: 200)
|
||||
var player3 = Player(id: 3, name: "Craig", score: 150)
|
||||
var player4 = Player(id: 4, name: "David", score: 120)
|
||||
try dbQueue.write { db in
|
||||
try player1.insert(db)
|
||||
try player2.insert(db)
|
||||
try player3.insert(db)
|
||||
try player4.insert(db)
|
||||
}
|
||||
|
||||
// When we fetch players ordered by name
|
||||
let players = try dbQueue.read(Player.all().orderedByName().fetchAll)
|
||||
|
||||
// Then fetched players are ordered by name
|
||||
XCTAssertEqual(players, [player1, player2, player3, player4])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
Async/Await + SwiftUI Demo Application
|
||||
======================================
|
||||
|
||||
<img align="right" src="https://github.com/groue/GRDB.swift/raw/master/Documentation/DemoApps/GRDBCombineDemo/Screenshot.png" width="50%">
|
||||
|
||||
**This demo application is an Async/Await + SwiftUI application.** For a demo application that uses UIKit, see [GRDBDemoiOS](../GRDBDemoiOS/README.md), and for Combine + SwiftUI, see [GRDBCombineDemo](../GRDBCombineDemo/README.md).
|
||||
|
||||
**Requirements**: iOS 15.0+ / Xcode 13.1+
|
||||
|
||||
> **Note**: This demo app is not a project template. Do not copy it as a starting point for your application. Instead, create a new project, choose a GRDB [installation method](../../../README.md#installation), and use the demo as an inspiration.
|
||||
|
||||
The topics covered in this demo are:
|
||||
|
||||
- How to setup a database in an iOS app.
|
||||
- How to define a simple [Codable Record](../../../README.md#codable-records).
|
||||
- How to track database changes and animate a SwiftUI List with [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation) Combine publishers.
|
||||
- How to apply the recommendations of [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices).
|
||||
- How to perform `async` database accesses.
|
||||
- How to feed SwiftUI previews with a transient database.
|
||||
|
||||
**Files of interest:**
|
||||
|
||||
- [GRDBAsyncDemoApp.swift](GRDBAsyncDemo/GRDBAsyncDemoApp.swift)
|
||||
|
||||
`GRDBAsyncDemoApp` feeds the app views with a database, through the SwiftUI environment.
|
||||
|
||||
- [AppDatabase.swift](GRDBAsyncDemo/AppDatabase.swift)
|
||||
|
||||
`AppDatabase` is the type that grants database access. It uses [DatabaseMigrator](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasemigrator) in order to setup the database schema.
|
||||
|
||||
- [Persistence.swift](GRDBAsyncDemo/Persistence.swift)
|
||||
|
||||
This file instantiates various `AppDatabase` for the various projects needs: one database on disk for the application, and in-memory databases for SwiftUI previews.
|
||||
|
||||
- [Player.swift](GRDBAsyncDemo/Player.swift)
|
||||
|
||||
`Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records).
|
||||
|
||||
- [PlayerRequest.swift](GRDBAsyncDemo/PlayerRequest.swift), [AppView.swift](GRDBAsyncDemo/Views/AppView.swift)
|
||||
|
||||
`PlayerRequest` defines the player requests used by the app (sorted by score, or by name).
|
||||
|
||||
`PlayerRequest` feeds the `@Query` property wrapper (`@Query`, defined in [GRDBQuery](https://github.com/groue/GRDBQuery), allows SwiftUI views to display up-to-date database content).
|
||||
|
||||
`AppView` is the SwiftUI view that uses `@Query` in order to feed its player list.
|
||||
|
||||
- [GRDBAsyncDemoTests](GRDBAsyncDemoTests)
|
||||
|
||||
- Test the database schema
|
||||
- Test the `Player` record and its requests
|
||||
- Test the `PlayerRequest` methods that feed the list of players.
|
||||
- Test the `AppDatabase` methods that let the app access the database.
|
||||
|
After Width: | Height: | Size: 799 KiB |