Compare commits

...

69 Commits
7.1.0 ... 8.0.0

Author SHA1 Message Date
John Estropia
b20be10a19 update Readme banner image 2021-04-11 11:39:43 +09:00
John Estropia
0ee8fbabd5 version bump, update resources 2021-04-11 11:26:02 +09:00
John Estropia
1f562b25a7 Updated README 2021-04-11 11:03:17 +09:00
John Estropia
593c0510d3 Allow placeholder Views in ObjectReader when an object becomes nil 2021-03-13 11:25:27 +09:00
John Estropia
338e4ddc9f Fix sig abort error 2021-03-07 17:02:09 +09:00
John Estropia
bfb1df3c40 added Publishers for addStorage functions 2021-03-07 16:08:19 +09:00
John Estropia
003bf897e2 Cleanup unnecessary #available branches 2021-03-07 12:01:08 +09:00
John Estropia
0b127956d3 Removed ObservableObject implementations of ListPublisher/ObjectPublisher in favor of LiveList/LiveObject and ".reactive" publishers 2021-03-07 10:38:53 +09:00
John Estropia
098e560fcc Merge branch 'develop' of github.com:JohnEstropia/CoreStore into develop 2021-03-07 10:17:34 +09:00
John Estropia
7dbd3777ec added sugar syntax for ForEach that removes requirement for the "id:" argument 2021-03-07 10:17:20 +09:00
John Estropia
447d8e5880 fix compile error 2021-03-03 16:59:20 +09:00
John Estropia
1f97225efa SwiftUI and Combine utilities appledocs. Bump up to 8.0 and iOS 11.0/macOS 10.13 2021-03-01 09:14:41 +09:00
John Estropia
d13b0cfabb update unit tests 2021-02-21 10:59:55 +09:00
John Estropia
d7b852fca4 SwiftUI utilities done (for now) 2021-02-21 10:56:27 +09:00
John Estropia
f2efe175e5 deprecate misleading API for sectionIndexTransformers 2021-02-21 10:17:58 +09:00
John Estropia
8f3a6638d8 Delete long-deprecated CoreStore namespace 2021-02-21 10:16:08 +09:00
John Estropia
f7471f56a4 Prototyping SwiftUI utilities 2021-02-16 09:12:36 +09:00
John Estropia
edd8ba55d8 fix rare duplication of uniqueID values during import when an import candidate is added by a previous insertion block 2021-01-23 16:51:45 +09:00
John Estropia
18aac84335 fix compiler error 2021-01-02 10:36:33 +09:00
John Estropia
c6be892cb0 added .where(combinedByAnd:) and .where(combinedByOr:) to help compiler with long && and || chains 2021-01-02 10:00:23 +09:00
John Estropia
2cd8101987 Implement ObjectRepresentation on ObjectMonitor, ObjectPublisher, and ObjectSnapshot for future APIs 2021-01-02 09:59:15 +09:00
John Estropia
e1aed37da0 avoid using default queue qos 2020-12-26 23:45:11 +09:00
John Estropia
5de5ecee06 give opportunity for faster equating of ObjectSnapshot 2020-12-26 23:44:35 +09:00
John Estropia
9406901b28 WIP: SwiftUI utils 2020-12-26 19:21:46 +09:00
John Estropia
477f478d85 Add missing Where operators (fixes #410) 2020-12-26 14:02:20 +09:00
John Estropia
668b5ad606 Update playground samples to use Field properties instead of old syntax 2020-12-07 14:08:05 +09:00
John Estropia
f985828f3b cleanup 2020-11-17 18:14:23 +09:00
John Estropia
bb3bc940c2 Remove the dataStack SwiftUI environment key since it's prone to initialization issues 2020-11-11 16:56:20 +09:00
John Estropia
2d5bc77219 Merge branch 'master' into minIOS11
# Conflicts:
#	Sources/ListSnapshot.swift
2020-11-11 13:44:58 +09:00
John Estropia
a40df37192 version bump 2020-11-11 13:29:02 +09:00
John Estropia
63b3d25d78 clear warnings 2020-11-11 13:18:11 +09:00
John Estropia
74721b5c12 allow ObjectSnapshot and ObjectPublisher as parameter to Where clauses 2020-10-10 16:55:35 +09:00
John Estropia
f136549b46 prevent creation of ObjectSnapshot if object is already deleted 2020-10-09 20:14:04 +09:00
John Estropia
4ec2b2e311 Optimize ListSnapshot collection implementation 2020-10-05 23:12:17 +09:00
John Estropia
3d82ee18c7 reinclude LegacyDemo in workspace 2020-10-03 14:36:57 +09:00
John Estropia
d6eae8c091 fix podspec 2020-09-19 15:07:27 +09:00
John Estropia
6f8d86cd8f remove xcuserdata 2020-09-19 14:59:14 +09:00
John Estropia
3b6f226389 version bump 2020-09-19 14:44:55 +09:00
John Estropia
228e4e8e1a rename CoreStoreDemo to LegacyDemo 2020-09-19 14:32:43 +09:00
John Estropia
98a42feb95 demo cleanup 2020-09-19 12:38:09 +09:00
John Estropia
2f93ee7591 Migrations demo done 2020-09-17 23:21:41 +09:00
John Estropia
11f5cb9a05 WIP: demo 2020-09-14 09:46:47 +09:00
John Estropia
2c00fc31bc WIP: migrations demo 2020-09-13 14:17:06 +09:00
John Estropia
2bbf6b34ea WIP: migrations demo 2020-09-08 09:09:36 +09:00
John Estropia
8d7f282743 added demo for classic ListMonitor 2020-08-30 20:16:01 +09:00
John Estropia
007da014f8 cleanup 2020-08-30 10:24:10 +09:00
John Estropia
b26e50f777 add version lock to demo 2020-08-29 23:06:12 +09:00
John Estropia
611bc53c9a cleanup 2020-08-29 23:05:07 +09:00
John Estropia
9d36582c10 minor fix 2020-08-29 20:06:00 +09:00
John Estropia
1c735a9228 improve Pokedex demo 2020-08-29 20:02:05 +09:00
John Estropia
1db91fcec3 make demo compilable on Xcode 11 2020-08-27 09:50:12 +09:00
John Estropia
8b3b947406 pokedex demo 2020-08-20 00:39:03 +09:00
John Estropia
2c0cadf2fa WIP 2020-08-19 18:49:08 +09:00
John Estropia
5e536556da fix for SR-13069 2020-08-19 11:01:12 +09:00
John Estropia
d75029f54b WIP 2020-08-19 08:32:18 +09:00
John Estropia
204c4de1f6 use randomElement in unit test 2020-08-18 17:50:00 +09:00
John Estropia
0f3455a4a4 WIP 2020-08-18 12:05:20 +09:00
John Estropia
72f36e7237 WIP 2020-08-17 17:09:41 +09:00
John Estropia
d988daa025 WIP: new demo app 2020-08-17 09:06:25 +09:00
John Estropia
e720504855 Update README.md 2020-06-20 17:37:12 +09:00
John Estropia
ee51c5ad9c fix readme indentations 2020-06-20 16:47:52 +09:00
John Estropia
4ac7df7364 version bump 2020-06-20 13:16:14 +09:00
John Estropia
56f873ccb3 fix readme table of contents 2020-06-20 13:12:11 +09:00
John Estropia
1f90f846f3 readme update 2020-06-20 13:07:10 +09:00
John Estropia
4a97d5a8dc remove persistentstores during DataStack deinitialization to prevent warnings in Unit tests during teardown 2020-05-26 09:43:39 +09:00
John Estropia
0eb9b6393d add unit testst for Field dynamic initializers 2020-05-24 10:54:16 +09:00
John Estropia
56d0ea46ea Implement dynamic initializers for Field properties (fixes #382) 2020-05-23 12:07:16 +09:00
John Estropia
a7568eebdb fix comments 2020-04-15 16:49:45 +09:00
John Estropia
4049e1944a trigger lazy initialization of ObjectPublisher observation after addObserver() (fixes #383) 2020-04-15 16:48:42 +09:00
287 changed files with 14765 additions and 3414 deletions

2
.gitignore vendored
View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "CoreStore"
s.version = "7.1.0"
s.swift_version = "5.2"
s.version = "8.0.0"
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"

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"
}
]
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}

