Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dda69389eb | ||
|
|
b20be10a19 | ||
|
|
0ee8fbabd5 | ||
|
|
1f562b25a7 | ||
|
|
593c0510d3 | ||
|
|
338e4ddc9f | ||
|
|
bfb1df3c40 | ||
|
|
003bf897e2 | ||
|
|
0b127956d3 | ||
|
|
098e560fcc | ||
|
|
7dbd3777ec | ||
|
|
447d8e5880 | ||
|
|
1f97225efa | ||
|
|
d13b0cfabb | ||
|
|
d7b852fca4 | ||
|
|
f2efe175e5 | ||
|
|
8f3a6638d8 | ||
|
|
f7471f56a4 | ||
|
|
edd8ba55d8 | ||
|
|
18aac84335 | ||
|
|
c6be892cb0 | ||
|
|
2cd8101987 | ||
|
|
e1aed37da0 | ||
|
|
5de5ecee06 | ||
|
|
9406901b28 | ||
|
|
477f478d85 | ||
|
|
668b5ad606 | ||
|
|
f985828f3b | ||
|
|
bb3bc940c2 | ||
|
|
2d5bc77219 | ||
|
|
a40df37192 | ||
|
|
63b3d25d78 | ||
|
|
74721b5c12 | ||
|
|
f136549b46 | ||
|
|
4ec2b2e311 | ||
|
|
3d82ee18c7 | ||
|
|
d6eae8c091 | ||
|
|
6f8d86cd8f | ||
|
|
3b6f226389 | ||
|
|
228e4e8e1a | ||
|
|
98a42feb95 | ||
|
|
2f93ee7591 | ||
|
|
11f5cb9a05 | ||
|
|
2c00fc31bc | ||
|
|
2bbf6b34ea | ||
|
|
8d7f282743 | ||
|
|
007da014f8 | ||
|
|
b26e50f777 | ||
|
|
611bc53c9a | ||
|
|
9d36582c10 | ||
|
|
1c735a9228 | ||
|
|
1db91fcec3 | ||
|
|
8b3b947406 | ||
|
|
2c0cadf2fa | ||
|
|
5e536556da | ||
|
|
d75029f54b | ||
|
|
204c4de1f6 | ||
|
|
0f3455a4a4 | ||
|
|
72f36e7237 | ||
|
|
d988daa025 | ||
|
|
e720504855 | ||
|
|
ee51c5ad9c | ||
|
|
4ac7df7364 | ||
|
|
56f873ccb3 | ||
|
|
1f90f846f3 | ||
|
|
4a97d5a8dc | ||
|
|
0eb9b6393d | ||
|
|
56d0ea46ea | ||
|
|
a7568eebdb | ||
|
|
4049e1944a | ||
|
|
73b7fcd907 | ||
|
|
74c1a97af4 | ||
|
|
97f2a53124 | ||
|
|
b6db872be0 | ||
|
|
0d9299f900 | ||
|
|
7f928dc684 | ||
|
|
231e138ab0 | ||
|
|
361dba58c6 | ||
|
|
e1b03b4a89 | ||
|
|
0df6c737c1 | ||
|
|
12c5aeaaa4 | ||
|
|
dd3fb17dd0 | ||
|
|
627a5d4355 | ||
|
|
58629bc1df | ||
|
|
f925803b93 | ||
|
|
843adf21f7 | ||
|
|
2d1b1e0592 | ||
|
|
38e9878c04 | ||
|
|
8cb8b95c2e | ||
|
|
8e4e308ccc | ||
|
|
f0f4049798 | ||
|
|
c20fe4ac17 | ||
|
|
e9c3312612 | ||
|
|
92ad895044 | ||
|
|
bcc2d9def3 | ||
|
|
43f61359da | ||
|
|
5e37ee4566 | ||
|
|
c544e0cce8 |
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [JohnEstropia]
|
||||
2
.gitignore
vendored
@@ -12,3 +12,5 @@ Playground_macOS.playground/playground.xcworkspace/xcuserdata
|
||||
.swiftpm/xcode/package.xcworkspace/xcuserdata
|
||||
.swiftpm/xcode/xcuserdata
|
||||
Playground_iOS.playground/playground.xcworkspace/xcuserdata
|
||||
LegacyDemo/LegacyDemo.xcodeproj/xcuserdata
|
||||
Demo/Demo.xcodeproj/xcuserdata
|
||||
|
||||
BIN
CoreStore.png
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 61 KiB |
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "CoreStore"
|
||||
s.version = "7.0.3"
|
||||
s.swift_version = "5.1"
|
||||
s.version = "8.0.1"
|
||||
s.swift_version = "5.4"
|
||||
s.license = "MIT"
|
||||
s.homepage = "https://github.com/JohnEstropia/CoreStore"
|
||||
s.documentation_url = "https://JohnEstropia.github.io/CoreStore"
|
||||
@@ -9,10 +9,10 @@ Pod::Spec.new do |s|
|
||||
s.author = { "John Rommel Estropia" => "rommel.estropia@gmail.com" }
|
||||
s.source = { :git => "https://github.com/JohnEstropia/CoreStore.git", :tag => s.version.to_s }
|
||||
|
||||
s.ios.deployment_target = "10.0"
|
||||
s.osx.deployment_target = "10.12"
|
||||
s.watchos.deployment_target = "3.0"
|
||||
s.tvos.deployment_target = "10.0"
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.13"
|
||||
s.watchos.deployment_target = "4.0"
|
||||
s.tvos.deployment_target = "11.0"
|
||||
|
||||
s.source_files = "Sources", "Sources/**/*.{swift,h,m}"
|
||||
s.public_header_files = "Sources/**/*.h"
|
||||
|
||||
BIN
CoreStore.sketch
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
LastUpgradeVersion = "1200"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
LastUpgradeVersion = "1200"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
LastUpgradeVersion = "1200"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
LastUpgradeVersion = "1200"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
5
CoreStore.xcworkspace/contents.xcworkspacedata
generated
@@ -1,10 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Demo/Demo.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:CoreStore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:CoreStoreDemo/CoreStoreDemo.xcodeproj">
|
||||
location = "group:LegacyDemo/LegacyDemo.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<?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>IDESourceControlProjectFavoriteDictionaryKey</key>
|
||||
<false/>
|
||||
<key>IDESourceControlProjectIdentifier</key>
|
||||
<string>B6855E48-4B19-4321-B1C7-CB2706E12777</string>
|
||||
<key>IDESourceControlProjectName</key>
|
||||
<string>CoreStoreDemo</string>
|
||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
||||
<dict>
|
||||
<key>4B60F1BCB491FF717C56441AE7783C74F417BE48</key>
|
||||
<string>github.com:JohnEstropia/CoreStore.git</string>
|
||||
<key>8B2E522D57154DFA93A06982C36315ECBEA4FA97</key>
|
||||
<string>github.com:JohnEstropia/GCDKit.git</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectPath</key>
|
||||
<string>CoreStoreDemo/CoreStoreDemo.xcodeproj</string>
|
||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
||||
<dict>
|
||||
<key>4B60F1BCB491FF717C56441AE7783C74F417BE48</key>
|
||||
<string>../../..</string>
|
||||
<key>8B2E522D57154DFA93A06982C36315ECBEA4FA97</key>
|
||||
<string>../../..Libraries/GCDKit</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectURL</key>
|
||||
<string>github.com:JohnEstropia/CoreStore.git</string>
|
||||
<key>IDESourceControlProjectVersion</key>
|
||||
<integer>111</integer>
|
||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
||||
<string>4B60F1BCB491FF717C56441AE7783C74F417BE48</string>
|
||||
<key>IDESourceControlProjectWCConfigurations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>4B60F1BCB491FF717C56441AE7783C74F417BE48</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>CoreStore</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>8B2E522D57154DFA93A06982C36315ECBEA4FA97</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>GCDKit</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "4B60F1BCB491FF717C56441AE7783C74F417BE48",
|
||||
"DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : {
|
||||
|
||||
},
|
||||
"DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : {
|
||||
"8B2E522D57154DFA93A06982C36315ECBEA4FA97" : 0,
|
||||
"4B60F1BCB491FF717C56441AE7783C74F417BE48" : 0
|
||||
},
|
||||
"DVTSourceControlWorkspaceBlueprintIdentifierKey" : "B6855E48-4B19-4321-B1C7-CB2706E12777",
|
||||
"DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : {
|
||||
"8B2E522D57154DFA93A06982C36315ECBEA4FA97" : "CoreStoreLibraries\/GCDKit",
|
||||
"4B60F1BCB491FF717C56441AE7783C74F417BE48" : "CoreStore"
|
||||
},
|
||||
"DVTSourceControlWorkspaceBlueprintNameKey" : "CoreStoreDemo",
|
||||
"DVTSourceControlWorkspaceBlueprintVersion" : 203,
|
||||
"DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "CoreStoreDemo\/CoreStoreDemo.xcodeproj",
|
||||
"DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [
|
||||
{
|
||||
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:JohnEstropia\/CoreStore.git",
|
||||
"DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
|
||||
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "4B60F1BCB491FF717C56441AE7783C74F417BE48"
|
||||
},
|
||||
{
|
||||
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:JohnEstropia\/GCDKit.git",
|
||||
"DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
|
||||
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "8B2E522D57154DFA93A06982C36315ECBEA4FA97"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?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>IDESourceControlProjectFavoriteDictionaryKey</key>
|
||||
<false/>
|
||||
<key>IDESourceControlProjectIdentifier</key>
|
||||
<string>7C5E31AC-5DD0-43DA-A5C6-AF73B4532D86</string>
|
||||
<key>IDESourceControlProjectName</key>
|
||||
<string>project</string>
|
||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
||||
<dict>
|
||||
<key>4B60F1BCB491FF717C56441AE7783C74F417BE48</key>
|
||||
<string>github.com:JohnEstropia/HardcoreData.git</string>
|
||||
<key>8B2E522D57154DFA93A06982C36315ECBEA4FA97</key>
|
||||
<string>github.com:JohnEstropia/GCDKit.git</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectPath</key>
|
||||
<string>HardcoreDataDemo/HardcoreDataDemo.xcodeproj/project.xcworkspace</string>
|
||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
||||
<dict>
|
||||
<key>4B60F1BCB491FF717C56441AE7783C74F417BE48</key>
|
||||
<string>../../..</string>
|
||||
<key>8B2E522D57154DFA93A06982C36315ECBEA4FA97</key>
|
||||
<string>../../..Libraries/GCDKit</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectURL</key>
|
||||
<string>github.com:JohnEstropia/HardcoreData.git</string>
|
||||
<key>IDESourceControlProjectVersion</key>
|
||||
<integer>111</integer>
|
||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
||||
<string>4B60F1BCB491FF717C56441AE7783C74F417BE48</string>
|
||||
<key>IDESourceControlProjectWCConfigurations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>8B2E522D57154DFA93A06982C36315ECBEA4FA97</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>GCDKit</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>4B60F1BCB491FF717C56441AE7783C74F417BE48</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>HardcoreData</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<<<<<<< Updated upstream
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
|
||||
=======
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9532" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9530"/>
|
||||
>>>>>>> Stashed changes
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<<<<<<< Updated upstream
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright © 2015 John Rommel Estropia. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
|
||||
<rect key="frame" x="20" y="439" width="441" height="21"/>
|
||||
=======
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2015 John Rommel Estropia. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
|
||||
<rect key="frame" x="20" y="439.5" width="441" height="20.5"/>
|
||||
>>>>>>> Stashed changes
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.92549019607843142" green="0.94117647058823528" blue="0.94509803921568625" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="CoreStore" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
|
||||
<rect key="frame" x="20" y="133" width="441" height="57.5"/>
|
||||
<fontDescription key="fontDescription" name="HelveticaNeue-UltraLight" family="Helvetica Neue" pointSize="50"/>
|
||||
<color key="textColor" red="0.92549019607843142" green="0.94117647058823528" blue="0.94509803921568625" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.20392156862745098" green="0.28627450980392155" blue="0.36862745098039218" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/>
|
||||
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/>
|
||||
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l"/>
|
||||
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0"/>
|
||||
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/>
|
||||
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="548" y="455"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -1,86 +0,0 @@
|
||||
//
|
||||
// Palette.swift
|
||||
// CoreStoreDemo
|
||||
//
|
||||
// Created by John Rommel Estropia on 2015/05/05.
|
||||
// Copyright © 2018 John Rommel Estropia. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Palette
|
||||
|
||||
final class Palette: CoreStoreObject {
|
||||
|
||||
let hue = Value.Required<Int>("hue", initial: 0)
|
||||
let saturation = Value.Required<Float>("saturation", initial: 0)
|
||||
let brightness = Value.Required<Float>("brightness", initial: 0)
|
||||
|
||||
let colorName = Value.Optional<String>(
|
||||
"colorName",
|
||||
isTransient: true,
|
||||
customGetter: Palette.getColorName
|
||||
)
|
||||
|
||||
static func randomHue() -> Int {
|
||||
|
||||
return Int(arc4random_uniform(360))
|
||||
}
|
||||
|
||||
private static func getColorName(_ partialObject: PartialObject<Palette>) -> String? {
|
||||
|
||||
if let colorName = partialObject.primitiveValue(for: { $0.colorName }) {
|
||||
|
||||
return colorName
|
||||
}
|
||||
|
||||
let colorName: String
|
||||
switch partialObject.value(for: { $0.hue }) % 360 {
|
||||
|
||||
case 0 ..< 20: colorName = "Lower Reds"
|
||||
case 20 ..< 57: colorName = "Oranges and Browns"
|
||||
case 57 ..< 90: colorName = "Yellow-Greens"
|
||||
case 90 ..< 159: colorName = "Greens"
|
||||
case 159 ..< 197: colorName = "Blue-Greens"
|
||||
case 197 ..< 241: colorName = "Blues"
|
||||
case 241 ..< 297: colorName = "Violets"
|
||||
case 297 ..< 331: colorName = "Magentas"
|
||||
default: colorName = "Upper Reds"
|
||||
}
|
||||
|
||||
partialObject.setPrimitiveValue(colorName, for: { $0.colorName })
|
||||
return colorName
|
||||
}
|
||||
}
|
||||
|
||||
extension Palette {
|
||||
|
||||
var color: UIColor {
|
||||
|
||||
return UIColor(
|
||||
hue: CGFloat(self.hue.value) / 360.0,
|
||||
saturation: CGFloat(self.saturation.value),
|
||||
brightness: CGFloat(self.brightness.value),
|
||||
alpha: 1.0
|
||||
)
|
||||
}
|
||||
|
||||
var colorText: String {
|
||||
|
||||
return "H: \(self.hue.value)˚, S: \(round(self.saturation.value * 100.0))%, B: \(round(self.brightness.value * 100.0))%"
|
||||
}
|
||||
}
|
||||
|
||||
extension Palette {
|
||||
|
||||
func setInitialValues(in transaction: BaseDataTransaction) {
|
||||
|
||||
self.hue .= Palette.randomHue()
|
||||
self.saturation .= Float(1.0)
|
||||
self.brightness .= Float(arc4random_uniform(70) + 30) / 100.0
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,9 @@ class BaseTestCase: XCTestCase {
|
||||
|
||||
XCTFail(error.coreStoreDumpString)
|
||||
}
|
||||
self.addTeardownBlock {
|
||||
stack.unsafeRemoveAllPersistentStoresAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
@nonobjc
|
||||
|
||||
@@ -191,16 +191,8 @@
|
||||
XCTAssertEqualObjects([[sqliteStorage class] storeType], [CSSQLiteStore storeType]);
|
||||
XCTAssertEqualObjects([[sqliteStorage class] storeType], NSSQLiteStoreType);
|
||||
XCTAssertNil(sqliteStorage.configuration);
|
||||
NSDictionary *storeOptions;
|
||||
if (@available(iOS 11.0, macOS 10.13, tvOS 11.0, *)) {
|
||||
|
||||
storeOptions = @{ NSSQLitePragmasOption: @{ @"journal_mode": @"WAL" },
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: @YES };
|
||||
}
|
||||
else {
|
||||
|
||||
storeOptions = @{ NSSQLitePragmasOption: @{ @"journal_mode": @"WAL" }};
|
||||
}
|
||||
NSDictionary *storeOptions = @{ NSSQLitePragmasOption: @{ @"journal_mode": @"WAL" },
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: @YES };
|
||||
XCTAssertEqualObjects(sqliteStorage.storeOptions, storeOptions);
|
||||
XCTAssertNil(sqliteError);
|
||||
}
|
||||
|
||||
@@ -23,13 +23,15 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - ConvenienceTests
|
||||
|
||||
@available(macOS 10.12, *)
|
||||
class ConvenienceTests: BaseTestCase {
|
||||
|
||||
@objc
|
||||
@@ -64,7 +66,7 @@ class ConvenienceTests: BaseTestCase {
|
||||
|
||||
self.prepareStack { (stack) in
|
||||
|
||||
_ = withExtendedLifetime(stack.beginUnsafe()) { (transaction: UnsafeDataTransaction) in
|
||||
withExtendedLifetime(stack.beginUnsafe()) { (transaction: UnsafeDataTransaction) in
|
||||
|
||||
let controller = transaction.createFetchedResultsController(
|
||||
From<TestEntity1>(),
|
||||
|
||||
@@ -37,78 +37,149 @@ import CoreStore
|
||||
#endif
|
||||
|
||||
class Animal: CoreStoreObject {
|
||||
|
||||
let species = Value.Required<String>("species", initial: "Swift")
|
||||
let master = Relationship.ToOne<Person>("master")
|
||||
let color = Transformable.Optional<Color>("color")
|
||||
|
||||
@Field.Stored("species")
|
||||
var species: String = "Swift"
|
||||
|
||||
@Field.Coded("color", coder: FieldCoders.NSCoding.self)
|
||||
var color: Color? = .blue
|
||||
|
||||
@Field.Relationship("master")
|
||||
var master: Person?
|
||||
}
|
||||
|
||||
class Dog: Animal {
|
||||
|
||||
let nickname = Value.Optional<String>("nickname")
|
||||
let age = Value.Required<Int>("age", initial: 1)
|
||||
let friends = Relationship.ToManyOrdered<Dog>("friends")
|
||||
let friendedBy = Relationship.ToManyUnordered<Dog>("friendedBy", inverse: { $0.friends })
|
||||
static let commonNicknames = ["Spot", "Benjie", "Max", "Milo"]
|
||||
|
||||
@Field.Stored(
|
||||
"nickname",
|
||||
dynamicInitialValue: {
|
||||
commonNicknames.randomElement()!
|
||||
}
|
||||
)
|
||||
var nickname: String
|
||||
|
||||
@Field.Stored("age")
|
||||
var age: Int = 1
|
||||
|
||||
@Field.Relationship("friends")
|
||||
var friends: [Dog]
|
||||
|
||||
@Field.Relationship("friendedBy", inverse: \.$friends)
|
||||
var friendedBy: Set<Dog>
|
||||
}
|
||||
|
||||
struct CustomType {
|
||||
var string = "customString"
|
||||
}
|
||||
|
||||
enum Job: String, CaseIterable {
|
||||
|
||||
case unemployed
|
||||
case engineer
|
||||
case doctor
|
||||
case lawyer
|
||||
|
||||
init?(data: Data) {
|
||||
|
||||
guard
|
||||
let rawValue = String(data: data, encoding: .utf8),
|
||||
let value = Self.init(rawValue: rawValue)
|
||||
else {
|
||||
|
||||
return nil
|
||||
}
|
||||
self = value
|
||||
}
|
||||
|
||||
func toData() -> Data {
|
||||
|
||||
return Data(self.rawValue.utf8)
|
||||
}
|
||||
}
|
||||
|
||||
class Person: CoreStoreObject {
|
||||
|
||||
let title = Value.Required<String>(
|
||||
|
||||
@Field.Stored(
|
||||
"title",
|
||||
initial: "Mr.",
|
||||
customSetter: Person.setTitle
|
||||
customSetter: { (object, field, newValue) in
|
||||
field.primitiveValue = newValue
|
||||
object.$displayName.primitiveValue = nil
|
||||
}
|
||||
)
|
||||
|
||||
let name = Value.Required<String>(
|
||||
var title: String = "Mr."
|
||||
|
||||
@Field.Stored(
|
||||
"name",
|
||||
initial: "",
|
||||
customSetter: Person.setName
|
||||
customSetter: { (object, field, newValue) in
|
||||
field.primitiveValue = newValue
|
||||
object.$displayName.primitiveValue = nil
|
||||
}
|
||||
)
|
||||
|
||||
let displayName = Value.Optional<String>(
|
||||
var name: String = ""
|
||||
|
||||
@Field.Virtual(
|
||||
"displayName",
|
||||
isTransient: true,
|
||||
customGetter: Person.getDisplayName(_:),
|
||||
customGetter: Person.getDisplayName(_:_:),
|
||||
affectedByKeyPaths: Person.keyPathsAffectingDisplayName()
|
||||
)
|
||||
var displayName: String?
|
||||
|
||||
let spouse = Relationship.ToOne<Person>("spouse")
|
||||
|
||||
let pets = Relationship.ToManyUnordered<Animal>("pets", inverse: { $0.master })
|
||||
@Field.Virtual(
|
||||
"customType",
|
||||
customGetter: { (object, field) in
|
||||
|
||||
private let _spouse = Relationship.ToOne<Person>("_spouseInverse", inverse: { $0.spouse })
|
||||
if let value = field.primitiveValue {
|
||||
|
||||
|
||||
private static func setTitle(_ partialObject: PartialObject<Person>, _ newValue: String) {
|
||||
|
||||
partialObject.setPrimitiveValue(newValue, for: { $0.title })
|
||||
partialObject.setPrimitiveValue(nil, for: { $0.displayName })
|
||||
}
|
||||
|
||||
private static func setName(_ partialObject: PartialObject<Person>, _ newValue: String) {
|
||||
|
||||
partialObject.setPrimitiveValue(newValue, for: { $0.name })
|
||||
partialObject.setPrimitiveValue(nil, for: { $0.displayName })
|
||||
}
|
||||
|
||||
static func getDisplayName(_ partialObject: PartialObject<Person>) -> String? {
|
||||
|
||||
if let displayName = partialObject.primitiveValue(for: { $0.displayName }) {
|
||||
|
||||
return displayName
|
||||
return value
|
||||
}
|
||||
let value = CustomType()
|
||||
field.primitiveValue = value
|
||||
return value
|
||||
}
|
||||
let title = partialObject.value(for: { $0.title })
|
||||
let name = partialObject.value(for: { $0.name })
|
||||
let displayName = "\(title) \(name)"
|
||||
partialObject.setPrimitiveValue(displayName, for: { $0.displayName })
|
||||
return displayName
|
||||
)
|
||||
var customField: CustomType
|
||||
|
||||
@Field.Coded(
|
||||
"job",
|
||||
coder: (
|
||||
encode: { $0.toData() },
|
||||
decode: { $0.flatMap(Job.init(data:)) ?? .unemployed }
|
||||
),
|
||||
dynamicInitialValue: {
|
||||
Job.allCases.randomElement()!
|
||||
}
|
||||
)
|
||||
var job: Job
|
||||
|
||||
@Field.Relationship("spouse")
|
||||
var spouse: Person?
|
||||
|
||||
@Field.Relationship("pets", inverse: \.$master)
|
||||
var pets: Set<Animal>
|
||||
|
||||
@Field.Relationship("_spouseInverse", inverse: \.$spouse)
|
||||
private var spouseInverse: Person?
|
||||
|
||||
private static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {
|
||||
|
||||
if let value = field.primitiveValue {
|
||||
|
||||
return value
|
||||
}
|
||||
let title = object.$title.value
|
||||
let name = object.$name.value
|
||||
let value = "\(title) \(name)"
|
||||
field.primitiveValue = value
|
||||
return value
|
||||
}
|
||||
|
||||
static func keyPathsAffectingDisplayName() -> Set<String> {
|
||||
private static func keyPathsAffectingDisplayName() -> Set<String> {
|
||||
|
||||
return [
|
||||
String(keyPath: \Person.title),
|
||||
String(keyPath: \Person.name)
|
||||
String(keyPath: \Person.$title),
|
||||
String(keyPath: \Person.$name)
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -131,20 +202,20 @@ class DynamicModelTests: BaseTestDataTestCase {
|
||||
],
|
||||
versionLock: [
|
||||
"Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
|
||||
"Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7],
|
||||
"Person": [0x2831cf046084d96d, 0xbe19b13ace54641, 0x635a082728b0f6f0, 0x3d4ef2dd4b74a87c]
|
||||
"Dog": [0xad6de93adc5565d, 0x7897e51253eba5a3, 0xd12b9ce0b13600f3, 0x5a4827cd794cd15e],
|
||||
"Person": [0xf3e6ba6016bbedc6, 0x50dedf64f0eba490, 0xa32088a0ee83468d, 0xb72d1d0b37bd0992]
|
||||
]
|
||||
)
|
||||
)
|
||||
self.prepareStack(dataStack, configurations: [nil]) { (stack) in
|
||||
|
||||
let k1 = String(keyPath: \Animal.species)
|
||||
let k1 = String(keyPath: \Animal.$species)
|
||||
XCTAssertEqual(k1, "species")
|
||||
|
||||
let k2 = String(keyPath: \Dog.species)
|
||||
let k2 = String(keyPath: \Dog.$species)
|
||||
XCTAssertEqual(k2, "species")
|
||||
|
||||
let k3 = String(keyPath: \Dog.nickname)
|
||||
let k3 = String(keyPath: \Dog.$nickname)
|
||||
XCTAssertEqual(k3, "nickname")
|
||||
|
||||
let updateDone = self.expectation(description: "update-done")
|
||||
@@ -156,27 +227,28 @@ class DynamicModelTests: BaseTestDataTestCase {
|
||||
asynchronous: { (transaction) in
|
||||
|
||||
let animal = transaction.create(Into<Animal>())
|
||||
XCTAssertEqual(animal.species.value, "Swift")
|
||||
XCTAssertTrue(type(of: animal.species.value) == String.self)
|
||||
XCTAssertEqual(animal.species, "Swift")
|
||||
XCTAssertTrue(type(of: animal.species) == String.self)
|
||||
XCTAssertEqual(animal.color, Color.blue)
|
||||
|
||||
animal.species .= "Sparrow"
|
||||
XCTAssertEqual(animal.species.value, "Sparrow")
|
||||
animal.species = "Sparrow"
|
||||
XCTAssertEqual(animal.species, "Sparrow")
|
||||
|
||||
animal.color .= .yellow
|
||||
XCTAssertEqual(animal.color.value, Color.yellow)
|
||||
animal.color = .yellow
|
||||
XCTAssertEqual(animal.color, Color.yellow)
|
||||
|
||||
for property in Animal.metaProperties(includeSuperclasses: true) {
|
||||
|
||||
switch property.keyPath {
|
||||
|
||||
case String(keyPath: \Animal.species):
|
||||
XCTAssertTrue(property is ValueContainer<Animal>.Required<String>)
|
||||
case String(keyPath: \Animal.$species):
|
||||
XCTAssertTrue(property is FieldContainer<Animal>.Stored<String>)
|
||||
|
||||
case String(keyPath: \Animal.master):
|
||||
XCTAssertTrue(property is RelationshipContainer<Animal>.ToOne<Person>)
|
||||
case String(keyPath: \Animal.$master):
|
||||
XCTAssertTrue(property is FieldContainer<Animal>.Relationship<Person?>)
|
||||
|
||||
case String(keyPath: \Animal.color):
|
||||
XCTAssertTrue(property is TransformableContainer<Animal>.Optional<Color>)
|
||||
case String(keyPath: \Animal.$color):
|
||||
XCTAssertTrue(property is FieldContainer<Animal>.Coded<Color?>)
|
||||
|
||||
default:
|
||||
XCTFail("Unknown KeyPath: \"\(property.keyPath)\"")
|
||||
@@ -184,64 +256,51 @@ class DynamicModelTests: BaseTestDataTestCase {
|
||||
}
|
||||
|
||||
let dog = transaction.create(Into<Dog>())
|
||||
XCTAssertEqual(dog.species.value, "Swift")
|
||||
XCTAssertEqual(dog.nickname.value, nil)
|
||||
XCTAssertEqual(dog.age.value, 1)
|
||||
XCTAssertEqual(dog.species, "Swift")
|
||||
XCTAssertEqual(dog.age, 1)
|
||||
XCTAssertTrue(Dog.commonNicknames.contains(dog.nickname))
|
||||
|
||||
for property in Dog.metaProperties(includeSuperclasses: true) {
|
||||
|
||||
switch property.keyPath {
|
||||
|
||||
case String(keyPath: \Dog.species):
|
||||
XCTAssertTrue(property is ValueContainer<Animal>.Required<String>)
|
||||
case String(keyPath: \Dog.$species):
|
||||
XCTAssertTrue(property is FieldContainer<Animal>.Stored<String>)
|
||||
|
||||
case String(keyPath: \Dog.master):
|
||||
XCTAssertTrue(property is RelationshipContainer<Animal>.ToOne<Person>)
|
||||
case String(keyPath: \Dog.$master):
|
||||
XCTAssertTrue(property is FieldContainer<Animal>.Relationship<Person?>)
|
||||
|
||||
case String(keyPath: \Dog.color):
|
||||
XCTAssertTrue(property is TransformableContainer<Animal>.Optional<Color>)
|
||||
case String(keyPath: \Dog.$color):
|
||||
XCTAssertTrue(property is FieldContainer<Animal>.Coded<Color?>)
|
||||
|
||||
case String(keyPath: \Dog.nickname):
|
||||
XCTAssertTrue(property is ValueContainer<Dog>.Optional<String>)
|
||||
case String(keyPath: \Dog.$nickname):
|
||||
XCTAssertTrue(property is FieldContainer<Dog>.Stored<String>)
|
||||
|
||||
case String(keyPath: \Dog.age):
|
||||
XCTAssertTrue(property is ValueContainer<Dog>.Required<Int>)
|
||||
case String(keyPath: \Dog.$age):
|
||||
XCTAssertTrue(property is FieldContainer<Dog>.Stored<Int>)
|
||||
|
||||
case String(keyPath: \Dog.friends):
|
||||
XCTAssertTrue(property is RelationshipContainer<Dog>.ToManyOrdered<Dog>)
|
||||
case String(keyPath: \Dog.$friends):
|
||||
XCTAssertTrue(property is FieldContainer<Dog>.Relationship<[Dog]>)
|
||||
|
||||
case String(keyPath: \Dog.friendedBy):
|
||||
XCTAssertTrue(property is RelationshipContainer<Dog>.ToManyUnordered<Dog>)
|
||||
case String(keyPath: \Dog.$friendedBy):
|
||||
XCTAssertTrue(property is FieldContainer<Dog>.Relationship<Set<Dog>>)
|
||||
|
||||
default:
|
||||
XCTFail("Unknown KeyPath: \"\(property.keyPath)\"")
|
||||
}
|
||||
}
|
||||
|
||||
// #if swift(>=5.1)
|
||||
//
|
||||
// let dogKeyPathBuilder = Dog.keyPathBuilder()
|
||||
// XCTAssertEqual(dogKeyPathBuilder.species.keyPathString, "SELF.species")
|
||||
// XCTAssertEqual(dogKeyPathBuilder.master.title.keyPathString, "SELF.master.title")
|
||||
// let a = dogKeyPathBuilder.master
|
||||
// let b = dogKeyPathBuilder.master.spouse
|
||||
// let c = dogKeyPathBuilder.master.spouse.pets
|
||||
// let d = dogKeyPathBuilder.master.spouse.pets.color
|
||||
// XCTAssertEqual(dogKeyPathBuilder.master.spouse.pets.color.keyPathString, "SELF.master.spouse.pets.color")
|
||||
//
|
||||
// #endif
|
||||
|
||||
let didSetObserver = dog.species.observe(options: [.new, .old]) { (object, change) in
|
||||
let didSetObserver = dog.observe(\.$species, options: [.new, .old]) { (object, change) in
|
||||
|
||||
XCTAssertEqual(object, dog)
|
||||
XCTAssertEqual(change.kind, .setting)
|
||||
XCTAssertEqual(change.newValue, "Dog")
|
||||
XCTAssertEqual(change.oldValue, "Swift")
|
||||
XCTAssertFalse(change.isPrior)
|
||||
XCTAssertEqual(object.species.value, "Dog")
|
||||
XCTAssertEqual(object.species, "Dog")
|
||||
didSetObserverDone.fulfill()
|
||||
}
|
||||
let willSetObserver = dog.species.observe(options: [.new, .old, .prior]) { (object, change) in
|
||||
let willSetObserver = dog.observe(\.$species, options: [.new, .old, .prior]) { (object, change) in
|
||||
|
||||
XCTAssertEqual(object, dog)
|
||||
XCTAssertEqual(change.kind, .setting)
|
||||
@@ -250,28 +309,31 @@ class DynamicModelTests: BaseTestDataTestCase {
|
||||
if change.isPrior {
|
||||
|
||||
XCTAssertNil(change.newValue)
|
||||
XCTAssertEqual(object.species.value, "Swift")
|
||||
XCTAssertEqual(object.species, "Swift")
|
||||
willSetPriorObserverDone.fulfill()
|
||||
}
|
||||
else {
|
||||
|
||||
XCTAssertEqual(change.newValue, "Dog")
|
||||
XCTAssertEqual(object.species.value, "Dog")
|
||||
XCTAssertEqual(object.species, "Dog")
|
||||
willSetNotPriorObserverDone.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
dog.species .= "Dog"
|
||||
XCTAssertEqual(dog.species.value, "Dog")
|
||||
dog.species = "Dog"
|
||||
XCTAssertEqual(dog.species, "Dog")
|
||||
|
||||
didSetObserver.invalidate()
|
||||
willSetObserver.invalidate()
|
||||
|
||||
dog.nickname .= "Spot"
|
||||
XCTAssertEqual(dog.nickname.value, "Spot")
|
||||
dog.nickname = "Spot"
|
||||
XCTAssertEqual(dog.nickname, "Spot")
|
||||
|
||||
let person = transaction.create(Into<Person>())
|
||||
XCTAssertTrue(person.pets.value.isEmpty)
|
||||
XCTAssertTrue(person.pets.isEmpty)
|
||||
XCTAssertEqual(person.customField.string, "customString")
|
||||
let initialJob = person.job
|
||||
XCTAssertTrue(Job.allCases.contains(initialJob))
|
||||
|
||||
XCTAssertEqual(
|
||||
person.rawObject!
|
||||
@@ -280,7 +342,7 @@ class DynamicModelTests: BaseTestDataTestCase {
|
||||
["title", "name"]
|
||||
)
|
||||
|
||||
person.name .= "Joe"
|
||||
person.name = "Joe"
|
||||
|
||||
XCTAssertEqual(person.rawObject!.value(forKey: "name") as! String?, "Joe")
|
||||
XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "Mr. Joe")
|
||||
@@ -288,46 +350,66 @@ class DynamicModelTests: BaseTestDataTestCase {
|
||||
person.rawObject!.setValue("AAAA", forKey: "displayName")
|
||||
XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "AAAA")
|
||||
|
||||
person.name .= "John"
|
||||
XCTAssertEqual(person.name.value, "John")
|
||||
XCTAssertEqual(person.displayName.value, "Mr. John") // Custom getter
|
||||
person.name = "John"
|
||||
XCTAssertEqual(person.name, "John")
|
||||
XCTAssertEqual(person.displayName, "Mr. John") // Custom getter
|
||||
|
||||
let personSnapshot1 = person.asSnapshot(in: transaction)!
|
||||
XCTAssertEqual(person.name.value, personSnapshot1.name)
|
||||
XCTAssertEqual(person.title.value, personSnapshot1.title)
|
||||
XCTAssertEqual(person.displayName.value, personSnapshot1.displayName)
|
||||
XCTAssertEqual(person.name, personSnapshot1.$name)
|
||||
XCTAssertEqual(person.title, personSnapshot1.$title)
|
||||
XCTAssertEqual(person.displayName, personSnapshot1.$displayName)
|
||||
XCTAssertEqual(person.job, personSnapshot1.$job)
|
||||
|
||||
person.title .= "Sir"
|
||||
XCTAssertEqual(person.displayName.value, "Sir John")
|
||||
person.title = "Sir"
|
||||
XCTAssertEqual(person.displayName, "Sir John")
|
||||
|
||||
XCTAssertEqual(personSnapshot1.name, "John")
|
||||
XCTAssertEqual(personSnapshot1.title, "Mr.")
|
||||
XCTAssertEqual(personSnapshot1.displayName, "Mr. John")
|
||||
XCTAssertEqual(personSnapshot1.$name, "John")
|
||||
XCTAssertEqual(personSnapshot1.$title, "Mr.")
|
||||
XCTAssertEqual(personSnapshot1.$displayName, "Mr. John")
|
||||
|
||||
person.customField.string = "newCustomString"
|
||||
XCTAssertEqual(person.customField.string, "newCustomString")
|
||||
|
||||
person.job = .engineer
|
||||
XCTAssertEqual(person.job, .engineer)
|
||||
|
||||
let personSnapshot2 = person.asSnapshot(in: transaction)!
|
||||
XCTAssertEqual(person.name.value, personSnapshot2.name)
|
||||
XCTAssertEqual(person.title.value, personSnapshot2.title)
|
||||
XCTAssertEqual(person.displayName.value, personSnapshot2.displayName)
|
||||
XCTAssertEqual(person.name, personSnapshot2.$name)
|
||||
XCTAssertEqual(person.title, personSnapshot2.$title)
|
||||
XCTAssertEqual(person.displayName, personSnapshot2.$displayName)
|
||||
XCTAssertEqual(person.job, personSnapshot2.$job)
|
||||
|
||||
var personSnapshot3 = personSnapshot2
|
||||
personSnapshot3.name = "James"
|
||||
XCTAssertEqual(personSnapshot1.name, "John")
|
||||
XCTAssertEqual(personSnapshot1.displayName, "Mr. John")
|
||||
XCTAssertEqual(personSnapshot2.name, "John")
|
||||
XCTAssertEqual(personSnapshot2.displayName, "Sir John")
|
||||
XCTAssertEqual(personSnapshot3.name, "James")
|
||||
XCTAssertEqual(personSnapshot3.displayName, "Sir John")
|
||||
personSnapshot3.$name = "James"
|
||||
XCTAssertEqual(personSnapshot1.$name, "John")
|
||||
XCTAssertEqual(personSnapshot1.$displayName, "Mr. John")
|
||||
XCTAssertEqual(personSnapshot1.$job, initialJob)
|
||||
XCTAssertEqual(personSnapshot2.$name, "John")
|
||||
XCTAssertEqual(personSnapshot2.$displayName, "Sir John")
|
||||
XCTAssertEqual(personSnapshot2.$job, .engineer)
|
||||
XCTAssertEqual(personSnapshot3.$name, "James")
|
||||
XCTAssertEqual(personSnapshot3.$displayName, "Sir John")
|
||||
XCTAssertEqual(personSnapshot3.$job, .engineer)
|
||||
|
||||
|
||||
|
||||
person.pets.value.insert(dog)
|
||||
person.pets.insert(dog)
|
||||
XCTAssertEqual(person.pets.count, 1)
|
||||
XCTAssertEqual(person.pets.value.first, dog)
|
||||
XCTAssertEqual(person.pets.value.first?.master.value, person)
|
||||
XCTAssertEqual(dog.master.value, person)
|
||||
XCTAssertEqual(dog.master.value?.pets.value.first, dog)
|
||||
XCTAssertEqual(person.pets.first, dog)
|
||||
XCTAssertEqual(person.pets.first?.master, person)
|
||||
XCTAssertEqual(dog.master, person)
|
||||
XCTAssertEqual(dog.master?.pets.first, dog)
|
||||
},
|
||||
success: { _ in
|
||||
|
||||
|
||||
let person = try! stack.fetchOne(From<Person>())
|
||||
XCTAssertNotNil(person)
|
||||
|
||||
let personPublisher = person!.asPublisher(in: stack)
|
||||
XCTAssertEqual(personPublisher.$name, "John")
|
||||
XCTAssertEqual(personPublisher.$displayName, "Sir John")
|
||||
XCTAssertEqual(personPublisher.$job, .engineer)
|
||||
|
||||
updateDone.fulfill()
|
||||
},
|
||||
failure: { _ in
|
||||
@@ -338,59 +420,67 @@ class DynamicModelTests: BaseTestDataTestCase {
|
||||
stack.perform(
|
||||
asynchronous: { (transaction) in
|
||||
|
||||
let p1 = Where<Animal>({ $0.species == "Sparrow" })
|
||||
let p1 = Where<Animal>({ $0.$species == "Sparrow" })
|
||||
XCTAssertEqual(p1.predicate, NSPredicate(format: "%K == %@", "species", "Sparrow"))
|
||||
|
||||
let bird = try transaction.fetchOne(From<Animal>(), p1)
|
||||
XCTAssertNotNil(bird)
|
||||
XCTAssertEqual(bird!.species.value, "Sparrow")
|
||||
XCTAssertEqual(bird!.species, "Sparrow")
|
||||
XCTAssertEqual(bird!.color, Color.yellow)
|
||||
|
||||
let p2 = Where<Dog>({ $0.nickname == "Spot" })
|
||||
let p2 = Where<Dog>({ $0.$nickname == "Spot" })
|
||||
XCTAssertEqual(p2.predicate, NSPredicate(format: "%K == %@", "nickname", "Spot"))
|
||||
|
||||
let dog = try transaction.fetchOne(From<Dog>().where(\.nickname == "Spot"))
|
||||
let dog = try transaction.fetchOne(From<Dog>().where(\.$nickname == "Spot"))
|
||||
XCTAssertNotNil(dog)
|
||||
XCTAssertEqual(dog!.nickname.value, "Spot")
|
||||
XCTAssertEqual(dog!.species.value, "Dog")
|
||||
XCTAssertEqual(dog!.nickname, "Spot")
|
||||
XCTAssertEqual(dog!.species, "Dog")
|
||||
|
||||
let person = try transaction.fetchOne(From<Person>())
|
||||
XCTAssertNotNil(person)
|
||||
XCTAssertEqual(person!.pets.value.first, dog)
|
||||
XCTAssertEqual(person!.name, "John")
|
||||
XCTAssertEqual(person!.title, "Sir")
|
||||
XCTAssertEqual(person!.displayName, "Sir John")
|
||||
XCTAssertEqual(person!.customField.string, "customString")
|
||||
XCTAssertEqual(person!.job, .engineer)
|
||||
XCTAssertEqual(person!.pets.first, dog)
|
||||
|
||||
let p3 = Where<Dog>({ $0.age == 10 })
|
||||
let p3 = Where<Dog>({ $0.$age == 10 })
|
||||
XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10))
|
||||
|
||||
let totalAge = try transaction.queryValue(From<Dog>().select(Int.self, .sum(\Dog.age)))
|
||||
let totalAge = try transaction.queryValue(
|
||||
From<Dog>().select(Int.self, .sum(\.$age))
|
||||
)
|
||||
XCTAssertEqual(totalAge, 1)
|
||||
|
||||
_ = try transaction.fetchAll(
|
||||
From<Dog>()
|
||||
.where(\Animal.species == "Dog" && \Dog.age == 10)
|
||||
.where(\Animal.$species == "Dog" && \Dog.$age == 10)
|
||||
)
|
||||
_ = try transaction.fetchAll(
|
||||
From<Dog>()
|
||||
.where(\Dog.age == 10 && \Animal.species == "Dog")
|
||||
.orderBy(.ascending({ $0.species }))
|
||||
.where(\Dog.$age == 10 && \Animal.$species == "Dog")
|
||||
.orderBy(.ascending({ $0.$species }))
|
||||
)
|
||||
_ = try transaction.fetchAll(
|
||||
From<Dog>(),
|
||||
Where<Dog>({ $0.age > 10 && $0.age <= 15 })
|
||||
Where<Dog>({ $0.$age > 10 && $0.$age <= 15 })
|
||||
)
|
||||
_ = try transaction.fetchAll(
|
||||
From<Dog>(),
|
||||
Where<Dog>({ $0.species == "Dog" && $0.age == 10 })
|
||||
Where<Dog>({ $0.$species == "Dog" && $0.$age == 10 })
|
||||
)
|
||||
_ = try transaction.fetchAll(
|
||||
From<Dog>(),
|
||||
Where<Dog>({ $0.age == 10 && $0.species == "Dog" })
|
||||
Where<Dog>({ $0.$age == 10 && $0.$species == "Dog" })
|
||||
)
|
||||
_ = try transaction.fetchAll(
|
||||
From<Dog>(),
|
||||
Where<Dog>({ $0.age > 10 && $0.age <= 15 })
|
||||
Where<Dog>({ $0.$age > 10 && $0.$age <= 15 })
|
||||
)
|
||||
_ = try transaction.fetchAll(
|
||||
From<Dog>(),
|
||||
(\Dog.age > 10 && \Dog.age <= 15)
|
||||
(\Dog.$age > 10 && \Dog.$age <= 15)
|
||||
)
|
||||
},
|
||||
success: { _ in
|
||||
@@ -402,15 +492,20 @@ class DynamicModelTests: BaseTestDataTestCase {
|
||||
XCTFail()
|
||||
}
|
||||
)
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
|
||||
self.waitForExpectations(timeout: 10, handler: { _ in })
|
||||
|
||||
self.addTeardownBlock {
|
||||
dataStack.unsafeRemoveAllPersistentStoresAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() {
|
||||
|
||||
XCTAssertEqual(String(keyPath: \Animal.species), "species")
|
||||
XCTAssertEqual(String(keyPath: \Dog.species), "species")
|
||||
XCTAssertEqual(String(keyPath: \Animal.$species), "species")
|
||||
XCTAssertEqual(String(keyPath: \Dog.$species), "species")
|
||||
}
|
||||
|
||||
@nonobjc
|
||||
|
||||
@@ -132,8 +132,8 @@ final class FetchTests: BaseTestDataTestCase {
|
||||
}
|
||||
)
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -267,8 +267,8 @@ final class FetchTests: BaseTestDataTestCase {
|
||||
}
|
||||
)
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
|
||||
@objc
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
@@ -31,7 +32,6 @@ import CoreStore
|
||||
|
||||
// MARK: - ListObserverTests
|
||||
|
||||
@available(macOS 10.12, *)
|
||||
class ListObserverTests: BaseTestDataTestCase {
|
||||
|
||||
@objc
|
||||
@@ -53,7 +53,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
|
||||
var events = 0
|
||||
|
||||
let willChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -67,7 +67,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 0
|
||||
}
|
||||
)
|
||||
let didInsertSectionExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertSection:toSectionIndex:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -87,7 +87,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 1
|
||||
}
|
||||
)
|
||||
let didInsertObjectExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertObject:toIndexPath:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -119,7 +119,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 2
|
||||
}
|
||||
)
|
||||
let didChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -184,7 +184,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
|
||||
var events = 0
|
||||
|
||||
let willChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -199,7 +199,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
}
|
||||
)
|
||||
|
||||
let didUpdateObjectExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitor:didUpdateObject:atIndexPath:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -250,7 +250,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 1 || events == 2
|
||||
}
|
||||
)
|
||||
let didChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -329,7 +329,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
|
||||
var events = 0
|
||||
|
||||
let willChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -343,7 +343,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 0
|
||||
}
|
||||
)
|
||||
let didMoveObjectExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitor:didMoveObject:fromIndexPath:toIndexPath:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -376,7 +376,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 1
|
||||
}
|
||||
)
|
||||
let didChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -437,7 +437,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
|
||||
var events = 0
|
||||
|
||||
let willChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -451,7 +451,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 0
|
||||
}
|
||||
)
|
||||
let didUpdateObjectExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitor:didDeleteObject:fromIndexPath:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -480,7 +480,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 1 || events == 2
|
||||
}
|
||||
)
|
||||
let didDeleteSectionExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitor:didDeleteSection:fromSectionIndex:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -508,7 +508,7 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
return events == 3
|
||||
}
|
||||
)
|
||||
let didChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -551,7 +551,6 @@ class ListObserverTests: BaseTestDataTestCase {
|
||||
|
||||
// MARK: TestListObserver
|
||||
|
||||
@available(macOS 10.12, *)
|
||||
class TestListObserver: ListSectionObserver {
|
||||
|
||||
// MARK: ListObserver
|
||||
|
||||
@@ -31,7 +31,6 @@ import CoreStore
|
||||
|
||||
// MARK: - ObjectObserverTests
|
||||
|
||||
@available(macOS 10.12, *)
|
||||
class ObjectObserverTests: BaseTestDataTestCase {
|
||||
|
||||
@objc
|
||||
@@ -57,7 +56,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
|
||||
|
||||
var events = 0
|
||||
|
||||
let willUpdateExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -74,7 +73,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
|
||||
return events == 0
|
||||
}
|
||||
)
|
||||
let didUpdateExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "objectMonitor:didUpdateObject:changedPersistentKeys:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -154,7 +153,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
|
||||
|
||||
var events = 0
|
||||
|
||||
let didDeleteExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -203,7 +202,6 @@ class ObjectObserverTests: BaseTestDataTestCase {
|
||||
|
||||
// MARK: TestObjectObserver
|
||||
|
||||
@available(macOS 10.12, *)
|
||||
class TestObjectObserver: ObjectObserver {
|
||||
|
||||
typealias ObjectEntityType = TestEntity1
|
||||
|
||||
@@ -31,7 +31,6 @@ import CoreStore
|
||||
|
||||
// MARK: - ObjectPublisherTests
|
||||
|
||||
@available(macOS 10.12, *)
|
||||
class ObjectPublisherTests: BaseTestDataTestCase {
|
||||
|
||||
@objc
|
||||
@@ -144,6 +143,7 @@ class ObjectPublisherTests: BaseTestDataTestCase {
|
||||
XCTFail()
|
||||
}
|
||||
)
|
||||
|
||||
self.waitAndCheckExpectations()
|
||||
|
||||
withExtendedLifetime(objectPublisher, {})
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
@@ -31,7 +32,6 @@ import CoreStore
|
||||
|
||||
//MARK: - SectionByTests
|
||||
|
||||
@available(macOS 10.12, *)
|
||||
final class SectionByTests: XCTestCase {
|
||||
|
||||
@objc
|
||||
@@ -41,11 +41,14 @@ final class SectionByTests: XCTestCase {
|
||||
|
||||
let sectionBy = SectionBy<NSManagedObject>("key")
|
||||
XCTAssertEqual(sectionBy.sectionKeyPath, "key")
|
||||
XCTAssertEqual(sectionBy.sectionIndexTransformer("key"), "key")
|
||||
XCTAssertNil(sectionBy.sectionIndexTransformer("key"))
|
||||
}
|
||||
do {
|
||||
|
||||
let sectionBy = SectionBy<NSManagedObject>("key") { $0.flatMap { "\($0):suffix" } }
|
||||
let sectionBy = SectionBy<NSManagedObject>(
|
||||
"key",
|
||||
sectionIndexTransformer: { $0.flatMap { "\($0):suffix" } }
|
||||
)
|
||||
XCTAssertEqual(sectionBy.sectionKeyPath, "key")
|
||||
XCTAssertEqual(sectionBy.sectionIndexTransformer("key"), "key:suffix")
|
||||
XCTAssertNil(sectionBy.sectionIndexTransformer(nil))
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
import CoreStore
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
@@ -83,21 +84,11 @@ final class StorageInterfaceTests: XCTestCase {
|
||||
let store = SQLiteStore()
|
||||
XCTAssertEqual(type(of: store).storeType, NSSQLiteStoreType)
|
||||
XCTAssertNil(store.configuration)
|
||||
if #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"]] as NSDictionary
|
||||
)
|
||||
}
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
|
||||
XCTAssertEqual(store.fileURL, SQLiteStore.defaultFileURL)
|
||||
XCTAssertTrue(store.migrationMappingProviders.isEmpty)
|
||||
@@ -123,21 +114,11 @@ final class StorageInterfaceTests: XCTestCase {
|
||||
)
|
||||
XCTAssertEqual(type(of: store).storeType, NSSQLiteStoreType)
|
||||
XCTAssertEqual(store.configuration, "config1")
|
||||
if #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"]] as NSDictionary
|
||||
)
|
||||
}
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
|
||||
XCTAssertEqual(store.fileURL, fileURL)
|
||||
XCTAssertEqual(store.migrationMappingProviders as! [XcodeSchemaMappingProvider], [mappingProvider])
|
||||
@@ -160,21 +141,11 @@ final class StorageInterfaceTests: XCTestCase {
|
||||
)
|
||||
XCTAssertEqual(type(of: store).storeType, NSSQLiteStoreType)
|
||||
XCTAssertEqual(store.configuration, "config1")
|
||||
if #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"]] as NSDictionary
|
||||
)
|
||||
}
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
|
||||
XCTAssertEqual(store.fileURL.deletingLastPathComponent(), SQLiteStore.defaultRootDirectory)
|
||||
XCTAssertEqual(store.fileURL.lastPathComponent, fileName)
|
||||
@@ -209,21 +180,11 @@ final class StorageInterfaceTests: XCTestCase {
|
||||
let store = SQLiteStore.legacy()
|
||||
XCTAssertEqual(type(of: store).storeType, NSSQLiteStoreType)
|
||||
XCTAssertNil(store.configuration)
|
||||
if #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"]] as NSDictionary
|
||||
)
|
||||
}
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
|
||||
XCTAssertEqual(store.fileURL, SQLiteStore.legacyDefaultFileURL)
|
||||
XCTAssertTrue(store.migrationMappingProviders.isEmpty)
|
||||
@@ -246,21 +207,11 @@ final class StorageInterfaceTests: XCTestCase {
|
||||
)
|
||||
XCTAssertEqual(type(of: store).storeType, NSSQLiteStoreType)
|
||||
XCTAssertEqual(store.configuration, "config1")
|
||||
if #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"]] as NSDictionary
|
||||
)
|
||||
}
|
||||
XCTAssertEqual(
|
||||
store.storeOptions as NSDictionary?,
|
||||
[NSSQLitePragmasOption: ["journal_mode": "WAL"],
|
||||
NSBinaryStoreInsecureDecodingCompatibilityOption: true] as NSDictionary
|
||||
)
|
||||
|
||||
XCTAssertEqual(store.fileURL.deletingLastPathComponent(), SQLiteStore.legacyDefaultRootDirectory)
|
||||
XCTAssertEqual(store.fileURL.lastPathComponent, fileName)
|
||||
|
||||
@@ -383,8 +383,6 @@ final class TransactionTests: BaseTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(macOS 10.12, *)
|
||||
@objc
|
||||
dynamic func test_ThatSynchronousTransactions_CanCommitWithoutWaitingForMerges() {
|
||||
|
||||
@@ -400,7 +398,7 @@ final class TransactionTests: BaseTestCase {
|
||||
XCTAssertFalse(monitor.hasObjects())
|
||||
|
||||
var events = 0
|
||||
let willChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -414,7 +412,7 @@ final class TransactionTests: BaseTestCase {
|
||||
return events == 0
|
||||
}
|
||||
)
|
||||
let didInsertObjectExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertObject:toIndexPath:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -444,7 +442,7 @@ final class TransactionTests: BaseTestCase {
|
||||
return events == 1
|
||||
}
|
||||
)
|
||||
let didChangeExpectation = self.expectation(
|
||||
_ = self.expectation(
|
||||
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
|
||||
object: observer,
|
||||
handler: { (note) -> Bool in
|
||||
@@ -622,8 +620,8 @@ final class TransactionTests: BaseTestCase {
|
||||
}
|
||||
)
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -755,8 +753,8 @@ final class TransactionTests: BaseTestCase {
|
||||
}
|
||||
)
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -896,8 +894,8 @@ final class TransactionTests: BaseTestCase {
|
||||
}
|
||||
)
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
self.waitAndCheckExpectations()
|
||||
}
|
||||
|
||||
@objc
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
@@ -67,7 +68,7 @@ final class WhereTests: XCTestCase {
|
||||
dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() {
|
||||
|
||||
XCTAssertAllEqual(String(keyPath: \TestEntity1.testEntityID), "testEntityID")
|
||||
XCTAssertAllEqual(String(keyPath: \Animal.color), "color")
|
||||
XCTAssertAllEqual(String(keyPath: \Animal.$color), "color")
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -77,17 +78,10 @@ final class WhereTests: XCTestCase {
|
||||
|
||||
do {
|
||||
|
||||
// let keyPathBuilder = TestEntity1.keyPathBuilder()
|
||||
|
||||
// let kp = \TestEntity1.testToOne
|
||||
// print(keyPathBuilder.testString)
|
||||
// print(keyPathBuilder.testToOne)
|
||||
// print(keyPathBuilder.testToOne.testEntityID)
|
||||
XCTAssertAllEqual(
|
||||
#keyPath(TestEntity1.testToOne.testEntityID),
|
||||
(\TestEntity1.testToOne ~ \.testEntityID).description,
|
||||
String(keyPath: \TestEntity1.testToOne ~ \.testEntityID)
|
||||
// keyPathBuilder.testToOne.testEntityID.keyPathString
|
||||
)
|
||||
XCTAssertAllEqual(
|
||||
#keyPath(TestEntity1.testToOne.testToOne.testToManyUnordered),
|
||||
@@ -104,18 +98,18 @@ final class WhereTests: XCTestCase {
|
||||
|
||||
XCTAssertAllEqual(
|
||||
"master.pets",
|
||||
(\Animal.master ~ \.pets).description,
|
||||
String(keyPath: \Animal.master ~ \.pets)
|
||||
(\Animal.$master ~ \.$pets).description,
|
||||
String(keyPath: \Animal.$master ~ \.$pets)
|
||||
)
|
||||
XCTAssertAllEqual(
|
||||
"master.pets.species",
|
||||
(\Animal.master ~ \.pets ~ \.species).description,
|
||||
String(keyPath: \Animal.master ~ \.pets ~ \.species)
|
||||
(\Animal.$master ~ \.$pets ~ \.$species).description,
|
||||
String(keyPath: \Animal.$master ~ \.$pets ~ \.$species)
|
||||
)
|
||||
XCTAssertAllEqual(
|
||||
"master.pets.master",
|
||||
(\Animal.master ~ \.pets ~ \.master).description,
|
||||
String(keyPath: \Animal.master ~ \.pets ~ \.master)
|
||||
(\Animal.$master ~ \.$pets ~ \.$master).description,
|
||||
String(keyPath: \Animal.$master ~ \.$pets ~ \.$master)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -138,8 +132,8 @@ final class WhereTests: XCTestCase {
|
||||
|
||||
XCTAssertAllEqual(
|
||||
"master.pets.@count",
|
||||
(\Animal.master ~ \.pets).count().description,
|
||||
String(keyPath: (\Animal.master ~ \.pets).count())
|
||||
(\Animal.$master ~ \.$pets).count().description,
|
||||
String(keyPath: (\Animal.$master ~ \.$pets).count())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -162,13 +156,13 @@ final class WhereTests: XCTestCase {
|
||||
|
||||
XCTAssertAllEqual(
|
||||
"ANY master.pets",
|
||||
(\Animal.master ~ \.pets).any().description,
|
||||
String(keyPath: (\Animal.master ~ \.pets).any())
|
||||
(\Animal.$master ~ \.$pets).any().description,
|
||||
String(keyPath: (\Animal.$master ~ \.$pets).any())
|
||||
)
|
||||
XCTAssertAllEqual(
|
||||
"ANY master.pets.species",
|
||||
(\Animal.master ~ \.pets ~ \.species).any().description,
|
||||
String(keyPath: (\Animal.master ~ \.pets ~ \.species).any())
|
||||
(\Animal.$master ~ \.$pets ~ \.$species).any().description,
|
||||
String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).any())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -191,13 +185,13 @@ final class WhereTests: XCTestCase {
|
||||
|
||||
XCTAssertAllEqual(
|
||||
"ALL master.pets",
|
||||
(\Animal.master ~ \.pets).all().description,
|
||||
String(keyPath: (\Animal.master ~ \.pets).all())
|
||||
(\Animal.$master ~ \.$pets).all().description,
|
||||
String(keyPath: (\Animal.$master ~ \.$pets).all())
|
||||
)
|
||||
XCTAssertAllEqual(
|
||||
"ALL master.pets.species",
|
||||
(\Animal.master ~ \.pets ~ \.species).all().description,
|
||||
String(keyPath: (\Animal.master ~ \.pets ~ \.species).all())
|
||||
(\Animal.$master ~ \.$pets ~ \.$species).all().description,
|
||||
String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).all())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -220,13 +214,13 @@ final class WhereTests: XCTestCase {
|
||||
|
||||
XCTAssertAllEqual(
|
||||
"NONE master.pets",
|
||||
(\Animal.master ~ \.pets).none().description,
|
||||
String(keyPath: (\Animal.master ~ \.pets).none())
|
||||
(\Animal.$master ~ \.$pets).none().description,
|
||||
String(keyPath: (\Animal.$master ~ \.$pets).none())
|
||||
)
|
||||
XCTAssertAllEqual(
|
||||
"NONE master.pets.species",
|
||||
(\Animal.master ~ \.pets ~ \.species).none().description,
|
||||
String(keyPath: (\Animal.master ~ \.pets ~ \.species).none())
|
||||
(\Animal.$master ~ \.$pets ~ \.$species).none().description,
|
||||
String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).none())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -247,7 +241,7 @@ final class WhereTests: XCTestCase {
|
||||
}
|
||||
do {
|
||||
|
||||
let whereClause: Where<Animal> = (\.master ~ \.name) == dummy
|
||||
let whereClause: Where<Animal> = (\.$master ~ \.$name) == dummy
|
||||
let predicate = NSPredicate(format: "master.name == %@", dummy)
|
||||
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
|
||||
XCTAssertAllEqual(whereClause.predicate, predicate)
|
||||
@@ -265,7 +259,7 @@ final class WhereTests: XCTestCase {
|
||||
}
|
||||
do {
|
||||
|
||||
let whereClause: Where<Animal> = (\.master ~ \.spouse ~ \.name) == dummy
|
||||
let whereClause: Where<Animal> = (\.$master ~ \.$spouse ~ \.$name) == dummy
|
||||
let predicate = NSPredicate(format: "master.spouse.name == %@", dummy)
|
||||
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
|
||||
XCTAssertAllEqual(whereClause.predicate, predicate)
|
||||
@@ -283,7 +277,7 @@ final class WhereTests: XCTestCase {
|
||||
}
|
||||
do {
|
||||
|
||||
let whereClause: Where<Animal> = (\.master ~ \.pets).count() == count
|
||||
let whereClause: Where<Animal> = (\.$master ~ \.$pets).count() == count
|
||||
let predicate = NSPredicate(format: "master.pets.@count == %d", count)
|
||||
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
|
||||
XCTAssertAllEqual(whereClause.predicate, predicate)
|
||||
@@ -301,7 +295,7 @@ final class WhereTests: XCTestCase {
|
||||
}
|
||||
do {
|
||||
|
||||
let whereClause: Where<Animal> = (\.master ~ \.pets ~ \.species).any() == dummy
|
||||
let whereClause: Where<Animal> = (\.$master ~ \.$pets ~ \.$species).any() == dummy
|
||||
let predicate = NSPredicate(format: "ANY master.pets.species == %@", dummy)
|
||||
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
|
||||
XCTAssertAllEqual(whereClause.predicate, predicate)
|
||||
@@ -319,7 +313,7 @@ final class WhereTests: XCTestCase {
|
||||
}
|
||||
do {
|
||||
|
||||
let whereClause: Where<Animal> = (\.master ~ \.pets ~ \.species).all() == dummy
|
||||
let whereClause: Where<Animal> = (\.$master ~ \.$pets ~ \.$species).all() == dummy
|
||||
let predicate = NSPredicate(format: "ALL master.pets.species == %@", dummy)
|
||||
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
|
||||
XCTAssertAllEqual(whereClause.predicate, predicate)
|
||||
@@ -337,7 +331,7 @@ final class WhereTests: XCTestCase {
|
||||
}
|
||||
do {
|
||||
|
||||
let whereClause: Where<Animal> = (\.master ~ \.pets ~ \.species).none() == dummy
|
||||
let whereClause: Where<Animal> = (\.$master ~ \.$pets ~ \.$species).none() == dummy
|
||||
let predicate = NSPredicate(format: "NONE master.pets.species == %@", dummy)
|
||||
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
|
||||
XCTAssertAllEqual(whereClause.predicate, predicate)
|
||||
|
||||
1038
Demo/Demo.xcodeproj/project.pbxproj
Normal file
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
LastUpgradeVersion = "1200"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -14,10 +14,10 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B54AAD481AF4D26E00848AE0"
|
||||
BuildableName = "CoreStoreDemo.app"
|
||||
BlueprintName = "CoreStoreDemo"
|
||||
ReferencedContainer = "container:CoreStoreDemo.xcodeproj">
|
||||
BlueprintIdentifier = "B5A3911824E5429200E7E8BD"
|
||||
BuildableName = "Demo.app"
|
||||
BlueprintName = "Demo"
|
||||
ReferencedContainer = "container:Demo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -27,15 +27,6 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B54AAD481AF4D26E00848AE0"
|
||||
BuildableName = "CoreStoreDemo.app"
|
||||
BlueprintName = "CoreStoreDemo"
|
||||
ReferencedContainer = "container:CoreStoreDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
@@ -53,10 +44,10 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B54AAD481AF4D26E00848AE0"
|
||||
BuildableName = "CoreStoreDemo.app"
|
||||
BlueprintName = "CoreStoreDemo"
|
||||
ReferencedContainer = "container:CoreStoreDemo.xcodeproj">
|
||||
BlueprintIdentifier = "B5A3911824E5429200E7E8BD"
|
||||
BuildableName = "Demo.app"
|
||||
BlueprintName = "Demo"
|
||||
ReferencedContainer = "container:Demo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
@@ -70,10 +61,10 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "B54AAD481AF4D26E00848AE0"
|
||||
BuildableName = "CoreStoreDemo.app"
|
||||
BlueprintName = "CoreStoreDemo"
|
||||
ReferencedContainer = "container:CoreStoreDemo.xcodeproj">
|
||||
BlueprintIdentifier = "B5A3911824E5429200E7E8BD"
|
||||
BuildableName = "Demo.app"
|
||||
BlueprintName = "Demo"
|
||||
ReferencedContainer = "container:Demo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
70
Demo/Info.plist
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarTintParameters</key>
|
||||
<dict>
|
||||
<key>UINavigationBar</key>
|
||||
<dict>
|
||||
<key>Style</key>
|
||||
<string>UIBarStyleDefault</string>
|
||||
<key>Translucent</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
42
Demo/Rakefile
Normal file
@@ -0,0 +1,42 @@
|
||||
# coding: utf-8
|
||||
|
||||
task :format do
|
||||
|
||||
require 'xcodeproj'
|
||||
require 'fileutils'
|
||||
|
||||
project_path = 'Demo.xcodeproj'
|
||||
ignore_targets = []
|
||||
|
||||
# http://www.rubydoc.info/github/CocoaPods/Xcodeproj
|
||||
project = Xcodeproj::Project.open(project_path)
|
||||
validTargets = project.targets.select { |target| target.respond_to?(:product_type) }
|
||||
|
||||
validTargets.each do |target|
|
||||
if ignore_targets.include?(target.display_name)
|
||||
next
|
||||
end
|
||||
case target.product_type
|
||||
|
||||
when 'com.apple.product-type.application', 'com.apple.product-type.framework'
|
||||
target.source_build_phase.files.sort! do |f1, f2|
|
||||
result = (f1.display_name.count "+") <=> (f2.display_name.count "+")
|
||||
if result != 0
|
||||
next -result
|
||||
end
|
||||
result = (f1.display_name.count "-") <=> (f2.display_name.count "-")
|
||||
if result != 0
|
||||
next -result
|
||||
end
|
||||
result = (f1.display_name.count ".") <=> (f2.display_name.count ".")
|
||||
if result != 0
|
||||
next result
|
||||
end
|
||||
(f1.display_name <=> f2.display_name)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
project.save()
|
||||
end
|
||||
60
Demo/Resources/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" 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 interfaceStyle="dark" id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Bp2-lt-3DL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright © 2020 John Rommel Estropia. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="Yn3-8H-uzI">
|
||||
<rect key="frame" x="20" y="827" width="374" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="CoreStoreIcon" translatesAutoresizingMaskIntoConstraints="NO" id="IrK-8p-pit">
|
||||
<rect key="frame" x="122" y="228.5" width="170" height="170"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="IrK-8p-pit" secondAttribute="height" multiplier="1:1" id="WaM-8F-33r"/>
|
||||
<constraint firstAttribute="width" constant="170" id="dlo-1N-ikz"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="CoreStore" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="8Vu-0U-3hd">
|
||||
<rect key="frame" x="20" y="418.5" width="374" height="57.5"/>
|
||||
<fontDescription key="fontDescription" name="HelveticaNeue-UltraLight" family="Helvetica Neue" pointSize="50"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Yn3-8H-uzI" firstAttribute="leading" secondItem="Bp2-lt-3DL" secondAttribute="leading" constant="20" symbolic="YES" id="7Dq-xP-k2v"/>
|
||||
<constraint firstItem="IrK-8p-pit" firstAttribute="centerY" secondItem="Bp2-lt-3DL" secondAttribute="centerY" multiplier="0.7" id="HUz-XL-l27"/>
|
||||
<constraint firstItem="IrK-8p-pit" firstAttribute="centerX" secondItem="Bp2-lt-3DL" secondAttribute="centerX" id="TSf-yM-hl1"/>
|
||||
<constraint firstAttribute="centerX" secondItem="8Vu-0U-3hd" secondAttribute="centerX" id="TX2-HT-cKs"/>
|
||||
<constraint firstItem="Z3i-EZ-UGs" firstAttribute="bottom" secondItem="Yn3-8H-uzI" secondAttribute="bottom" constant="14" id="hAb-SJ-Qnm"/>
|
||||
<constraint firstAttribute="centerX" secondItem="Yn3-8H-uzI" secondAttribute="centerX" id="pNf-eo-RXZ"/>
|
||||
<constraint firstItem="8Vu-0U-3hd" firstAttribute="leading" secondItem="Bp2-lt-3DL" secondAttribute="leading" constant="20" symbolic="YES" id="pef-yD-C5e"/>
|
||||
<constraint firstItem="8Vu-0U-3hd" firstAttribute="top" secondItem="IrK-8p-pit" secondAttribute="bottom" constant="20" id="xQP-tq-hNL"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Z3i-EZ-UGs"/>
|
||||
</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="CoreStoreIcon" width="170" height="170"/>
|
||||
</resources>
|
||||
</document>
|
||||
115
Demo/Resources/Images.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-60@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-60@3x-1.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-76@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-83.5@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "Mask + Oval 1 + Oval 1 + Oval 1.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"idiom" : "car",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon-60@3x.png",
|
||||
"idiom" : "car",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
6
Demo/Resources/Images.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
15
Demo/Resources/Images.xcassets/CoreStoreIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "CoreStoreIcon.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
||||
BIN
Demo/Resources/Images.xcassets/CoreStoreIcon.imageset/CoreStoreIcon.pdf
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
33
Demo/⭐️Sources/AppDelegate.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
// MARK: - AppDelegate
|
||||
|
||||
@UIApplicationMain
|
||||
@objc final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
// MARK: UIApplicationDelegate
|
||||
|
||||
@objc dynamic func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@objc dynamic func application(
|
||||
_ application: UIApplication,
|
||||
configurationForConnecting connectingSceneSession: UISceneSession,
|
||||
options: UIScene.ConnectionOptions
|
||||
) -> UISceneConfiguration {
|
||||
|
||||
return UISceneConfiguration(
|
||||
name: "Default Configuration",
|
||||
sessionRole: connectingSceneSession.role
|
||||
)
|
||||
}
|
||||
}
|
||||
69
Demo/⭐️Sources/Helpers/ImageDownloader.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
// MARK: - ImageDownloader
|
||||
|
||||
final class ImageDownloader: ObservableObject {
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
private(set) var image: UIImage?
|
||||
|
||||
let url: URL?
|
||||
|
||||
init(url: URL?) {
|
||||
|
||||
self.url = url
|
||||
guard let url = url else {
|
||||
|
||||
return
|
||||
}
|
||||
if let image = Self.cache[url] {
|
||||
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
|
||||
func fetchImage(completion: @escaping (UIImage) -> Void = { _ in }) {
|
||||
|
||||
guard let url = url else {
|
||||
|
||||
return
|
||||
}
|
||||
if let image = Self.cache[url] {
|
||||
|
||||
self.objectWillChange.send()
|
||||
self.image = image
|
||||
completion(image)
|
||||
return
|
||||
}
|
||||
self.cancellable = URLSession.shared
|
||||
.dataTaskPublisher(for: url)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { output in
|
||||
|
||||
if let image = UIImage(data: output.data) {
|
||||
|
||||
Self.cache[url] = image
|
||||
self.objectWillChange.send()
|
||||
self.image = image
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private static var cache: [URL: UIImage] = [:]
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
}
|
||||
58
Demo/⭐️Sources/Helpers/InstructionsView.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - InstructionsView
|
||||
|
||||
struct InstructionsView: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(_ rows: (header: String, description: String)...) {
|
||||
|
||||
self.rows = rows.map({ .init(header: $0, description: $1) })
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color.white)
|
||||
.shadow(color: Color(.sRGB, white: 0.5, opacity: 0.3), radius: 2, x: 1, y: 1)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
ForEach(self.rows, id: \.header) { row in
|
||||
HStack(alignment: .firstTextBaseline, spacing: 5) {
|
||||
Text(row.header)
|
||||
.font(.callout)
|
||||
.fontWeight(.bold)
|
||||
Text(row.description)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(Color(.sRGB, white: 0, opacity: 0.8))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.fixedSize()
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let rows: [InstructionsView.Row]
|
||||
|
||||
|
||||
// MARK: - Row
|
||||
|
||||
struct Row: Hashable {
|
||||
|
||||
// MARK: Internal
|
||||
let header: String
|
||||
let description: String
|
||||
}
|
||||
}
|
||||
|
||||
29
Demo/⭐️Sources/Helpers/LazyView.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - LazyView
|
||||
|
||||
struct LazyView<Content: View>: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(_ load: @escaping () -> Content) {
|
||||
|
||||
self.load = load
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: Content {
|
||||
|
||||
self.load()
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let load: () -> Content
|
||||
}
|
||||
71
Demo/⭐️Sources/Helpers/Menu/Menu.ItemView.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
extension Menu {
|
||||
|
||||
// MARK: - Menu.ItemView
|
||||
|
||||
struct ItemView<Destination: View>: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
destination: @escaping () -> Destination
|
||||
) {
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.destination = destination
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: LazyView(self.destination)) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(self.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
self.subtitle.map {
|
||||
Text($0)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
fileprivate let title: String
|
||||
fileprivate let subtitle: String?
|
||||
fileprivate let destination: () -> Destination
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Menu_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
Menu.ItemView(
|
||||
title: "Item Title",
|
||||
subtitle: "A subtitle caption for this item",
|
||||
destination: {
|
||||
Color.blue
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
134
Demo/⭐️Sources/Helpers/Menu/Menu.MainView.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
extension Menu {
|
||||
|
||||
// MARK: - Menu.MainView
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
Section(header: Text("Modern (CoreStoreObject subclasses)")) {
|
||||
Menu.ItemView(
|
||||
title: "Placemarks",
|
||||
subtitle: "Making changes using Transactions",
|
||||
destination: {
|
||||
Modern.PlacemarksDemo.MainView()
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Time Zones",
|
||||
subtitle: "Fetching objects and Querying raw values",
|
||||
destination: {
|
||||
Modern.TimeZonesDemo.MainView()
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Colors (UIKit)",
|
||||
subtitle: "Observing list changes and single-object changes using DiffableDataSources",
|
||||
destination: {
|
||||
Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
Modern.ColorsDemo.UIKit.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
Modern.ColorsDemo.UIKit.DetailView(objectPublisher)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Colors (SwiftUI)",
|
||||
subtitle: "Observing list changes and single-object changes using SwiftUI bindings",
|
||||
destination: {
|
||||
Modern.ColorsDemo.MainView(
|
||||
listView: { listPublisher, onPaletteTapped in
|
||||
Modern.ColorsDemo.SwiftUI.ListView(
|
||||
listPublisher: listPublisher,
|
||||
onPaletteTapped: onPaletteTapped
|
||||
)
|
||||
},
|
||||
detailView: { objectPublisher in
|
||||
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Pokedex API",
|
||||
subtitle: "Importing JSON data from external source",
|
||||
destination: {
|
||||
Modern.PokedexDemo.MainView(
|
||||
listView: Modern.PokedexDemo.UIKit.ListView.init
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
Section(header: Text("Classic (NSManagedObject subclasses)")) {
|
||||
Menu.ItemView(
|
||||
title: "Colors",
|
||||
subtitle: "Observing list changes and single-object changes using ListMonitor",
|
||||
destination: {
|
||||
Classic.ColorsDemo.MainView()
|
||||
}
|
||||
)
|
||||
}
|
||||
Section(header: Text("Advanced")) {
|
||||
Menu.ItemView(
|
||||
title: "Accounts",
|
||||
subtitle: "Switching between multiple persistent stores",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
.disabled(true)
|
||||
Menu.ItemView(
|
||||
title: "Evolution",
|
||||
subtitle: "Migrating and reverse-migrating stores",
|
||||
destination: {
|
||||
Advanced.EvolutionDemo.MainView()
|
||||
}
|
||||
)
|
||||
Menu.ItemView(
|
||||
title: "Logger",
|
||||
subtitle: "Implementing a custom logger",
|
||||
destination: { EmptyView() }
|
||||
)
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle("CoreStore Demos")
|
||||
Menu.PlaceholderView()
|
||||
}
|
||||
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Menu_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Menu.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
44
Demo/⭐️Sources/Helpers/Menu/Menu.PlaceholderView.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
extension Menu {
|
||||
|
||||
// MARK: - Menu.PlaceholderView
|
||||
|
||||
struct PlaceholderView: UIViewControllerRepresentable {
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
typealias UIViewControllerType = UIViewController
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
return UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Menu_PlaceholderView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
return Menu.PlaceholderView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
10
Demo/⭐️Sources/Helpers/Menu/Menu.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Menu
|
||||
|
||||
enum Menu {}
|
||||
36
Demo/⭐️Sources/SceneDelegate.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// MARK: - SceneDelegate
|
||||
|
||||
@objc final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
// MARK: UIWindowSceneDelegate
|
||||
|
||||
@objc dynamic var window: UIWindow?
|
||||
|
||||
|
||||
// MARK: UISceneDelegate
|
||||
|
||||
@objc dynamic func scene(
|
||||
_ scene: UIScene,
|
||||
willConnectTo session: UISceneSession,
|
||||
options connectionOptions: UIScene.ConnectionOptions
|
||||
) {
|
||||
|
||||
guard case let scene as UIWindowScene = scene else {
|
||||
|
||||
return
|
||||
}
|
||||
let window = UIWindow(windowScene: scene)
|
||||
window.rootViewController = UIHostingController(
|
||||
rootView: Menu.MainView()
|
||||
)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
10
Demo/⭐️Sources/⭐️Demos/⭐️Advanced/Advanced.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
// MARK: - Advanced
|
||||
|
||||
/**
|
||||
Sample application of complex use cases
|
||||
*/
|
||||
enum Advanced {}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
typealias CreatureType = Advanced_EvolutionDemo_CreatureType
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
protocol Advanced_EvolutionDemo_CreatureType: DynamicObject, CustomStringConvertible {
|
||||
|
||||
var dnaCode: Int64 { get set }
|
||||
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Self
|
||||
|
||||
func mutate(in transaction: BaseDataTransaction)
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import Combine
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.CreaturesDataSource
|
||||
|
||||
/**
|
||||
A type-erasing adapter to support different `ListPublisher` types
|
||||
*/
|
||||
final class CreaturesDataSource: ObservableObject {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init<T: NSManagedObject & Advanced.EvolutionDemo.CreatureType>(
|
||||
listPublisher: ListPublisher<T>,
|
||||
dataStack: DataStack
|
||||
) {
|
||||
|
||||
self.numberOfItems = {
|
||||
listPublisher.snapshot.numberOfItems
|
||||
}
|
||||
self.itemDescriptionAtIndex = { index in
|
||||
listPublisher.snapshot[index].object?.description
|
||||
}
|
||||
self.addItems = { count in
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
let nextDNACode = try transaction.fetchCount(From<T>())
|
||||
for offset in 0 ..< count {
|
||||
|
||||
let object = transaction.create(Into<T>())
|
||||
object.dnaCode = .init(nextDNACode + offset)
|
||||
object.mutate(in: transaction)
|
||||
}
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
self.mutateItemAtIndex = { index in
|
||||
|
||||
let object = listPublisher.snapshot[index]
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
object
|
||||
.asEditable(in: transaction)?
|
||||
.mutate(in: transaction)
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
self.deleteAllItems = {
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
try transaction.deleteAll(From<T>())
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
listPublisher.addObserver(self) { [weak self] (listPublisher) in
|
||||
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
init<T: CoreStoreObject & Advanced.EvolutionDemo.CreatureType>(
|
||||
listPublisher: ListPublisher<T>,
|
||||
dataStack: DataStack
|
||||
) {
|
||||
|
||||
self.numberOfItems = {
|
||||
listPublisher.snapshot.numberOfItems
|
||||
}
|
||||
self.itemDescriptionAtIndex = { index in
|
||||
listPublisher.snapshot[index].object?.description
|
||||
}
|
||||
self.addItems = { count in
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
let nextDNACode = try transaction.fetchCount(From<T>())
|
||||
for offset in 0 ..< count {
|
||||
|
||||
let object = transaction.create(Into<T>())
|
||||
object.dnaCode = .init(nextDNACode + offset)
|
||||
object.mutate(in: transaction)
|
||||
}
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
self.mutateItemAtIndex = { index in
|
||||
|
||||
let object = listPublisher.snapshot[index]
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
object
|
||||
.asEditable(in: transaction)?
|
||||
.mutate(in: transaction)
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
self.deleteAllItems = {
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { transaction in
|
||||
|
||||
try transaction.deleteAll(From<T>())
|
||||
},
|
||||
completion: { _ in }
|
||||
)
|
||||
}
|
||||
listPublisher.addObserver(self) { [weak self] (listPublisher) in
|
||||
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfCreatures() -> Int {
|
||||
|
||||
return self.numberOfItems()
|
||||
}
|
||||
|
||||
func creatureDescription(at index: Int) -> String? {
|
||||
|
||||
return self.itemDescriptionAtIndex(index)
|
||||
}
|
||||
|
||||
func mutate(at index: Int) {
|
||||
|
||||
self.mutateItemAtIndex(index)
|
||||
}
|
||||
|
||||
func add(count: Int) {
|
||||
|
||||
self.addItems(count)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
|
||||
self.deleteAllItems()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let numberOfItems: () -> Int
|
||||
private let itemDescriptionAtIndex: (Int) -> String?
|
||||
private let mutateItemAtIndex: (Int) -> Void
|
||||
private let addItems: (Int) -> Void
|
||||
private let deleteAllItems: () -> Void
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - AdvancedEvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - GeologicalPeriod
|
||||
|
||||
enum GeologicalPeriod: RawRepresentable, CaseIterable, Hashable, CustomStringConvertible {
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
case ageOfInvertebrates
|
||||
case ageOfFishes
|
||||
case ageOfReptiles
|
||||
case ageOfMammals
|
||||
|
||||
var version: ModelVersion {
|
||||
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
var creatureType: Advanced.EvolutionDemo.CreatureType.Type {
|
||||
|
||||
switch self {
|
||||
|
||||
case .ageOfInvertebrates:
|
||||
return Advanced.EvolutionDemo.V1.Creature.self
|
||||
case .ageOfFishes:
|
||||
return Advanced.EvolutionDemo.V2.Creature.self
|
||||
case .ageOfReptiles:
|
||||
return Advanced.EvolutionDemo.V3.Creature.self
|
||||
case .ageOfMammals:
|
||||
return Advanced.EvolutionDemo.V4.Creature.self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
|
||||
switch self {
|
||||
|
||||
case .ageOfInvertebrates:
|
||||
return "Invertebrates"
|
||||
case .ageOfFishes:
|
||||
return "Fishes"
|
||||
case .ageOfReptiles:
|
||||
return "Reptiles"
|
||||
case .ageOfMammals:
|
||||
return "Mammals"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: RawRepresentable
|
||||
|
||||
typealias RawValue = ModelVersion
|
||||
|
||||
var rawValue: ModelVersion {
|
||||
|
||||
switch self {
|
||||
|
||||
case .ageOfInvertebrates:
|
||||
return Advanced.EvolutionDemo.V1.name
|
||||
case .ageOfFishes:
|
||||
return Advanced.EvolutionDemo.V2.name
|
||||
case .ageOfReptiles:
|
||||
return Advanced.EvolutionDemo.V3.name
|
||||
case .ageOfMammals:
|
||||
return Advanced.EvolutionDemo.V4.name
|
||||
}
|
||||
}
|
||||
|
||||
init?(rawValue: ModelVersion) {
|
||||
|
||||
switch rawValue {
|
||||
|
||||
case Advanced.EvolutionDemo.V1.name:
|
||||
self = .ageOfInvertebrates
|
||||
case Advanced.EvolutionDemo.V2.name:
|
||||
self = .ageOfFishes
|
||||
case Advanced.EvolutionDemo.V3.name:
|
||||
self = .ageOfReptiles
|
||||
case Advanced.EvolutionDemo.V4.name:
|
||||
self = .ageOfMammals
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.ItemView
|
||||
|
||||
struct ItemView: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(description: String?, mutate: @escaping () -> Void) {
|
||||
|
||||
self.description = description
|
||||
self.mutate = mutate
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(self.description ?? "")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Button(
|
||||
action: self.mutate,
|
||||
label: {
|
||||
Text("Mutate")
|
||||
.foregroundColor(.accentColor)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.disabled(self.description == nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
fileprivate let description: String?
|
||||
fileprivate let mutate: () -> Void
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Advanced_EvolutionDemo_ItemView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
Advanced.EvolutionDemo.ItemView(
|
||||
description: """
|
||||
dnaCode: 123
|
||||
numberOfLimbs: 4
|
||||
hasVertebrae: true
|
||||
hasHead: true
|
||||
hasTail: true
|
||||
habitat: land
|
||||
hasWings: false
|
||||
""",
|
||||
mutate: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.ListView
|
||||
|
||||
struct ListView: View {
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
let dataSource = self.dataSource
|
||||
return List {
|
||||
ForEach(0 ..< dataSource.numberOfCreatures(), id: \.self) { (index) in
|
||||
Advanced.EvolutionDemo.ItemView(
|
||||
description: dataSource.creatureDescription(at: index),
|
||||
mutate: {
|
||||
|
||||
dataSource.mutate(at: index)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
}
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(
|
||||
period: Advanced.EvolutionDemo.GeologicalPeriod,
|
||||
dataStack: DataStack,
|
||||
dataSource: Advanced.EvolutionDemo.CreaturesDataSource
|
||||
) {
|
||||
|
||||
self.period = period
|
||||
self.dataStack = dataStack
|
||||
self.dataSource = dataSource
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let period: Advanced.EvolutionDemo.GeologicalPeriod
|
||||
|
||||
private let dataStack: DataStack
|
||||
|
||||
@ObservedObject
|
||||
private var dataSource: Advanced.EvolutionDemo.CreaturesDataSource
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Advanced_EvolutionDemo_ListView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
let dataStack = DataStack(
|
||||
CoreStoreSchema(
|
||||
modelVersion: Advanced.EvolutionDemo.V4.name,
|
||||
entities: [
|
||||
Entity<Advanced.EvolutionDemo.V4.Creature>("Creature")
|
||||
]
|
||||
)
|
||||
)
|
||||
try! dataStack.addStorageAndWait(
|
||||
SQLiteStore(fileName: "Advanced.EvolutionDemo.ListView.Preview.sqlite")
|
||||
)
|
||||
try! dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
for dnaCode in 0 ..< 10 as Range<Int64> {
|
||||
|
||||
let object = transaction.create(Into<Advanced.EvolutionDemo.V4.Creature>())
|
||||
object.dnaCode = dnaCode
|
||||
object.mutate(in: transaction)
|
||||
}
|
||||
}
|
||||
)
|
||||
return Advanced.EvolutionDemo.ListView(
|
||||
period: .ageOfMammals,
|
||||
dataStack: dataStack,
|
||||
dataSource: Advanced.EvolutionDemo.V4.Creature.dataSource(in: dataStack)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.MainView
|
||||
|
||||
struct MainView: View {
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
let migrator = self.migrator
|
||||
let listView: AnyView
|
||||
if let current = migrator.current {
|
||||
|
||||
listView = AnyView(
|
||||
Advanced.EvolutionDemo.ListView(
|
||||
period: current.period,
|
||||
dataStack: current.dataStack,
|
||||
dataSource: current.dataSource
|
||||
)
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
||||
listView = AnyView(
|
||||
Advanced.EvolutionDemo.ProgressView(progress: migrator.progress)
|
||||
)
|
||||
}
|
||||
|
||||
return VStack(spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text("Age of")
|
||||
.padding(.trailing)
|
||||
Picker(selection: self.$migrator.currentPeriod, label: EmptyView()) {
|
||||
ForEach(Advanced.EvolutionDemo.GeologicalPeriod.allCases, id: \.self) { period in
|
||||
Text(period.description).tag(period)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
}
|
||||
.padding()
|
||||
listView
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
}
|
||||
.navigationBarTitle("Evolution")
|
||||
.disabled(migrator.isBusy || migrator.current == nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ObservedObject
|
||||
private var migrator: Advanced.EvolutionDemo.Migrator = .init()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Advanced_EvolutionDemo_MainView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
Advanced.EvolutionDemo.MainView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.ProgressView
|
||||
|
||||
struct ProgressView: View {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(progress: Progress?) {
|
||||
|
||||
self.progressObserver = .init(progress)
|
||||
}
|
||||
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
|
||||
guard self.progressObserver.isMigrating else {
|
||||
|
||||
return AnyView(
|
||||
VStack(alignment: .center) {
|
||||
Text("Preparing creatures...")
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
)
|
||||
}
|
||||
return AnyView(
|
||||
VStack(alignment: .leading) {
|
||||
Text("Migrating: \(self.progressObserver.localizedDescription)")
|
||||
.font(.headline)
|
||||
.padding([.top, .horizontal])
|
||||
Text("Progressive step: \(self.progressObserver.localizedAdditionalDescription)")
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal)
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(width: geometry.size.width, height: 8)
|
||||
RoundedRectangle(cornerRadius: 4, style: .continuous)
|
||||
.fill(Color.blue)
|
||||
.frame(
|
||||
width: geometry.size.width
|
||||
* self.progressObserver.fractionCompleted,
|
||||
height: 8
|
||||
)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
@ObservedObject
|
||||
private var progressObserver: ProgressObserver
|
||||
|
||||
|
||||
// MARK: - ProgressObserver
|
||||
|
||||
fileprivate final class ProgressObserver: ObservableObject {
|
||||
|
||||
private(set) var fractionCompleted: CGFloat = 0
|
||||
private(set) var localizedDescription: String = ""
|
||||
private(set) var localizedAdditionalDescription: String = ""
|
||||
|
||||
var isMigrating: Bool {
|
||||
|
||||
return self.progress != nil
|
||||
}
|
||||
|
||||
init(_ progress: Progress?) {
|
||||
|
||||
self.progress = progress
|
||||
|
||||
progress?.setProgressHandler { [weak self] (progess) in
|
||||
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
self.fractionCompleted = CGFloat(progress?.fractionCompleted ?? 0)
|
||||
self.localizedDescription = progress?.localizedDescription ?? ""
|
||||
self.localizedAdditionalDescription = progress?.localizedAdditionalDescription ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let progress: Progress?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Advanced_EvolutionDemo_ProgressView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
let progress = Progress(totalUnitCount: 10)
|
||||
progress.completedUnitCount = 3
|
||||
return Advanced.EvolutionDemo.ProgressView(
|
||||
progress: progress
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V1.Creature
|
||||
|
||||
@objc(Advanced_EvolutionDemo_V1_Creature)
|
||||
final class Advanced_EvolutionDemo_V1_Creature: NSManagedObject, Advanced.EvolutionDemo.CreatureType {
|
||||
|
||||
@NSManaged
|
||||
dynamic var dnaCode: Int64
|
||||
|
||||
@NSManaged
|
||||
dynamic var numberOfFlagella: Int32
|
||||
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
override var description: String {
|
||||
|
||||
return """
|
||||
dnaCode: \(self.dnaCode)
|
||||
numberOfFlagella: \(self.numberOfFlagella)
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
// MARK: Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
|
||||
|
||||
return .init(
|
||||
listPublisher: dataStack.publishList(
|
||||
From<Advanced.EvolutionDemo.V1.Creature>()
|
||||
.orderBy(.descending(\.dnaCode))
|
||||
),
|
||||
dataStack: dataStack
|
||||
)
|
||||
}
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int {
|
||||
|
||||
return try transaction.fetchCount(
|
||||
From<Advanced.EvolutionDemo.V1.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V1.Creature {
|
||||
|
||||
return transaction.create(
|
||||
Into<Advanced.EvolutionDemo.V1.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
func mutate(in transaction: BaseDataTransaction) {
|
||||
|
||||
self.numberOfFlagella = .random(in: 1...200)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V1
|
||||
|
||||
extension Advanced.EvolutionDemo.V1 {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V1.FromV2
|
||||
|
||||
enum FromV2 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static var mapping: XcodeSchemaMappingProvider {
|
||||
|
||||
return XcodeSchemaMappingProvider(
|
||||
from: Advanced.EvolutionDemo.V2.name,
|
||||
to: Advanced.EvolutionDemo.V1.name,
|
||||
mappingModelBundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V1
|
||||
|
||||
/**
|
||||
Namespace for V1 models (`Advanced.EvolutionDemo.GeologicalPeriod.ageOfInvertebrates`)
|
||||
*/
|
||||
enum V1 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static let name: ModelVersion = "Advanced.EvolutionDemo.V1"
|
||||
|
||||
typealias Creature = Advanced_EvolutionDemo_V1_Creature
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,7 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges</key>
|
||||
<true/>
|
||||
<key>SnapshotAutomaticallyBeforeSignificantChanges</key>
|
||||
<true/>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Advanced.EvolutionDemo.V1.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V1_Creature" syncable="YES">
|
||||
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numberOfFlagella" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Creature" positionX="-27" positionY="18" width="128" height="73"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V2_Creature" syncable="YES">
|
||||
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hasHead" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="hasTail" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="hasVertebrae" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="numberOfFlippers" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Creature" positionX="-9" positionY="36" width="128" height="118"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V2.Creature
|
||||
|
||||
@objc(Advanced_EvolutionDemo_V2_Creature)
|
||||
final class Advanced_EvolutionDemo_V2_Creature: NSManagedObject, Advanced.EvolutionDemo.CreatureType {
|
||||
|
||||
@NSManaged
|
||||
dynamic var dnaCode: Int64
|
||||
|
||||
@NSManaged
|
||||
dynamic var numberOfFlippers: Int32
|
||||
|
||||
@NSManaged
|
||||
dynamic var hasVertebrae: Bool
|
||||
|
||||
@NSManaged
|
||||
dynamic var hasHead: Bool
|
||||
|
||||
@NSManaged
|
||||
dynamic var hasTail: Bool
|
||||
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
override var description: String {
|
||||
|
||||
return """
|
||||
dnaCode: \(self.dnaCode)
|
||||
numberOfFlippers: \(self.numberOfFlippers)
|
||||
hasVertebrae: \(self.hasVertebrae)
|
||||
hasHead: \(self.hasHead)
|
||||
hasTail: \(self.hasTail)
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
// MARK: Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
|
||||
|
||||
return .init(
|
||||
listPublisher: dataStack.publishList(
|
||||
From<Advanced.EvolutionDemo.V2.Creature>()
|
||||
.orderBy(.descending(\.dnaCode))
|
||||
),
|
||||
dataStack: dataStack
|
||||
)
|
||||
}
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int {
|
||||
|
||||
return try transaction.fetchCount(
|
||||
From<Advanced.EvolutionDemo.V2.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V2.Creature {
|
||||
|
||||
return transaction.create(
|
||||
Into<Advanced.EvolutionDemo.V2.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
func mutate(in transaction: BaseDataTransaction) {
|
||||
|
||||
self.numberOfFlippers = .random(in: 1...4) * 2
|
||||
self.hasVertebrae = .random()
|
||||
self.hasHead = true
|
||||
self.hasTail = .random()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V2
|
||||
|
||||
extension Advanced.EvolutionDemo.V2 {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V2.FromV1
|
||||
|
||||
enum FromV1 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static var mapping: XcodeSchemaMappingProvider {
|
||||
|
||||
return XcodeSchemaMappingProvider(
|
||||
from: Advanced.EvolutionDemo.V1.name,
|
||||
to: Advanced.EvolutionDemo.V2.name,
|
||||
mappingModelBundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreData
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V2.FromV1MigrationPolicy
|
||||
|
||||
@objc(Advanced_EvolutionDemo_V2_FromV1MigrationPolicy)
|
||||
final class Advanced_EvolutionDemo_V2_FromV1MigrationPolicy: NSEntityMigrationPolicy {
|
||||
|
||||
// MARK: NSEntityMigrationPolicy
|
||||
|
||||
override func createDestinationInstances(
|
||||
forSource sInstance: NSManagedObject,
|
||||
in mapping: NSEntityMapping,
|
||||
manager: NSMigrationManager
|
||||
) throws {
|
||||
|
||||
try super.createDestinationInstances(
|
||||
forSource: sInstance,
|
||||
in: mapping,
|
||||
manager: manager
|
||||
)
|
||||
|
||||
for dInstance in manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]) {
|
||||
|
||||
dInstance.setValue(
|
||||
Bool.random(),
|
||||
forKey: #keyPath(Advanced.EvolutionDemo.V2.Creature.hasVertebrae)
|
||||
)
|
||||
dInstance.setValue(
|
||||
Bool.random(),
|
||||
forKey: #keyPath(Advanced.EvolutionDemo.V2.Creature.hasTail)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V2
|
||||
|
||||
extension Advanced.EvolutionDemo.V2 {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V2.FromV3
|
||||
|
||||
enum FromV3 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static var mapping: CustomSchemaMappingProvider {
|
||||
|
||||
return CustomSchemaMappingProvider(
|
||||
from: Advanced.EvolutionDemo.V3.name,
|
||||
to: Advanced.EvolutionDemo.V2.name,
|
||||
entityMappings: [
|
||||
.transformEntity(
|
||||
sourceEntity: "Creature",
|
||||
destinationEntity: "Creature",
|
||||
transformer: { (source, createDestination) in
|
||||
|
||||
let destination = createDestination()
|
||||
destination["dnaCode"] = source["dnaCode"]
|
||||
destination["numberOfFlippers"] = source["numberOfLimbs"]
|
||||
destination["hasVertebrae"] = source["hasVertebrae"]
|
||||
destination["hasHead"] = source["hasHead"]
|
||||
destination["hasTail"] = source["hasTail"]
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V2
|
||||
|
||||
/**
|
||||
Namespace for V2 models (`Advanced.EvolutionDemo.GeologicalPeriod.ageOfFishes`)
|
||||
*/
|
||||
enum V2 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static let name: ModelVersion = "Advanced.EvolutionDemo.V2"
|
||||
|
||||
typealias Creature = Advanced_EvolutionDemo_V2_Creature
|
||||
|
||||
typealias FromV1MigrationPolicy = Advanced_EvolutionDemo_V2_FromV1MigrationPolicy
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V2_Creature" syncable="YES">
|
||||
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hasHead" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="hasTail" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="hasVertebrae" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="numberOfFlippers" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Creature" positionX="-45" positionY="0" width="128" height="118"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import CoreStore
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V3
|
||||
|
||||
extension Advanced.EvolutionDemo.V3 {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V3.Creature
|
||||
|
||||
final class Creature: CoreStoreObject, Advanced.EvolutionDemo.CreatureType {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@Field.Stored("dnaCode")
|
||||
var dnaCode: Int64 = 0
|
||||
|
||||
@Field.Stored("numberOfLimbs")
|
||||
var numberOfLimbs: Int32 = 0
|
||||
|
||||
@Field.Stored("hasVertebrae")
|
||||
var hasVertebrae: Bool = false
|
||||
|
||||
@Field.Stored("hasHead")
|
||||
var hasHead: Bool = true
|
||||
|
||||
@Field.Stored("hasTail")
|
||||
var hasTail: Bool = true
|
||||
|
||||
@Field.Stored("hasWings")
|
||||
var hasWings: Bool = false
|
||||
|
||||
@Field.Stored("habitat")
|
||||
var habitat: Habitat = .water
|
||||
|
||||
|
||||
// MARK: - Habitat
|
||||
|
||||
enum Habitat: String, CaseIterable, ImportableAttributeType, FieldStorableType {
|
||||
|
||||
case water = "water"
|
||||
case land = "land"
|
||||
case amphibian = "amphibian"
|
||||
}
|
||||
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
|
||||
return """
|
||||
dnaCode: \(self.dnaCode)
|
||||
numberOfLimbs: \(self.numberOfLimbs)
|
||||
hasVertebrae: \(self.hasVertebrae)
|
||||
hasHead: \(self.hasHead)
|
||||
hasTail: \(self.hasTail)
|
||||
habitat: \(self.habitat)
|
||||
hasWings: \(self.hasWings)
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
// MARK: Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
|
||||
|
||||
return .init(
|
||||
listPublisher: dataStack.publishList(
|
||||
From<Advanced.EvolutionDemo.V3.Creature>()
|
||||
.orderBy(.descending(\.$dnaCode))
|
||||
),
|
||||
dataStack: dataStack
|
||||
)
|
||||
}
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int {
|
||||
|
||||
return try transaction.fetchCount(
|
||||
From<Advanced.EvolutionDemo.V3.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V3.Creature {
|
||||
|
||||
return transaction.create(
|
||||
Into<Advanced.EvolutionDemo.V3.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
func mutate(in transaction: BaseDataTransaction) {
|
||||
|
||||
self.numberOfLimbs = .random(in: 1...4) * 2
|
||||
self.hasVertebrae = .random()
|
||||
self.hasHead = true
|
||||
self.hasTail = .random()
|
||||
self.habitat = Habitat.allCases.randomElement()!
|
||||
self.hasWings = .random()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V3
|
||||
|
||||
extension Advanced.EvolutionDemo.V3 {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V3.FromV2
|
||||
|
||||
enum FromV2 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static var mapping: CustomSchemaMappingProvider {
|
||||
|
||||
return CustomSchemaMappingProvider(
|
||||
from: Advanced.EvolutionDemo.V2.name,
|
||||
to: Advanced.EvolutionDemo.V3.name,
|
||||
entityMappings: [
|
||||
.transformEntity(
|
||||
sourceEntity: "Creature",
|
||||
destinationEntity: "Creature",
|
||||
transformer: { (source, createDestination) in
|
||||
|
||||
let destination = createDestination()
|
||||
destination["dnaCode"] = source["dnaCode"]
|
||||
destination["numberOfLimbs"] = source["numberOfFlippers"]
|
||||
destination["hasVertebrae"] = source["hasVertebrae"]
|
||||
destination["hasHead"] = source["hasHead"]
|
||||
destination["hasTail"] = source["hasTail"]
|
||||
destination["hasWings"] = Bool.random()
|
||||
destination["habitat"] = Advanced.EvolutionDemo.V3.Creature.Habitat.allCases.randomElement()!.rawValue
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V3
|
||||
|
||||
extension Advanced.EvolutionDemo.V3 {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V3.FromV4
|
||||
|
||||
enum FromV4 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static var mapping: CustomSchemaMappingProvider {
|
||||
|
||||
return CustomSchemaMappingProvider(
|
||||
from: Advanced.EvolutionDemo.V4.name,
|
||||
to: Advanced.EvolutionDemo.V3.name,
|
||||
entityMappings: [
|
||||
.transformEntity(
|
||||
sourceEntity: "Creature",
|
||||
destinationEntity: "Creature",
|
||||
transformer: CustomSchemaMappingProvider.CustomMapping.inferredTransformation(_:_:)
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V3
|
||||
|
||||
/**
|
||||
Namespace for V3 models (`Advanced.EvolutionDemo.GeologicalPeriod.ageOfReptiles`)
|
||||
*/
|
||||
enum V3 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static let name: ModelVersion = "Advanced.EvolutionDemo.V3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import CoreStore
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V4
|
||||
|
||||
extension Advanced.EvolutionDemo.V4 {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V4.Creature
|
||||
|
||||
final class Creature: CoreStoreObject, Advanced.EvolutionDemo.CreatureType {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
@Field.Stored("dnaCode")
|
||||
var dnaCode: Int64 = 0
|
||||
|
||||
@Field.Stored("numberOfLimbs")
|
||||
var numberOfLimbs: Int32 = 0
|
||||
|
||||
@Field.Stored("hasVertebrae")
|
||||
var hasVertebrae: Bool = false
|
||||
|
||||
@Field.Stored("hasHead")
|
||||
var hasHead: Bool = true
|
||||
|
||||
@Field.Stored("hasTail")
|
||||
var hasTail: Bool = false
|
||||
|
||||
@Field.Stored("hasWings")
|
||||
var hasWings: Bool = false
|
||||
|
||||
|
||||
typealias Habitat = Advanced.EvolutionDemo.V3.Creature.Habitat
|
||||
|
||||
@Field.Stored("habitat")
|
||||
var habitat: Habitat = .water
|
||||
|
||||
@Field.Stored("isWarmBlooded")
|
||||
var isWarmBlooded: Bool = true
|
||||
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
|
||||
return """
|
||||
dnaCode: \(self.dnaCode)
|
||||
numberOfLimbs: \(self.numberOfLimbs)
|
||||
hasVertebrae: \(self.hasVertebrae)
|
||||
hasHead: \(self.hasHead)
|
||||
hasTail: \(self.hasTail)
|
||||
habitat: \(self.habitat)
|
||||
hasWings: \(self.hasWings)
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
// MARK: Advanced.EvolutionDemo.CreatureType
|
||||
|
||||
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
|
||||
|
||||
return .init(
|
||||
listPublisher: dataStack.publishList(
|
||||
From<Advanced.EvolutionDemo.V4.Creature>()
|
||||
.orderBy(.descending(\.$dnaCode))
|
||||
),
|
||||
dataStack: dataStack
|
||||
)
|
||||
}
|
||||
|
||||
static func count(in transaction: BaseDataTransaction) throws -> Int {
|
||||
|
||||
return try transaction.fetchCount(
|
||||
From<Advanced.EvolutionDemo.V4.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V4.Creature {
|
||||
|
||||
return transaction.create(
|
||||
Into<Advanced.EvolutionDemo.V4.Creature>()
|
||||
)
|
||||
}
|
||||
|
||||
func mutate(in transaction: BaseDataTransaction) {
|
||||
|
||||
self.numberOfLimbs = .random(in: 1...4) * 2
|
||||
self.hasVertebrae = .random()
|
||||
self.hasHead = true
|
||||
self.hasTail = .random()
|
||||
self.habitat = Habitat.allCases.randomElement()!
|
||||
self.hasWings = .random()
|
||||
self.isWarmBlooded = .random()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V4
|
||||
|
||||
extension Advanced.EvolutionDemo.V4 {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V4.FromV3
|
||||
|
||||
enum FromV3 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static var mapping: CustomSchemaMappingProvider {
|
||||
|
||||
return CustomSchemaMappingProvider(
|
||||
from: Advanced.EvolutionDemo.V3.name,
|
||||
to: Advanced.EvolutionDemo.V4.name,
|
||||
entityMappings: [
|
||||
.transformEntity(
|
||||
sourceEntity: "Creature",
|
||||
destinationEntity: "Creature",
|
||||
transformer: { (source, createDestination) in
|
||||
|
||||
let destination = createDestination()
|
||||
destination.enumerateAttributes { (destinationAttribute, sourceAttribute) in
|
||||
|
||||
if let sourceAttribute = sourceAttribute {
|
||||
|
||||
destination[destinationAttribute] = source[sourceAttribute]
|
||||
}
|
||||
}
|
||||
destination["isWarmBlooded"] = Bool.random()
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.V4
|
||||
|
||||
/**
|
||||
Namespace for V3 models (`Advanced.EvolutionDemo.GeologicalPeriod.ageOfMammals`)
|
||||
*/
|
||||
enum V4 {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
static let name: ModelVersion = "Advanced.EvolutionDemo.V4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
// MARK: - Advanced
|
||||
|
||||
extension Advanced {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
/**
|
||||
Sample execution of progressive migrations. This example demonstrates the following concepts:
|
||||
|
||||
- How to inspect the current model version of the store (if it exists)
|
||||
- How to do two-way migration chains (upgrades + downgrades)
|
||||
- How to support multiple versions of the model on the same app
|
||||
- How to migrate between `NSManagedObject` schema (`xcdatamodel` files) and `CoreStoreObject` schema.
|
||||
- How to use `XcodeSchemaMappingProvider`s for `NSManagedObject` stores, and `CustomSchemaMappingProvider`s for `CoreStoreObject` stores
|
||||
- How to manage migration models using namespacing technique
|
||||
|
||||
Note that ideally, your app should be supporting just the latest version of the model, and provide one-way progressive migrations from all the earlier versions.
|
||||
*/
|
||||
enum EvolutionDemo {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V1_Creature" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numberOfFlagella" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Creature" positionX="-36" positionY="9" width="128" height="73"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19F101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Creature" representedClassName="Advanced_EvolutionDemo_V1_Creature" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dnaCode" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numberOfFlagella" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Creature" positionX="-36" positionY="9" width="128" height="73"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,260 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo
|
||||
|
||||
extension Advanced.EvolutionDemo {
|
||||
|
||||
// MARK: - Advanced.EvolutionDemo.Migrator
|
||||
|
||||
final class Migrator: ObservableObject {
|
||||
|
||||
/**
|
||||
⭐️ Sample 1: Creating a complex `DataStack` that contains all schema histories. The `exactCurrentModelVersion` will specify the target version (if required), and `migrationChain` will provide the upgrade/downgrade progressive migration path.
|
||||
*/
|
||||
private func createDataStack(
|
||||
exactCurrentModelVersion: ModelVersion?,
|
||||
migrationChain: MigrationChain
|
||||
) -> DataStack {
|
||||
|
||||
let xcodeV1ToV2ModelSchema = XcodeDataModelSchema.from(
|
||||
modelName: "Advanced.EvolutionDemo.V1",
|
||||
bundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self)
|
||||
)
|
||||
return DataStack(
|
||||
schemaHistory: SchemaHistory(
|
||||
allSchema: xcodeV1ToV2ModelSchema.allSchema
|
||||
+ [
|
||||
CoreStoreSchema(
|
||||
modelVersion: Advanced.EvolutionDemo.V3.name,
|
||||
entities: [
|
||||
Entity<Advanced.EvolutionDemo.V3.Creature>("Creature")
|
||||
]
|
||||
),
|
||||
CoreStoreSchema(
|
||||
modelVersion: Advanced.EvolutionDemo.V4.name,
|
||||
entities: [
|
||||
Entity<Advanced.EvolutionDemo.V4.Creature>("Creature")
|
||||
]
|
||||
)
|
||||
],
|
||||
migrationChain: migrationChain,
|
||||
exactCurrentModelVersion: exactCurrentModelVersion
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
⭐️ Sample 2: Creating a complex `SQLiteStore` that contains all schema mappings for both upgrade and downgrade cases.
|
||||
*/
|
||||
private func accessSQLiteStore() -> SQLiteStore {
|
||||
|
||||
let upgradeMappings: [SchemaMappingProvider] = [
|
||||
Advanced.EvolutionDemo.V2.FromV1.mapping,
|
||||
Advanced.EvolutionDemo.V3.FromV2.mapping,
|
||||
Advanced.EvolutionDemo.V4.FromV3.mapping
|
||||
]
|
||||
let downgradeMappings: [SchemaMappingProvider] = [
|
||||
Advanced.EvolutionDemo.V3.FromV4.mapping,
|
||||
Advanced.EvolutionDemo.V2.FromV3.mapping,
|
||||
Advanced.EvolutionDemo.V1.FromV2.mapping,
|
||||
]
|
||||
return SQLiteStore(
|
||||
fileName: "Advanced.EvolutionDemo.sqlite",
|
||||
configuration: nil,
|
||||
migrationMappingProviders: upgradeMappings + downgradeMappings,
|
||||
localStorageOptions: []
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
⭐️ Sample 3: Find the model version used by an existing `SQLiteStore`, or just return the latest version if the store is not created yet.
|
||||
*/
|
||||
private func findCurrentVersion() -> ModelVersion {
|
||||
|
||||
let allVersions = Advanced.EvolutionDemo.GeologicalPeriod.allCases
|
||||
.map({ $0.version })
|
||||
|
||||
// Since we are only interested in finding current version, we'll assume an upgrading `MigrationChain`
|
||||
let dataStack = self.createDataStack(
|
||||
exactCurrentModelVersion: nil,
|
||||
migrationChain: MigrationChain(allVersions)
|
||||
)
|
||||
let migrations = try! dataStack.requiredMigrationsForStorage(
|
||||
self.accessSQLiteStore()
|
||||
)
|
||||
|
||||
// If no migrations are needed, it means either the store is not created yet, or the store is already at the latest model version. In either case, we already know that the store will use the latest version
|
||||
return migrations.first?.sourceVersion
|
||||
?? allVersions.last!
|
||||
}
|
||||
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
var currentPeriod: Advanced.EvolutionDemo.GeologicalPeriod = Advanced.EvolutionDemo.GeologicalPeriod.allCases.last! {
|
||||
|
||||
didSet {
|
||||
|
||||
self.selectModelVersion(self.currentPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var current: (
|
||||
period: Advanced.EvolutionDemo.GeologicalPeriod,
|
||||
dataStack: DataStack,
|
||||
dataSource: Advanced.EvolutionDemo.CreaturesDataSource
|
||||
)? {
|
||||
|
||||
willSet {
|
||||
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var isBusy: Bool = false
|
||||
|
||||
private(set) var progress: Progress?
|
||||
|
||||
|
||||
init() {
|
||||
|
||||
self.synchronizeCurrentVersion()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private func synchronizeCurrentVersion() {
|
||||
|
||||
guard
|
||||
let currentPeriod = Advanced.EvolutionDemo.GeologicalPeriod(rawValue: self.findCurrentVersion())
|
||||
else {
|
||||
|
||||
self.selectModelVersion(self.currentPeriod)
|
||||
return
|
||||
}
|
||||
self.selectModelVersion(currentPeriod)
|
||||
}
|
||||
|
||||
private func selectModelVersion(_ period: Advanced.EvolutionDemo.GeologicalPeriod) {
|
||||
|
||||
let currentPeriod = self.current?.period
|
||||
guard period != currentPeriod else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.objectWillChange.send()
|
||||
|
||||
self.isBusy = true
|
||||
|
||||
// explicitly trigger `NSPersistentStore` cleanup by deallocating the `DataStack`
|
||||
self.current = nil
|
||||
|
||||
let migrationChain: MigrationChain
|
||||
switch (currentPeriod?.version, period.version) {
|
||||
|
||||
case (nil, let newVersion):
|
||||
migrationChain = [newVersion]
|
||||
|
||||
case (let currentVersion?, let newVersion):
|
||||
let upgradeMigrationChain = Advanced.EvolutionDemo.GeologicalPeriod.allCases
|
||||
.map({ $0.version })
|
||||
let currentVersionIndex = upgradeMigrationChain.firstIndex(of: currentVersion)!
|
||||
let newVersionIndex = upgradeMigrationChain.firstIndex(of: newVersion)!
|
||||
|
||||
migrationChain = MigrationChain(
|
||||
currentVersionIndex > newVersionIndex
|
||||
? upgradeMigrationChain.reversed()
|
||||
: upgradeMigrationChain
|
||||
)
|
||||
}
|
||||
let dataStack = self.createDataStack(
|
||||
exactCurrentModelVersion: period.version,
|
||||
migrationChain: migrationChain
|
||||
)
|
||||
|
||||
let completion = { [weak self] () -> Void in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
defer {
|
||||
|
||||
self.isBusy = false
|
||||
}
|
||||
self.current = (
|
||||
period: period,
|
||||
dataStack: dataStack,
|
||||
dataSource: period.creatureType.dataSource(in: dataStack)
|
||||
)
|
||||
self.currentPeriod = period
|
||||
}
|
||||
|
||||
self.progress = dataStack.addStorage(
|
||||
self.accessSQLiteStore(),
|
||||
completion: { [weak self] result in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
guard case .success = result else {
|
||||
|
||||
self.objectWillChange.send()
|
||||
self.isBusy = false
|
||||
return
|
||||
}
|
||||
if self.progress == nil {
|
||||
|
||||
self.spawnCreatures(in: dataStack, period: period, completion: completion)
|
||||
}
|
||||
else {
|
||||
|
||||
completion()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func spawnCreatures(
|
||||
in dataStack: DataStack,
|
||||
period: Advanced.EvolutionDemo.GeologicalPeriod,
|
||||
completion: @escaping () -> Void
|
||||
) {
|
||||
|
||||
dataStack.perform(
|
||||
asynchronous: { (transaction) in
|
||||
|
||||
let creatureType = period.creatureType
|
||||
for dnaCode in try creatureType.count(in: transaction) ..< 10000 {
|
||||
|
||||
let object = creatureType.create(in: transaction)
|
||||
object.dnaCode = Int64(dnaCode)
|
||||
object.mutate(in: transaction)
|
||||
}
|
||||
},
|
||||
completion: { _ in completion() }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - VersionMetadata
|
||||
|
||||
private struct VersionMetadata {
|
||||
|
||||
let label: String
|
||||
let entityType: Advanced.EvolutionDemo.CreatureType.Type
|
||||
let schemaHistory: SchemaHistory
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Demo/⭐️Sources/⭐️Demos/⭐️Classic/Classic.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
// MARK: - Classic
|
||||
|
||||
/**
|
||||
Sample usages for `NSManagedObject` subclasses
|
||||
*/
|
||||
enum Classic {}
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import CoreStore
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Classic.ColorsDemo
|
||||
|
||||
extension Classic.ColorsDemo {
|
||||
|
||||
// MARK: - Classic.ColorsDemo.DetailView
|
||||
|
||||
struct DetailView: UIViewControllerRepresentable {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
init(_ palette: ObjectMonitor<Classic.ColorsDemo.Palette>) {
|
||||
|
||||
self.palette = palette
|
||||
}
|
||||
|
||||
// MARK: UIViewControllerRepresentable
|
||||
|
||||
typealias UIViewControllerType = Classic.ColorsDemo.DetailViewController
|
||||
|
||||
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
|
||||
|
||||
return UIViewControllerType(self.palette)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
|
||||
|
||||
uiViewController.palette = self.palette
|
||||
}
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
|
||||
|
||||
func makeCoordinator() -> ObjectMonitor<Classic.ColorsDemo.Palette> {
|
||||
|
||||
return self.palette
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let palette: ObjectMonitor<Classic.ColorsDemo.Palette>
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct _Demo_Classic_ColorsDemo_DetailView_Preview: PreviewProvider {
|
||||
|
||||
// MARK: PreviewProvider
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
try! Classic.ColorsDemo.dataStack.perform(
|
||||
synchronous: { transaction in
|
||||
|
||||
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
|
||||
return
|
||||
}
|
||||
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
|
||||
palette.setRandomHue()
|
||||
}
|
||||
)
|
||||
|
||||
return Classic.ColorsDemo.DetailView(
|
||||
Classic.ColorsDemo.dataStack.monitorObject(
|
||||
Classic.ColorsDemo.palettesMonitor[0, 0]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Demo
|
||||
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
|
||||
|
||||
import CoreStore
|
||||
|
||||
|
||||
// MARK: - Classic.ColorsDemo
|
||||
|
||||
extension Classic.ColorsDemo {
|
||||
|
||||
// MARK: - Classic.ColorsDemo.Filter
|
||||
|
||||
enum Filter: String, CaseIterable {
|
||||
|
||||
case all = "All Colors"
|
||||
case light = "Light Colors"
|
||||
case dark = "Dark Colors"
|
||||
|
||||
func next() -> Filter {
|
||||
|
||||
let allCases = Self.allCases
|
||||
return allCases[(allCases.firstIndex(of: self)! + 1) % allCases.count]
|
||||
}
|
||||
|
||||
func whereClause() -> Where<Classic.ColorsDemo.Palette> {
|
||||
|
||||
switch self {
|
||||
|
||||
case .all: return .init()
|
||||
case .light: return (\.brightness >= 0.6)
|
||||
case .dark: return (\.brightness <= 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||