Binary file not shown.

View File

@@ -62,6 +62,9 @@ class BaseTestCase: XCTestCase {
XCTFail(error.coreStoreDumpString)
}
self.addTeardownBlock {
stack.unsafeRemoveAllPersistentStoresAndWait()
}
}
@nonobjc

View File

@@ -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);
}

View File

@@ -29,7 +29,6 @@ import CoreStore
// MARK: - ConvenienceTests
@available(macOS 10.12, *)
class ConvenienceTests: BaseTestCase {
@objc
@@ -64,7 +63,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>(),

View File

@@ -49,9 +49,16 @@ class Animal: CoreStoreObject {
}
class Dog: Animal {
static let commonNicknames = ["Spot", "Benjie", "Max", "Milo"]
@Field.Stored("nickname")
var nickname: String?
@Field.Stored(
"nickname",
dynamicInitialValue: {
commonNicknames.randomElement()!
}
)
var nickname: String
@Field.Stored("age")
var age: Int = 1
@@ -67,7 +74,7 @@ struct CustomType {
var string = "customString"
}
enum Job: String {
enum Job: String, CaseIterable {
case unemployed
case engineer
@@ -139,9 +146,12 @@ class Person: CoreStoreObject {
coder: (
encode: { $0.toData() },
decode: { $0.flatMap(Job.init(data:)) ?? .unemployed }
)
),
dynamicInitialValue: {
Job.allCases.randomElement()!
}
)
var job: Job = .unemployed
var job: Job
@Field.Relationship("spouse")
var spouse: Person?
@@ -152,7 +162,7 @@ class Person: CoreStoreObject {
@Field.Relationship("_spouseInverse", inverse: \.$spouse)
private var spouseInverse: Person?
static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {
private static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {
if let value = field.primitiveValue {
@@ -165,7 +175,7 @@ class Person: CoreStoreObject {
return value
}
static func keyPathsAffectingDisplayName() -> Set<String> {
private static func keyPathsAffectingDisplayName() -> Set<String> {
return [
String(keyPath: \Person.$title),
@@ -192,7 +202,7 @@ class DynamicModelTests: BaseTestDataTestCase {
],
versionLock: [
"Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
"Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7],
"Dog": [0xad6de93adc5565d, 0x7897e51253eba5a3, 0xd12b9ce0b13600f3, 0x5a4827cd794cd15e],
"Person": [0xf3e6ba6016bbedc6, 0x50dedf64f0eba490, 0xa32088a0ee83468d, 0xb72d1d0b37bd0992]
]
)
@@ -247,8 +257,8 @@ class DynamicModelTests: BaseTestDataTestCase {
let dog = transaction.create(Into<Dog>())
XCTAssertEqual(dog.species, "Swift")
XCTAssertEqual(dog.nickname, nil)
XCTAssertEqual(dog.age, 1)
XCTAssertTrue(Dog.commonNicknames.contains(dog.nickname))
for property in Dog.metaProperties(includeSuperclasses: true) {
@@ -264,7 +274,7 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTAssertTrue(property is FieldContainer<Animal>.Coded<Color?>)
case String(keyPath: \Dog.$nickname):
XCTAssertTrue(property is FieldContainer<Dog>.Stored<String?>)
XCTAssertTrue(property is FieldContainer<Dog>.Stored<String>)
case String(keyPath: \Dog.$age):
XCTAssertTrue(property is FieldContainer<Dog>.Stored<Int>)
@@ -280,19 +290,6 @@ class DynamicModelTests: BaseTestDataTestCase {
}
}
// #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.observe(\.$species, options: [.new, .old]) { (object, change) in
XCTAssertEqual(object, dog)
@@ -335,7 +332,8 @@ class DynamicModelTests: BaseTestDataTestCase {
let person = transaction.create(Into<Person>())
XCTAssertTrue(person.pets.isEmpty)
XCTAssertEqual(person.customField.string, "customString")
XCTAssertEqual(person.job, .unemployed)
let initialJob = person.job
XCTAssertTrue(Job.allCases.contains(initialJob))
XCTAssertEqual(
person.rawObject!
@@ -385,7 +383,7 @@ class DynamicModelTests: BaseTestDataTestCase {
personSnapshot3.$name = "James"
XCTAssertEqual(personSnapshot1.$name, "John")
XCTAssertEqual(personSnapshot1.$displayName, "Mr. John")
XCTAssertEqual(personSnapshot1.$job, .unemployed)
XCTAssertEqual(personSnapshot1.$job, initialJob)
XCTAssertEqual(personSnapshot2.$name, "John")
XCTAssertEqual(personSnapshot2.$displayName, "Sir John")
XCTAssertEqual(personSnapshot2.$job, .engineer)
@@ -494,7 +492,12 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTFail()
}
)
self.waitAndCheckExpectations()
}
self.waitForExpectations(timeout: 10, handler: { _ in })
self.addTeardownBlock {
dataStack.unsafeRemoveAllPersistentStoresAndWait()
}
}

View File

@@ -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

View File

@@ -31,7 +31,6 @@ import CoreStore
// MARK: - ListObserverTests
@available(macOS 10.12, *)
class ListObserverTests: BaseTestDataTestCase {
@objc
@@ -551,7 +550,6 @@ class ListObserverTests: BaseTestDataTestCase {
// MARK: TestListObserver
@available(macOS 10.12, *)
class TestListObserver: ListSectionObserver {
// MARK: ListObserver

View File

@@ -31,7 +31,6 @@ import CoreStore
// MARK: - ObjectObserverTests
@available(macOS 10.12, *)
class ObjectObserverTests: BaseTestDataTestCase {
@objc
@@ -203,7 +202,6 @@ class ObjectObserverTests: BaseTestDataTestCase {
// MARK: TestObjectObserver
@available(macOS 10.12, *)
class TestObjectObserver: ObjectObserver {
typealias ObjectEntityType = TestEntity1

View File

@@ -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, {})

View File

@@ -31,7 +31,6 @@ import CoreStore
//MARK: - SectionByTests
@available(macOS 10.12, *)
final class SectionByTests: XCTestCase {
@objc
@@ -41,11 +40,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))

View File

@@ -83,21 +83,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 +113,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 +140,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 +179,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 +206,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)

View File

@@ -383,8 +383,6 @@ final class TransactionTests: BaseTestCase {
}
}
@available(macOS 10.12, *)
@objc
dynamic func test_ThatSynchronousTransactions_CanCommitWithoutWaitingForMerges() {
@@ -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

View File

@@ -77,17 +77,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),

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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

View 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>

View 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
}
}

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "CoreStoreIcon.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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
)
}
}

View 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?
}

View 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
}
}

View 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
}

View 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

View 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

View 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

View File

@@ -0,0 +1,10 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Foundation
// MARK: - Menu
enum Menu {}

View 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()
}
}

View 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 {}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)
)
}
}
}

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()
}
}

View File

@@ -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)
)
}
}
}

View File

@@ -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)
)
}
}
}

View File

@@ -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"]
}
)
]
)
}
}
}

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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()
}
}
}

View File

@@ -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
}
)
]
)
}
}
}

View File

@@ -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(_:_:)
)
]
)
}
}
}

View File

@@ -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"
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
)
]
)
}
}
}

View File

@@ -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"
}
}

View File

@@ -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 {}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,10 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Classic
/**
Sample usages for `NSManagedObject` subclasses
*/
enum Classic {}

View File

@@ -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

View File

@@ -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)
}
}
}
}

View File

@@ -0,0 +1,28 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.ItemCell
final class ItemCell: UITableViewCell {
// MARK: Internal
static let reuseIdentifier: String = NSStringFromClass(Classic.ColorsDemo.ItemCell.self)
func setPalette(_ palette: Classic.ColorsDemo.Palette) {
self.contentView.backgroundColor = palette.color
self.textLabel?.text = palette.colorText
self.textLabel?.textColor = palette.brightness > 0.6 ? .black : .white
}
}
}

View File

@@ -0,0 +1,89 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.ListView
struct ListView: UIViewControllerRepresentable {
// MARK: Internal
init(
listMonitor: ListMonitor<Classic.ColorsDemo.Palette>,
onPaletteTapped: @escaping (Classic.ColorsDemo.Palette) -> Void
) {
self.listMonitor = listMonitor
self.onPaletteTapped = onPaletteTapped
}
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = Classic.ColorsDemo.ListViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIViewControllerType(
listMonitor: self.listMonitor,
onPaletteTapped: self.onPaletteTapped
)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
uiViewController.setEditing(
context.environment.editMode?.wrappedValue.isEditing == true,
animated: true
)
}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
// MARK: Private
private let listMonitor: ListMonitor<Classic.ColorsDemo.Palette>
private let onPaletteTapped: (Classic.ColorsDemo.Palette) -> Void
}
}
#if DEBUG
struct _Demo_Classic_ColorsDemo_ListView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let minimumSamples = 10
try! Classic.ColorsDemo.dataStack.perform(
synchronous: { transaction in
let missing = minimumSamples
- (try transaction.fetchCount(From<Classic.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
for _ in 0..<missing {
let palette = transaction.create(Into<Classic.ColorsDemo.Palette>())
palette.setRandomHue()
}
}
)
return Classic.ColorsDemo.ListView(
listMonitor: Classic.ColorsDemo.palettesMonitor,
onPaletteTapped: { _ in }
)
}
}
#endif

View File

@@ -0,0 +1,247 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import SwiftUI
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.MainView
struct MainView: View {
// MARK: Internal
init() {
let listMonitor = Classic.ColorsDemo.palettesMonitor
self.listMonitor = listMonitor
self.listHelper = .init(listMonitor: listMonitor)
self._filter = Binding(
get: { Classic.ColorsDemo.filter },
set: { Classic.ColorsDemo.filter = $0 }
)
}
// MARK: View
var body: some View {
let detailView: AnyView
if let selectedObject = self.listHelper.selectedObject() {
detailView = AnyView(
Classic.ColorsDemo.DetailView(selectedObject)
)
}
else {
detailView = AnyView(EmptyView())
}
let listMonitor = self.listMonitor
return VStack(spacing: 0) {
Classic.ColorsDemo.ListView
.init(
listMonitor: listMonitor,
onPaletteTapped: {
self.listHelper.setSelectedPalette($0)
}
)
.navigationBarTitle(
Text("Colors (\(self.listHelper.count) objects)")
)
.frame(minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.vertical)
detailView
.edgesIgnoringSafeArea(.all)
.frame(minHeight: 0, maxHeight: .infinity)
}
.navigationBarItems(
leading: HStack {
EditButton()
Button(
action: { self.clearColors() },
label: { Text("Clear") }
)
},
trailing: HStack {
Button(
action: { self.changeFilter() },
label: { Text(self.filter.rawValue) }
)
Button(
action: { self.shuffleColors() },
label: { Text("Shuffle") }
)
Button(
action: { self.addColor() },
label: { Text("Add") }
)
}
)
}
// MARK: Private
private let listMonitor: ListMonitor<Classic.ColorsDemo.Palette>
@ObservedObject
private var listHelper: ListHelper
@Binding
private var filter: Classic.ColorsDemo.Filter
private func changeFilter() {
Classic.ColorsDemo.filter = Classic.ColorsDemo.filter.next()
}
private func clearColors() {
Classic.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
try transaction.deleteAll(From<Classic.ColorsDemo.Palette>())
},
completion: { _ in }
)
}
private func addColor() {
Classic.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
_ = transaction.create(Into<Classic.ColorsDemo.Palette>())
},
completion: { _ in }
)
}
private func shuffleColors() {
Classic.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
for palette in try transaction.fetchAll(From<Classic.ColorsDemo.Palette>()) {
palette.setRandomHue()
}
},
completion: { _ in }
)
}
// MARK: - Classic.ColorsDemo.MainView.ListHelper
fileprivate final class ListHelper: ObservableObject, ListObjectObserver {
// MARK: FilePrivate
fileprivate private(set) var count: Int = 0
fileprivate init(listMonitor: ListMonitor<Classic.ColorsDemo.Palette>) {
listMonitor.addObserver(self)
self.count = listMonitor.numberOfObjects()
}
fileprivate func selectedObject() -> ObjectMonitor<Classic.ColorsDemo.Palette>? {
return self.selectedPalette.flatMap {
guard !$0.isDeleted else {
return nil
}
return Classic.ColorsDemo.dataStack.monitorObject($0)
}
}
fileprivate func setSelectedPalette(_ palette: Classic.ColorsDemo.Palette?) {
guard self.selectedPalette != palette else {
return
}
self.objectWillChange.send()
if let palette = palette, !palette.isDeleted {
self.selectedPalette = palette
}
else {
self.selectedPalette = nil
}
}
// MARK: ListObserver
typealias ListEntityType = Classic.ColorsDemo.Palette
func listMonitorDidChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
self.objectWillChange.send()
self.count = monitor.numberOfObjects()
}
func listMonitorDidRefetch(_ monitor: ListMonitor<ListEntityType>) {
self.objectWillChange.send()
self.count = monitor.numberOfObjects()
}
// MARK: ListObjectObserver
func listMonitor(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>, didDeleteObject object: Classic.ColorsDemo.Palette, fromIndexPath indexPath: IndexPath) {
if self.selectedPalette == object {
self.setSelectedPalette(nil)
}
}
// MARK: Private
private var selectedPalette: Classic.ColorsDemo.Palette?
}
}
}
#if DEBUG
struct _Demo_Classic_ColorsDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let minimumSamples = 10
try! Classic.ColorsDemo.dataStack.perform(
synchronous: { transaction in
let missing = minimumSamples
- (try transaction.fetchCount(From<Classic.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
for _ in 0..<missing {
let palette = transaction.create(Into<Classic.ColorsDemo.Palette>())
palette.setRandomHue()
}
}
)
return Classic.ColorsDemo.MainView()
}
}
#endif

View File

@@ -0,0 +1,101 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreData
import UIKit
// MARK: - Classic.ColorsDemo.Palette
@objc(Classic_ColorsDemo_Palette)
final class Classic_ColorsDemo_Palette: NSManagedObject {
// MARK: Internal
@NSManaged
dynamic var hue: Float
@NSManaged
dynamic var saturation: Float
@NSManaged
dynamic var brightness: Float
@objc
dynamic var colorGroup: String! {
let key = #keyPath(colorGroup)
if case let value as String = self.getValue(forKvcKey: key) {
return value
}
let newValue: String
switch self.hue * 359 {
case 0 ..< 20: newValue = "Lower Reds"
case 20 ..< 57: newValue = "Oranges and Browns"
case 57 ..< 90: newValue = "Yellow-Greens"
case 90 ..< 159: newValue = "Greens"
case 159 ..< 197: newValue = "Blue-Greens"
case 197 ..< 241: newValue = "Blues"
case 241 ..< 297: newValue = "Violets"
case 297 ..< 331: newValue = "Magentas"
default: newValue = "Upper Reds"
}
self.setPrimitiveValue(newValue, forKey: key)
return newValue
}
var color: UIColor {
let newValue = UIColor(
hue: CGFloat(self.hue),
saturation: CGFloat(self.saturation),
brightness: CGFloat(self.brightness),
alpha: 1.0
)
return newValue
}
var colorText: String {
let newValue: String = "H: \(self.hue * 359)˚, S: \(round(self.saturation * 100.0))%, B: \(round(self.brightness * 100.0))%"
return newValue
}
func setRandomHue() {
self.hue = Self.randomHue()
}
// MARK: NSManagedObject
public override func awakeFromInsert() {
super.awakeFromInsert()
self.hue = Self.randomHue()
self.saturation = Self.randomSaturation()
self.brightness = Self.randomBrightness()
}
// MARK: Private
private static func randomHue() -> Float {
return Float.random(in: 0.0 ... 1.0)
}
private static func randomSaturation() -> Float {
return Float.random(in: 0.4 ... 1.0)
}
private static func randomBrightness() -> Float {
return Float.random(in: 0.1 ... 0.9)
}
}

View File

@@ -0,0 +1,59 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
// MARK: - Classic
extension Classic {
// MARK: - Classic.ColorsDemo
/**
Sample usages for observing lists or single instances of `NSManagedObject`s
*/
enum ColorsDemo {
// MARK: Internal
typealias Palette = Classic_ColorsDemo_Palette
static let dataStack: DataStack = {
let dataStack = DataStack(
xcodeModelName: "Classic.ColorsDemo",
bundle: Bundle(for: Palette.self)
)
/**
- Important: `addStorageAndWait(_:)` was used here to simplify initializing the demo, but in practice the asynchronous function variants are recommended.
*/
try! dataStack.addStorageAndWait(
SQLiteStore(
fileName: "Classic.ColorsDemo.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch
)
)
return dataStack
}()
static let palettesMonitor: ListMonitor<Classic.ColorsDemo.Palette> = Classic.ColorsDemo.dataStack.monitorSectionedList(
From<Classic.ColorsDemo.Palette>()
.sectionBy(\.colorGroup)
.where(Classic.ColorsDemo.filter.whereClause())
.orderBy(.ascending(\.hue))
)
static var filter: Classic.ColorsDemo.Filter = .all {
didSet {
Classic.ColorsDemo.palettesMonitor.refetch(
self.filter.whereClause(),
OrderBy<Classic.ColorsDemo.Palette>(.ascending(\.hue))
)
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20C5048l" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Palette" representedClassName="Classic_ColorsDemo_Palette" syncable="YES">
<attribute name="brightness" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="colorGroup" optional="YES" transient="YES" attributeType="String"/>
<attribute name="hue" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="saturation" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<fetchedProperty name="testFetchProperty" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="Palette"/>
</fetchedProperty>
</entity>
<elements>
<element name="Palette" positionX="-63" positionY="-18" width="128" height="110"/>
</elements>
</model>

View File

@@ -0,0 +1,291 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.DetailViewController
final class DetailViewController: UIViewController, ObjectObserver {
/**
Sample 1: We can normally use `ObjectPublisher` directly, which is simpler. But for this demo, we will be using `ObjectMonitor` instead because we need to keep track of which properties change to prevent our `UISlider` from stuttering. Refer to the `objectMonitor(_:didUpdateObject:changedPersistentKeys:)` implementation below.
*/
var palette: ObjectMonitor<Classic.ColorsDemo.Palette> {
didSet {
oldValue.removeObserver(self)
self.startMonitoringObject()
}
}
init(_ palette: ObjectMonitor<Classic.ColorsDemo.Palette>) {
self.palette = palette
super.init(nibName: nil, bundle: nil)
}
/**
Sample 2: Once the views are created, we can start receiving `ObjectMonitor` updates in our `ObjectObserver` conformance methods. We typically call this at the end of `viewDidLoad`. Note that after the `addObserver` call, only succeeding updates will trigger our `ObjectObserver` methods, so to immediately display the current values, we need to initialize our views once (in this case, using `reloadPaletteInfo(_:changedKeys:)`.
*/
private func startMonitoringObject() {
self.palette.addObserver(self)
if let palette = self.palette.object {
self.reloadPaletteInfo(palette, changedKeys: nil)
}
}
/**
Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ObjectMonitor`s safely remove deallocated observers automatically.
*/
deinit {
self.palette.removeObserver(self)
}
/**
Sample 4: Our `objectMonitor(_:didUpdateObject:changedPersistentKeys:)` implementation passes a `Set<KeyPathString>` to our reload method. We can then inspect which values were triggered by each `UISlider`, so we can avoid double-updates that can lag the `UISlider` dragging.
*/
func reloadPaletteInfo(
_ palette: Classic.ColorsDemo.Palette,
changedKeys: Set<KeyPathString>?
) {
self.view.backgroundColor = palette.color
self.hueLabel.text = "H: \(Int(palette.hue * 359))°"
self.saturationLabel.text = "S: \(Int(palette.saturation * 100))%"
self.brightnessLabel.text = "B: \(Int(palette.brightness * 100))%"
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Classic.ColorsDemo.Palette.hue)) == true {
self.hueSlider.value = Float(palette.hue)
}
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Classic.ColorsDemo.Palette.saturation)) == true {
self.saturationSlider.value = palette.saturation
}
if changedKeys == nil
|| changedKeys?.contains(String(keyPath: \Classic.ColorsDemo.Palette.brightness)) == true {
self.brightnessSlider.value = palette.brightness
}
}
// MARK: ObjectObserver
func objectMonitor(
_ monitor: ObjectMonitor<Classic.ColorsDemo.Palette>,
didUpdateObject object: Classic.ColorsDemo.Palette,
changedPersistentKeys: Set<KeyPathString>
) {
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
let view = self.view!
let containerView = UIView()
do {
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = UIColor.white
containerView.layer.cornerRadius = 10
containerView.layer.masksToBounds = true
containerView.layer.shadowColor = UIColor(white: 0.5, alpha: 0.3).cgColor
containerView.layer.shadowOffset = .init(width: 1, height: 1)
containerView.layer.shadowRadius = 2
view.addSubview(containerView)
}
let vStackView = UIStackView()
do {
vStackView.translatesAutoresizingMaskIntoConstraints = false
vStackView.axis = .vertical
vStackView.spacing = 10
vStackView.distribution = .fill
vStackView.alignment = .fill
containerView.addSubview(vStackView)
}
let palette = self.palette.object
let rows: [(label: UILabel, slider: UISlider, initialValue: Float, sliderValueChangedSelector: Selector)] = [
(
self.hueLabel,
self.hueSlider,
palette?.hue ?? 0,
#selector(self.hueSliderValueDidChange(_:))
),
(
self.saturationLabel,
self.saturationSlider,
palette?.saturation ?? 0,
#selector(self.saturationSliderValueDidChange(_:))
),
(
self.brightnessLabel,
self.brightnessSlider,
palette?.brightness ?? 0,
#selector(self.brightnessSliderValueDidChange(_:))
)
]
for (label, slider, initialValue, sliderValueChangedSelector) in rows {
let hStackView = UIStackView()
do {
hStackView.translatesAutoresizingMaskIntoConstraints = false
hStackView.axis = .horizontal
hStackView.spacing = 5
hStackView.distribution = .fill
hStackView.alignment = .center
vStackView.addArrangedSubview(hStackView)
}
do {
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = UIColor(white: 0, alpha: 0.8)
label.textAlignment = .center
hStackView.addArrangedSubview(label)
}
do {
slider.translatesAutoresizingMaskIntoConstraints = false
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = initialValue
slider.addTarget(
self,
action: sliderValueChangedSelector,
for: .valueChanged
)
hStackView.addArrangedSubview(slider)
}
}
layout: do {
NSLayoutConstraint.activate(
[
containerView.leadingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 10
),
containerView.bottomAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -10
),
containerView.trailingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
constant: -10
),
vStackView.topAnchor.constraint(
equalTo: containerView.topAnchor,
constant: 15
),
vStackView.leadingAnchor.constraint(
equalTo: containerView.leadingAnchor,
constant: 15
),
vStackView.bottomAnchor.constraint(
equalTo: containerView.bottomAnchor,
constant: -15
),
vStackView.trailingAnchor.constraint(
equalTo: containerView.trailingAnchor,
constant: -15
)
]
)
NSLayoutConstraint.activate(
rows.map { label, _, _, _ in
label.widthAnchor.constraint(equalToConstant: 80)
}
)
}
self.startMonitoringObject()
}
// MARK: Private
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
private let hueLabel: UILabel = .init()
private let saturationLabel: UILabel = .init()
private let brightnessLabel: UILabel = .init()
private let hueSlider: UISlider = .init()
private let saturationSlider: UISlider = .init()
private let brightnessSlider: UISlider = .init()
@objc
private dynamic func hueSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Classic.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.hue = value
},
completion: { _ in }
)
}
@objc
private dynamic func saturationSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Classic.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.saturation = value
},
completion: { _ in }
)
}
@objc
private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) {
let value = sender.value
Classic.ColorsDemo.dataStack.perform(
asynchronous: { [weak self] (transaction) in
let palette = transaction.edit(self?.palette.object)
palette?.brightness = value
},
completion: { _ in }
)
}
}
}

View File

@@ -0,0 +1,199 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import UIKit
// MARK: - Classic.ColorsDemo
extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.ListViewController
final class ListViewController: UITableViewController, ListSectionObserver {
/**
Sample 1: Once the views are created, we can start observing `ListMonitor` updates. We typically call this at the end of `viewDidLoad`. Note that the `addObserver`'s closure argument will only be called on the succeeding updates, so to immediately display the current values, we need to call `tableView.reloadData()` once.
*/
private func startObservingList() {
self.listMonitor.addObserver(self)
self.tableView.reloadData()
}
/**
Sample 2: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListMonitor`s safely remove deallocated observers automatically.
*/
deinit {
self.listMonitor.removeObserver(self)
}
/**
Sample 3: `ListSectionObserver` (and inherently, `ListObjectObserver` and `ListObserver`) conformance
*/
// MARK: ListObserver
typealias ListEntityType = Classic.ColorsDemo.Palette
func listMonitorWillChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
self.tableView.beginUpdates()
}
func listMonitorDidChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
self.tableView.endUpdates()
}
func listMonitorDidRefetch(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
self.tableView.reloadData()
}
// MARK: ListObjectObserver
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didInsertObject object: ListEntityType, toIndexPath indexPath: IndexPath) {
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) {
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didUpdateObject object: ListEntityType, atIndexPath indexPath: IndexPath) {
if case let cell as Classic.ColorsDemo.ItemCell = self.tableView.cellForRow(at: indexPath) {
cell.setPalette(object)
}
}
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didMoveObject object: ListEntityType, fromIndexPath: IndexPath, toIndexPath: IndexPath) {
self.tableView.deleteRows(at: [fromIndexPath], with: .automatic)
self.tableView.insertRows(at: [toIndexPath], with: .automatic)
}
// MARK: ListSectionObserver
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) {
self.tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) {
self.tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic)
}
// MARK: UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
return self.listMonitor.numberOfSections()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.listMonitor.numberOfObjects(in: section)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: Classic.ColorsDemo.ItemCell.reuseIdentifier,
for: indexPath
) as! Classic.ColorsDemo.ItemCell
cell.setPalette(self.listMonitor[indexPath])
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.listMonitor.sectionInfo(at: section).name
}
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
let object = self.listMonitor[indexPath]
Classic.ColorsDemo.dataStack.perform(
asynchronous: { (transaction) in
transaction.delete(object)
},
completion: { _ in }
)
default:
break
}
}
// MARK: Internal
init(
listMonitor: ListMonitor<Classic.ColorsDemo.Palette>,
onPaletteTapped: @escaping (Classic.ColorsDemo.Palette) -> Void
) {
self.listMonitor = listMonitor
self.onPaletteTapped = onPaletteTapped
super.init(style: .plain)
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(
Classic.ColorsDemo.ItemCell.self,
forCellReuseIdentifier: Classic.ColorsDemo.ItemCell.reuseIdentifier
)
self.startObservingList()
}
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.onPaletteTapped(
self.listMonitor[indexPath]
)
}
// MARK: Private
private let listMonitor: ListMonitor<Classic.ColorsDemo.Palette>
private let onPaletteTapped: (Classic.ColorsDemo.Palette) -> Void
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
}
}

View File

@@ -0,0 +1,10 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
// MARK: - Modern
/**
Sample usages for `CoreStoreObject` subclasses
*/
enum Modern {}

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