diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86348a76..a82baaa3 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,8 @@ name: "CoreDataPlus CI" -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - on: schedule: - - cron: '0 23 * * *' # At the end of every day + - cron: '0 0 * * 2' # Run every Tuesday push: branches: - main @@ -16,12 +12,16 @@ on: - main - develop +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + jobs: info: name: Show macOS and Xcode versions - runs-on: macos-12 + runs-on: macos-14 env: - DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer steps: - name: Versions run: | @@ -32,95 +32,114 @@ jobs: xcrun simctl list macOS: name: Test macOS - runs-on: macos-12 + runs-on: macos-14 env: - DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: macOS - run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus macOS" -destination "platform=macOS" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus" -destination "platform=macOS" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' - name: Upload tests report if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: macOS tests report path: ~/Downloads/Report iOS: name: Test iOS - runs-on: macos-12 + runs-on: macos-14 env: - DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer strategy: matrix: - destination: ["OS=16.0,name=iPhone 13 Pro"] #, "OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X"] + destination: ["OS=17.0,name=iPhone 15 Pro"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: iOS - ${{ matrix.destination }} - run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus iOS" -destination "${{ matrix.destination }}" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus" -destination "${{ matrix.destination }}" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' - name: Upload tests report if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: iOS tests report - path: ~/Downloads/Report + path: ~/Downloads/Report + visionOS: + name: Test visionOS + runs-on: macos-14 + env: + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + strategy: + matrix: + destination: ["OS=1.0,name=Apple Vision Pro"] + steps: + - uses: actions/checkout@v4 + - name: iOS - ${{ matrix.destination }} + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus" -destination "${{ matrix.destination }}" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' + + - name: Upload tests report + if: always() + uses: actions/upload-artifact@v4 + with: + name: visionOS tests report + path: ~/Downloads/Report tvOS: name: Test tvOS - runs-on: macos-12 + runs-on: macos-14 env: - DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer strategy: matrix: - destination: ["OS=16.0,name=Apple TV 4K (2nd generation)"] #, "OS=13.0,name=Apple TV 4K (at 1080p)", "OS=11.4,name=Apple TV 4K"] + destination: ["OS=17.0,name=Apple TV"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: tvOS - ${{ matrix.destination }} - run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus tvOS" -destination "${{ matrix.destination }}" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus" -destination "${{ matrix.destination }}" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' - name: Upload tests report if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: tvOS tests report path: ~/Downloads/Report watchOS: name: Test watchOS - runs-on: macOS-12 + runs-on: macos-14 env: - DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer strategy: matrix: - destination: ["OS=9.0,name=Apple Watch Series 8 (45mm)"] #, ""OS=5.3,name=Apple Watch Series 4 - 44mm", "OS=4.2,name=Apple Watch Series 3 - 42mm"] + destination: ["OS=10.0,name=Apple Watch Series 9 (45mm)"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: watchOS - ${{ matrix.destination }} - run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus watchOS" -destination "${{ matrix.destination }}" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "CoreDataPlus.xcodeproj" -scheme "CoreDataPlus" -destination "${{ matrix.destination }}" clean test -quiet -resultBundlePath '~/Downloads/Report/report.xcresult' - name: Upload tests report if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: watchOS tests report path: ~/Downloads/Report - spm: + SPM: name: Test SPM Integration - runs-on: macos-12 + runs-on: macos-14 env: - DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: SPM Test run: | swift --version swift test - lint: - name: Swift Lint - runs-on: macos-12 - steps: - - uses: actions/checkout@v2 - - name: Run SwiftLint - run: | - swift --version - swiftlint --reporter github-actions-logging +# lint: +# name: Swift Lint +# runs-on: macos-14 +# steps: +# - uses: actions/checkout@v4 +# - name: Run SwiftLint +# run: | +# swift --version +# swiftlint --reporter github-actions-logging diff --git a/.jazzy.yml b/.jazzy.yml deleted file mode 100755 index 60a58445..00000000 --- a/.jazzy.yml +++ /dev/null @@ -1,7 +0,0 @@ -module: CoreDataPlus -author: Alessandro Marzoli -author_url: http://www.alessandromarzoli.com -github_url: http://www.alessandromarzoli.com/CoreDataPlus/ -copyright: "Β©[Alessandro Marzoli](http://www.alessandromarzoli.com)" -clean: true -podspec: CoreDataPlus.podspec diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..72da7055 --- /dev/null +++ b/.swift-format @@ -0,0 +1,69 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentation" : { + "spaces" : 2 + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 120, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : false, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 8, + "version" : 1 +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f48e88..2bb98758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +### 6.0.0 ⭐ + +- Xcode 15. +- Swift 5.10 support. +- VisionOS support. +- Swift Concurrency support. +- `NSCompositeAttributeDescription` extensions. +- Removed `LightweightMigrationManager`. +- `entityName` is now ovverridable +- `ModelVersion` now support both the old migration flow (`LegacyMigration`) and the new one (`StagedMigration`) + ### 5.0.0 ⭐ - Xcode 13. @@ -19,7 +30,7 @@ - Added `NSAttributeDescription` utility methods. - Added `NSEntityMapping` utility methods. - Added `NSAttributeDescription` utility methods. -- Added `LightweightMigrationManger`, a `NSMigrationManager` subclass to do *lightweight* migrations with a fake progress reporting. +- Added `LightweightMigrationManager`, a `NSMigrationManager` subclass to do *lightweight* migrations with a fake progress reporting. - Added `MigrationProgressReporter` to report migration progress via a `Progress` object. - Added a `NSManagedObjectContext` helper method to create a child context. - Added support for `NSPersistentStoreCoordinator` notifications payloads. diff --git a/CoreDataPlus.xcodeproj/project.pbxproj b/CoreDataPlus.xcodeproj/project.pbxproj index 37306c02..6eb391d9 100644 --- a/CoreDataPlus.xcodeproj/project.pbxproj +++ b/CoreDataPlus.xcodeproj/project.pbxproj @@ -3,10 +3,21 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ + 06B3A5A62BD7E447009217C0 /* Swift-Format Lint */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 06B3A5A92BD7E447009217C0 /* Build configuration list for PBXAggregateTarget "Swift-Format Lint" */; + buildPhases = ( + 06B3A5AA2BD7E459009217C0 /* Lint */, + ); + dependencies = ( + ); + name = "Swift-Format Lint"; + productName = "Swift-Format lint"; + }; 2364CE511FA383EF0076F2B8 /* SwiftLint */ = { isa = PBXAggregateTarget; buildConfigurationList = 2364CE541FA383F00076F2B8 /* Build configuration list for PBXAggregateTarget "SwiftLint" */; @@ -32,96 +43,8 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 061C684E261EF969000BF0A2 /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C684D261EF969000BF0A2 /* BaseTestCase.swift */; }; - 061C684F261EF969000BF0A2 /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C684D261EF969000BF0A2 /* BaseTestCase.swift */; }; - 061C6850261EF969000BF0A2 /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C684D261EF969000BF0A2 /* BaseTestCase.swift */; }; - 061C689326209443000BF0A2 /* CustomTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C689226209443000BF0A2 /* CustomTransformer.swift */; }; - 061C689426209443000BF0A2 /* CustomTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C689226209443000BF0A2 /* CustomTransformer.swift */; }; - 061C689526209443000BF0A2 /* CustomTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C689226209443000BF0A2 /* CustomTransformer.swift */; }; - 061C689626209443000BF0A2 /* CustomTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C689226209443000BF0A2 /* CustomTransformer.swift */; }; - 063E100E264BC2F90050E84C /* Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063E100D264BC2F90050E84C /* Deprecated.swift */; }; - 063E100F264BC2F90050E84C /* Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063E100D264BC2F90050E84C /* Deprecated.swift */; }; - 063E1010264BC2F90050E84C /* Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063E100D264BC2F90050E84C /* Deprecated.swift */; }; - 063E1011264BC2F90050E84C /* Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063E100D264BC2F90050E84C /* Deprecated.swift */; }; - 065094062643E8BC00B3EE12 /* MigrationProgressReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065094052643E8BC00B3EE12 /* MigrationProgressReporter.swift */; }; - 065094072643E8BC00B3EE12 /* MigrationProgressReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065094052643E8BC00B3EE12 /* MigrationProgressReporter.swift */; }; - 065094082643E8BC00B3EE12 /* MigrationProgressReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065094052643E8BC00B3EE12 /* MigrationProgressReporter.swift */; }; - 065094092643E8BC00B3EE12 /* MigrationProgressReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065094052643E8BC00B3EE12 /* MigrationProgressReporter.swift */; }; - 0650940B2643E8EB00B3EE12 /* LightweightMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0650940A2643E8EB00B3EE12 /* LightweightMigrationManager.swift */; }; - 0650940C2643E8EB00B3EE12 /* LightweightMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0650940A2643E8EB00B3EE12 /* LightweightMigrationManager.swift */; }; - 0650940D2643E8EB00B3EE12 /* LightweightMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0650940A2643E8EB00B3EE12 /* LightweightMigrationManager.swift */; }; - 0650940E2643E8EB00B3EE12 /* LightweightMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0650940A2643E8EB00B3EE12 /* LightweightMigrationManager.swift */; }; - 06968AD326454AFF00088D76 /* CoreDataPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 236350741F95EC1A00B3A16A /* CoreDataPlus.framework */; }; - 06968ADA26454CAC00088D76 /* NSFetchRequestResultCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift */; }; - 06968ADB26454CAC00088D76 /* NSManagedObjectUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351061F95F28C00B3A16A /* NSManagedObjectUtilsTests.swift */; }; - 06968ADC26454CAC00088D76 /* NSFetchRequestResultUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift */; }; - 06968ADD26454CAC00088D76 /* NSAttributeDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282323A2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift */; }; - 06968ADE26454CAC00088D76 /* ModelVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351041F95F28B00B3A16A /* ModelVersionTests.swift */; }; - 06968ADF26454CAC00088D76 /* MigrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230C087B21417E1900A1B6CB /* MigrationsTests.swift */; }; - 06968AE026454CAC00088D76 /* TransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CDC6261DFC8F000563F0 /* TransformerTests.swift */; }; - 06968AE126454CAC00088D76 /* FetchedResultsChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D1056A20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift */; }; - 06968AE226454CAC00088D76 /* NSManagedObjectContextUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351031F95F28B00B3A16A /* NSManagedObjectContextUtilsTests.swift */; }; - 06968AE326454CAC00088D76 /* NSEntityDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift */; }; - 06968AE426454CAC00088D76 /* ProgrammaticallyDefinedModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift */; }; - 06968AE526454CAC00088D76 /* NSManagedObjectContextHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift */; }; - 06968AE626454CAC00088D76 /* NSManagedObjectContextInvestigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232440B622D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift */; }; - 06968AE726454CAC00088D76 /* NSManagedObjectUpdateTimestampableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift */; }; - 06968AE826454CAC00088D76 /* NSFetchRequestUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351071F95F28C00B3A16A /* NSFetchRequestUtilsTests.swift */; }; - 06968AE926454CAC00088D76 /* NSManagedObjectDelayedDeletableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift */; }; - 06968AEA26454CAC00088D76 /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift */; }; - 06968AEB26454CAC00088D76 /* NSSetCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CE35244762FA00AD19D8 /* NSSetCoreDataTests.swift */; }; - 06968AEC26454CAC00088D76 /* ProgrammaticMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4C1F2622029400F0CB25 /* ProgrammaticMigrationTests.swift */; }; - 06968AED26454CB200088D76 /* NotificationPayloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2351687122A2DD4500340611 /* NotificationPayloadTests.swift */; }; - 06968AEE26454CB200088D76 /* NotificationMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C6493920396FFA00AA514A /* NotificationMergeTests.swift */; }; - 06968AEF26454CBC00088D76 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232942624754700F37442 /* Page.swift */; }; - 06968AF026454CBC00088D76 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282326826244D3A00F37442 /* Author.swift */; }; - 06968AF126454CBC00088D76 /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282331E26257A7400F37442 /* Feedback.swift */; }; - 06968AF226454CBC00088D76 /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233642625C55100F37442 /* Content.swift */; }; - 06968AF326454CBC00088D76 /* Book.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282327E2624569400F37442 /* Book.swift */; }; - 06968AF426454CBC00088D76 /* Cover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233712625C56A00F37442 /* Cover.swift */; }; - 06968AF526454CBF00088D76 /* SampleModel2+V1.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233412625C23F00F37442 /* SampleModel2+V1.swift */; }; - 06968AF626454CBF00088D76 /* SampleModel2+V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282334E2625C2A200F37442 /* SampleModel2+V2.swift */; }; - 06968AF726454CBF00088D76 /* SampleModel2+V3.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A448242631B937003059A7 /* SampleModel2+V3.swift */; }; - 06968AF826454CBF00088D76 /* NSManagedObjectContext+SampleModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282325A26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift */; }; - 06968AF926454CBF00088D76 /* SampleModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4BC62621F3B000F0CB25 /* SampleModel2.swift */; }; - 06968AFA26454CC700088D76 /* SampleModelV1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 06A6CE25261E0961000563F0 /* SampleModelV1.sqlite */; }; - 06968AFB26454CCB00088D76 /* SampleModelV2.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D27A2E2D255F29D20043B43F /* SampleModelV2.sqlite */; }; - 06968AFC26454CD300088D76 /* V2to3MakerPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237728642148FACE00FDAF32 /* V2to3MakerPolicy.swift */; }; - 06968AFD26454CD300088D76 /* V2toV3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 2377287021490B9D00FDAF32 /* V2toV3.xcmappingmodel */; }; - 06968AFE26454CD600088D76 /* SampleModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2363510B1F95F28C00B3A16A /* SampleModelVersion.swift */; }; - 06968AFF26454CD900088D76 /* NSManagedObject+DelayedDeletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808D1F94FFF600A365C0 /* NSManagedObject+DelayedDeletable.swift */; }; - 06968B0026454CD900088D76 /* Maker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 234DA7AA214A5D2B00D5C24F /* Maker.swift */; }; - 06968B0126454CD900088D76 /* NSManagedObject+UpdateTimestampable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808B1F94FFF600A365C0 /* NSManagedObject+UpdateTimestampable.swift */; }; - 06968B0226454CD900088D76 /* Car.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EEF5441F962E5700A2E72F /* Car.swift */; }; - 06968B0326454CD900088D76 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EEF5481F962E6A00A2E72F /* Person.swift */; }; - 06968B0426454CDD00088D76 /* SampleModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA231F9641BD0058654B /* SampleModel.xcdatamodeld */; }; - 06968B0526454CE000088D76 /* NSManagedObjectContext+SampleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233654361F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift */; }; - 06968B0626454CE500088D76 /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C684D261EF969000BF0A2 /* BaseTestCase.swift */; }; - 06968B0726454CE500088D76 /* InMemoryTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236526AA215A46C200A51C9F /* InMemoryTestCase.swift */; }; - 06968B0826454CE500088D76 /* OnDiskTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239F4CBF22F49F2F007888BA /* OnDiskTestCase.swift */; }; - 06968B0926454CE500088D76 /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232C22624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift */; }; - 06968B0A26454CE500088D76 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FFB86122EC948C00391D40 /* Utils.swift */; }; - 06968B0B26454CE500088D76 /* CoreDataErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282332B2625AB2300F37442 /* CoreDataErrors.swift */; }; - 06968B0D26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06968B0C26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift */; }; - 06968B0E26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06968B0C26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift */; }; - 06968B0F26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06968B0C26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift */; }; - 06968B1026454FBF00088D76 /* BookCoverToCoverMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06968B0C26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift */; }; - 06968B122645685F00088D76 /* FeedbackMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06968B112645685F00088D76 /* FeedbackMigrationManager.swift */; }; - 06968B132645685F00088D76 /* FeedbackMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06968B112645685F00088D76 /* FeedbackMigrationManager.swift */; }; - 06968B142645685F00088D76 /* FeedbackMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06968B112645685F00088D76 /* FeedbackMigrationManager.swift */; }; - 06968B152645685F00088D76 /* FeedbackMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06968B112645685F00088D76 /* FeedbackMigrationManager.swift */; }; - 06A6CDA0261DE0E7000563F0 /* Transformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CD9F261DE0E7000563F0 /* Transformer.swift */; }; - 06A6CDA1261DE0E7000563F0 /* Transformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CD9F261DE0E7000563F0 /* Transformer.swift */; }; - 06A6CDA2261DE0E7000563F0 /* Transformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CD9F261DE0E7000563F0 /* Transformer.swift */; }; - 06A6CDA3261DE0E7000563F0 /* Transformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CD9F261DE0E7000563F0 /* Transformer.swift */; }; - 06A6CDC7261DFC8F000563F0 /* TransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CDC6261DFC8F000563F0 /* TransformerTests.swift */; }; - 06A6CDC8261DFC8F000563F0 /* TransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CDC6261DFC8F000563F0 /* TransformerTests.swift */; }; - 06A6CDC9261DFC8F000563F0 /* TransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CDC6261DFC8F000563F0 /* TransformerTests.swift */; }; - 06A6CE26261E0966000563F0 /* SampleModelV1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 06A6CE25261E0961000563F0 /* SampleModelV1.sqlite */; }; - 06A6CE30261E0966000563F0 /* SampleModelV1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 06A6CE25261E0961000563F0 /* SampleModelV1.sqlite */; }; - 06A6CE3A261E0966000563F0 /* SampleModelV1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 06A6CE25261E0961000563F0 /* SampleModelV1.sqlite */; }; + 06A1D11F2BF34C6300F62CA1 /* LegacyMigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A1D11E2BF34C6300F62CA1 /* LegacyMigrationStep.swift */; }; 06AF615026F091370090A61B /* CoreDataPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06AF614826F091370090A61B /* CoreDataPlus.framework */; }; - 06AF615D26F092350090A61B /* CoreDataPlus.h in Headers */ = {isa = PBXBuildFile; fileRef = 23B5806D1F94FEDF00A365C0 /* CoreDataPlus.h */; settings = {ATTRIBUTES = (Public, ); }; }; 06AF615E26F092BB0090A61B /* NSPersistentStoreCoordinator+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FD2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift */; }; 06AF615F26F092BB0090A61B /* NSBatchDeleteResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F32614E3D9001902FB /* NSBatchDeleteResult+Utils.swift */; }; 06AF616026F092BB0090A61B /* NSDerivedAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232AB26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift */; }; @@ -142,40 +65,38 @@ 06AF616F26F092BB0090A61B /* NSManagedObject+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FF2614E3D9001902FB /* NSManagedObject+Utils.swift */; }; 06AF617026F092BB0090A61B /* NSManagedObjectContext+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FC2614E3D9001902FB /* NSManagedObjectContext+History.swift */; }; 06AF617126F092BB0090A61B /* Collection+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F72614E3D9001902FB /* Collection+CoreData.swift */; }; - 06AF617226F092C30090A61B /* MigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A042614E3D9001902FB /* MigrationStep.swift */; }; 06AF617326F092C30090A61B /* ModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A022614E3D9001902FB /* ModelVersion.swift */; }; - 06AF617426F092C30090A61B /* LightweightMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0650940A2643E8EB00B3EE12 /* LightweightMigrationManager.swift */; }; 06AF617526F092C30090A61B /* MigrationProgressReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065094052643E8BC00B3EE12 /* MigrationProgressReporter.swift */; }; 06AF617626F092C30090A61B /* Migrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A9263BFA22000BBD13 /* Migrator.swift */; }; 06AF617726F092C80090A61B /* Notification+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A072614E3D9001902FB /* Notification+Utils.swift */; }; 06AF617826F092C80090A61B /* Notification+Payloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A062614E3D9001902FB /* Notification+Payloads.swift */; }; 06AF617926F092CC0090A61B /* Transformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CD9F261DE0E7000563F0 /* Transformer.swift */; }; 06AF617A26F092CC0090A61B /* CustomTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C689226209443000BF0A2 /* CustomTransformer.swift */; }; - 06AF617B26F093E20090A61B /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift */; }; - 06AF617C26F093E20090A61B /* NSManagedObjectUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351061F95F28C00B3A16A /* NSManagedObjectUtilsTests.swift */; }; - 06AF617D26F093E20090A61B /* NSFetchRequestUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351071F95F28C00B3A16A /* NSFetchRequestUtilsTests.swift */; }; - 06AF617E26F093E20090A61B /* FetchedResultsChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D1056A20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift */; }; - 06AF617F26F093E20090A61B /* NSAttributeDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282323A2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift */; }; - 06AF618026F093E20090A61B /* NSEntityDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift */; }; - 06AF618126F093E20090A61B /* FetchesWithAffectedStoresTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E06365264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift */; }; - 06AF618226F093E20090A61B /* NSFetchRequestResultCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift */; }; - 06AF618326F093E20090A61B /* MigrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230C087B21417E1900A1B6CB /* MigrationsTests.swift */; }; - 06AF618426F093E20090A61B /* NSManagedObjectContextInvestigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232440B622D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift */; }; - 06AF618526F093E20090A61B /* NSSetCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CE35244762FA00AD19D8 /* NSSetCoreDataTests.swift */; }; - 06AF618626F093E20090A61B /* ProgrammaticMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4C1F2622029400F0CB25 /* ProgrammaticMigrationTests.swift */; }; - 06AF618726F093E20090A61B /* ModelVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351041F95F28B00B3A16A /* ModelVersionTests.swift */; }; - 06AF618826F093E20090A61B /* TransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CDC6261DFC8F000563F0 /* TransformerTests.swift */; }; - 06AF618926F093E20090A61B /* NSManagedObjectDelayedDeletableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift */; }; - 06AF618A26F093E20090A61B /* NSPredicateUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D56626E5F8DB00929CA6 /* NSPredicateUtilsTests.swift */; }; - 06AF618B26F093E20090A61B /* NSManagedObjectUpdateTimestampableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift */; }; - 06AF618C26F093E20090A61B /* NSManagedObjectContextHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift */; }; - 06AF618D26F093E20090A61B /* NSManagedObjectContextUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351031F95F28B00B3A16A /* NSManagedObjectContextUtilsTests.swift */; }; - 06AF618E26F093E20090A61B /* ProgrammaticallyDefinedModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift */; }; - 06AF618F26F093E20090A61B /* NSFetchRequestResultUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift */; }; - 06AF619026F093E60090A61B /* NotificationMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C6493920396FFA00AA514A /* NotificationMergeTests.swift */; }; - 06AF619126F093E60090A61B /* NotificationPayloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2351687122A2DD4500340611 /* NotificationPayloadTests.swift */; }; - 06AF619226F093EE0090A61B /* SampleModelV1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 06A6CE25261E0961000563F0 /* SampleModelV1.sqlite */; }; - 06AF619326F093F20090A61B /* SampleModelV2.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D27A2E2D255F29D20043B43F /* SampleModelV2.sqlite */; }; + 06AF617B26F093E20090A61B /* NSPersistentStoreCoordinatorUtils_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtils_Tests.swift */; }; + 06AF617C26F093E20090A61B /* NSManagedObjectUtils_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351061F95F28C00B3A16A /* NSManagedObjectUtils_Tests.swift */; }; + 06AF617D26F093E20090A61B /* NSFetchRequestUtils_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351071F95F28C00B3A16A /* NSFetchRequestUtils_Tests.swift */; }; + 06AF617E26F093E20090A61B /* FetchedResultsChanges_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D1056A20D7EFEF00AE84CC /* FetchedResultsChanges_Tests.swift */; }; + 06AF617F26F093E20090A61B /* NSAttributeDescriptionUtils_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282323A2624342400F37442 /* NSAttributeDescriptionUtils_Tests.swift */; }; + 06AF618026F093E20090A61B /* NSEntityDescriptionUtils_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtils_Tests.swift */; }; + 06AF618126F093E20090A61B /* FetchesWithAffectedStores_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E06365264ABC0D009145B6 /* FetchesWithAffectedStores_Tests.swift */; }; + 06AF618226F093E20090A61B /* NSFetchRequestResultCoreData_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreData_Tests.swift */; }; + 06AF618326F093E20090A61B /* Migrations_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230C087B21417E1900A1B6CB /* Migrations_Tests.swift */; }; + 06AF618426F093E20090A61B /* NSManagedObjectContextInvestigation_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232440B622D2546100A04649 /* NSManagedObjectContextInvestigation_Tests.swift */; }; + 06AF618526F093E20090A61B /* NSSetCoreData_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CE35244762FA00AD19D8 /* NSSetCoreData_Tests.swift */; }; + 06AF618626F093E20090A61B /* ProgrammaticMigration_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4C1F2622029400F0CB25 /* ProgrammaticMigration_Tests.swift */; }; + 06AF618726F093E20090A61B /* ModelVersion_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351041F95F28B00B3A16A /* ModelVersion_Tests.swift */; }; + 06AF618826F093E20090A61B /* Transformer_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06A6CDC6261DFC8F000563F0 /* Transformer_Tests.swift */; }; + 06AF618926F093E20090A61B /* NSManagedObjectDelayedDeletable_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletable_Tests.swift */; }; + 06AF618A26F093E20090A61B /* NSPredicateUtils_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D56626E5F8DB00929CA6 /* NSPredicateUtils_Tests.swift */; }; + 06AF618B26F093E20090A61B /* NSManagedObjectUpdateTimestampable_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampable_Tests.swift */; }; + 06AF618C26F093E20090A61B /* NSManagedObjectContextHistory_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistory_Tests.swift */; }; + 06AF618D26F093E20090A61B /* NSManagedObjectContextUtils_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351031F95F28B00B3A16A /* NSManagedObjectContextUtils_Tests.swift */; }; + 06AF618E26F093E20090A61B /* ProgrammaticallyDefinedModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModel_Tests.swift */; }; + 06AF618F26F093E20090A61B /* NSFetchRequestResultUtils_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtils_Tests.swift */; }; + 06AF619026F093E60090A61B /* NotificationMerge_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C6493920396FFA00AA514A /* NotificationMerge_Tests.swift */; }; + 06AF619126F093E60090A61B /* NotificationPayload_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2351687122A2DD4500340611 /* NotificationPayload_Tests.swift */; }; + 06AF619226F093EE0090A61B /* SampleModel_V1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 06A6CE25261E0961000563F0 /* SampleModel_V1.sqlite */; }; + 06AF619326F093F20090A61B /* SampleModel_V2.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D27A2E2D255F29D20043B43F /* SampleModel_V2.sqlite */; }; 06AF619426F093F60090A61B /* V2to3MakerPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237728642148FACE00FDAF32 /* V2to3MakerPolicy.swift */; }; 06AF619526F093FB0090A61B /* SampleModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2363510B1F95F28C00B3A16A /* SampleModelVersion.swift */; }; 06AF619626F093FE0090A61B /* NSManagedObject+DelayedDeletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808D1F94FFF600A365C0 /* NSManagedObject+DelayedDeletable.swift */; }; @@ -205,273 +126,21 @@ 06AF61AE26F0941A0090A61B /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232C22624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift */; }; 06AF61AF26F0941A0090A61B /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FFB86122EC948C00391D40 /* Utils.swift */; }; 06AF61B026F0941A0090A61B /* CoreDataErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282332B2625AB2300F37442 /* CoreDataErrors.swift */; }; - 06AF61B226F0959F0090A61B /* V2toV3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 2377287021490B9D00FDAF32 /* V2toV3.xcmappingmodel */; }; - 06B71C742629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift */; }; - 06B71C752629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift */; }; - 06B71C762629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift */; }; - 06B71CB82629DB04008DFD11 /* PersistentStoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71CB72629DB04008DFD11 /* PersistentStoreOptions.swift */; }; - 06B71CB92629DB04008DFD11 /* PersistentStoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71CB72629DB04008DFD11 /* PersistentStoreOptions.swift */; }; - 06B71CBA2629DB05008DFD11 /* PersistentStoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71CB72629DB04008DFD11 /* PersistentStoreOptions.swift */; }; - 06B71CBB2629DB05008DFD11 /* PersistentStoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B71CB72629DB04008DFD11 /* PersistentStoreOptions.swift */; }; - 06E06366264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E06365264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift */; }; - 06E06367264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E06365264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift */; }; - 06E06368264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E06365264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift */; }; - 06E06369264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E06365264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift */; }; - 06F7D55D26E5F85D00929CA6 /* NSPredicate+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D55C26E5F85D00929CA6 /* NSPredicate+Utils.swift */; }; - 06F7D55E26E5F85D00929CA6 /* NSPredicate+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D55C26E5F85D00929CA6 /* NSPredicate+Utils.swift */; }; - 06F7D55F26E5F85D00929CA6 /* NSPredicate+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D55C26E5F85D00929CA6 /* NSPredicate+Utils.swift */; }; - 06F7D56026E5F85D00929CA6 /* NSPredicate+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D55C26E5F85D00929CA6 /* NSPredicate+Utils.swift */; }; - 06F7D56726E5F8DB00929CA6 /* NSPredicateUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D56626E5F8DB00929CA6 /* NSPredicateUtilsTests.swift */; }; - 06F7D56826E5F8DB00929CA6 /* NSPredicateUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D56626E5F8DB00929CA6 /* NSPredicateUtilsTests.swift */; }; - 06F7D56926E5F8DB00929CA6 /* NSPredicateUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D56626E5F8DB00929CA6 /* NSPredicateUtilsTests.swift */; }; - 06F7D56A26E5F8DB00929CA6 /* NSPredicateUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F7D56626E5F8DB00929CA6 /* NSPredicateUtilsTests.swift */; }; - 230C087C21417E1900A1B6CB /* MigrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230C087B21417E1900A1B6CB /* MigrationsTests.swift */; }; - 230C087D21417E1900A1B6CB /* MigrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230C087B21417E1900A1B6CB /* MigrationsTests.swift */; }; - 230C087E21417E1900A1B6CB /* MigrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230C087B21417E1900A1B6CB /* MigrationsTests.swift */; }; - 232440B722D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232440B622D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift */; }; - 232440B822D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232440B622D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift */; }; - 232440B922D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232440B622D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift */; }; - 233654371F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233654361F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift */; }; - 233654381F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233654361F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift */; }; - 233654391F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233654361F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift */; }; - 234130661F95FF90002BE7FC /* SampleModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2363510B1F95F28C00B3A16A /* SampleModelVersion.swift */; }; - 234130671F95FF90002BE7FC /* ModelVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351041F95F28B00B3A16A /* ModelVersionTests.swift */; }; - 234130681F95FF90002BE7FC /* NSFetchRequestUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351071F95F28C00B3A16A /* NSFetchRequestUtilsTests.swift */; }; - 2341306A1F95FF90002BE7FC /* NSManagedObjectContextUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351031F95F28B00B3A16A /* NSManagedObjectContextUtilsTests.swift */; }; - 2341306B1F95FF90002BE7FC /* NSManagedObjectUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351061F95F28C00B3A16A /* NSManagedObjectUtilsTests.swift */; }; - 2341306D1F95FF91002BE7FC /* SampleModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2363510B1F95F28C00B3A16A /* SampleModelVersion.swift */; }; - 2341306E1F95FF91002BE7FC /* ModelVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351041F95F28B00B3A16A /* ModelVersionTests.swift */; }; - 2341306F1F95FF91002BE7FC /* NSFetchRequestUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351071F95F28C00B3A16A /* NSFetchRequestUtilsTests.swift */; }; - 234130711F95FF91002BE7FC /* NSManagedObjectContextUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351031F95F28B00B3A16A /* NSManagedObjectContextUtilsTests.swift */; }; - 234130721F95FF91002BE7FC /* NSManagedObjectUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351061F95F28C00B3A16A /* NSManagedObjectUtilsTests.swift */; }; - 234130741F95FF91002BE7FC /* SampleModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2363510B1F95F28C00B3A16A /* SampleModelVersion.swift */; }; - 234130751F95FF91002BE7FC /* ModelVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351041F95F28B00B3A16A /* ModelVersionTests.swift */; }; - 234130761F95FF91002BE7FC /* NSFetchRequestUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351071F95F28C00B3A16A /* NSFetchRequestUtilsTests.swift */; }; - 234130781F95FF91002BE7FC /* NSManagedObjectContextUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351031F95F28B00B3A16A /* NSManagedObjectContextUtilsTests.swift */; }; - 234130791F95FF91002BE7FC /* NSManagedObjectUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236351061F95F28C00B3A16A /* NSManagedObjectUtilsTests.swift */; }; - 234DA7AB214A5D2B00D5C24F /* Maker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 234DA7AA214A5D2B00D5C24F /* Maker.swift */; }; - 234DA7AC214A5D2B00D5C24F /* Maker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 234DA7AA214A5D2B00D5C24F /* Maker.swift */; }; - 234DA7AD214A5D2B00D5C24F /* Maker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 234DA7AA214A5D2B00D5C24F /* Maker.swift */; }; - 2351687222A2DD4500340611 /* NotificationPayloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2351687122A2DD4500340611 /* NotificationPayloadTests.swift */; }; - 2351687322A2DD4500340611 /* NotificationPayloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2351687122A2DD4500340611 /* NotificationPayloadTests.swift */; }; - 2351687422A2DD4500340611 /* NotificationPayloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2351687122A2DD4500340611 /* NotificationPayloadTests.swift */; }; - 23541AFA22ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift */; }; - 23541AFB22ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift */; }; - 23541AFC22ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift */; }; - 236350B51F95F0ED00B3A16A /* CoreDataPlus.h in Headers */ = {isa = PBXBuildFile; fileRef = 23B5806D1F94FEDF00A365C0 /* CoreDataPlus.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 236350B61F95F0EF00B3A16A /* CoreDataPlus.h in Headers */ = {isa = PBXBuildFile; fileRef = 23B5806D1F94FEDF00A365C0 /* CoreDataPlus.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 236350B71F95F0F200B3A16A /* CoreDataPlus.h in Headers */ = {isa = PBXBuildFile; fileRef = 23B5806D1F94FEDF00A365C0 /* CoreDataPlus.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 236526AB215A46C200A51C9F /* InMemoryTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236526AA215A46C200A51C9F /* InMemoryTestCase.swift */; }; - 236526AC215A46C200A51C9F /* InMemoryTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236526AA215A46C200A51C9F /* InMemoryTestCase.swift */; }; - 236526AD215A46C200A51C9F /* InMemoryTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236526AA215A46C200A51C9F /* InMemoryTestCase.swift */; }; - 237728652148FACE00FDAF32 /* V2to3MakerPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237728642148FACE00FDAF32 /* V2to3MakerPolicy.swift */; }; - 237728662148FACE00FDAF32 /* V2to3MakerPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237728642148FACE00FDAF32 /* V2to3MakerPolicy.swift */; }; - 237728672148FACE00FDAF32 /* V2to3MakerPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237728642148FACE00FDAF32 /* V2to3MakerPolicy.swift */; }; - 2377287121490B9D00FDAF32 /* V2toV3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 2377287021490B9D00FDAF32 /* V2toV3.xcmappingmodel */; }; - 2377287221490B9D00FDAF32 /* V2toV3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 2377287021490B9D00FDAF32 /* V2toV3.xcmappingmodel */; }; - 2377287321490B9D00FDAF32 /* V2toV3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 2377287021490B9D00FDAF32 /* V2toV3.xcmappingmodel */; }; - 2383AC641FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift */; }; - 2383AC651FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift */; }; - 2383AC661FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift */; }; - 23948D1F1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift */; }; - 23948D201F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift */; }; - 23948D211F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift */; }; - 239F4CC022F49F2F007888BA /* OnDiskTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239F4CBF22F49F2F007888BA /* OnDiskTestCase.swift */; }; - 239F4CC122F49F2F007888BA /* OnDiskTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239F4CBF22F49F2F007888BA /* OnDiskTestCase.swift */; }; - 239F4CC222F49F2F007888BA /* OnDiskTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239F4CBF22F49F2F007888BA /* OnDiskTestCase.swift */; }; - 23A8A44D1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift */; }; - 23A8A44E1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift */; }; - 23A8A44F1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift */; }; - 23B5807B1F94FEDF00A365C0 /* CoreDataPlus.h in Headers */ = {isa = PBXBuildFile; fileRef = 23B5806D1F94FEDF00A365C0 /* CoreDataPlus.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 23C3B2B61FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift */; }; - 23C3B2B71FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift */; }; - 23C3B2B81FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift */; }; - 23C6493A20396FFA00AA514A /* NotificationMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C6493920396FFA00AA514A /* NotificationMergeTests.swift */; }; - 23C6493B20396FFA00AA514A /* NotificationMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C6493920396FFA00AA514A /* NotificationMergeTests.swift */; }; - 23C6493C20396FFA00AA514A /* NotificationMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23C6493920396FFA00AA514A /* NotificationMergeTests.swift */; }; - 23D1056B20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D1056A20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift */; }; - 23D1056C20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D1056A20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift */; }; - 23D1056D20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D1056A20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift */; }; - 23DAFA2A1F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift */; }; - 23DAFA2B1F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift */; }; - 23DAFA2C1F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift */; }; - 23EEF5451F962E5700A2E72F /* Car.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EEF5441F962E5700A2E72F /* Car.swift */; }; - 23EEF5461F962E5700A2E72F /* Car.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EEF5441F962E5700A2E72F /* Car.swift */; }; - 23EEF5471F962E5700A2E72F /* Car.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EEF5441F962E5700A2E72F /* Car.swift */; }; - 23EEF5491F962E6A00A2E72F /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EEF5481F962E6A00A2E72F /* Person.swift */; }; - 23EEF54A1F962E6A00A2E72F /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EEF5481F962E6A00A2E72F /* Person.swift */; }; - 23EEF54B1F962E6A00A2E72F /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EEF5481F962E6A00A2E72F /* Person.swift */; }; - 23EFDE921F95FE730038BE75 /* CoreDataPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 23B5806A1F94FEDF00A365C0 /* CoreDataPlus.framework */; }; - 23EFDEA11F95FE990038BE75 /* CoreDataPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 236350811F95EC3000B3A16A /* CoreDataPlus.framework */; }; - 23EFDEB01F95FEB50038BE75 /* CoreDataPlus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2363509D1F95EC5600B3A16A /* CoreDataPlus.framework */; }; - 23F8E9E01F965B2000C65565 /* SampleModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA231F9641BD0058654B /* SampleModel.xcdatamodeld */; }; - 23F8E9E11F965B2100C65565 /* SampleModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA231F9641BD0058654B /* SampleModel.xcdatamodeld */; }; - 23F8E9E21F965B2200C65565 /* SampleModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 23DAFA231F9641BD0058654B /* SampleModel.xcdatamodeld */; }; - 23FFB86222EC948C00391D40 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FFB86122EC948C00391D40 /* Utils.swift */; }; - 23FFB86322EC948C00391D40 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FFB86122EC948C00391D40 /* Utils.swift */; }; - 23FFB86422EC948C00391D40 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FFB86122EC948C00391D40 /* Utils.swift */; }; - D2011A112614E3D9001902FB /* NSBatchDeleteResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F32614E3D9001902FB /* NSBatchDeleteResult+Utils.swift */; }; - D2011A122614E3D9001902FB /* NSBatchDeleteResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F32614E3D9001902FB /* NSBatchDeleteResult+Utils.swift */; }; - D2011A132614E3D9001902FB /* NSBatchDeleteResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F32614E3D9001902FB /* NSBatchDeleteResult+Utils.swift */; }; - D2011A142614E3D9001902FB /* NSBatchDeleteResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F32614E3D9001902FB /* NSBatchDeleteResult+Utils.swift */; }; - D2011A152614E3D9001902FB /* NSManagedObjectContext+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F42614E3D9001902FB /* NSManagedObjectContext+Utils.swift */; }; - D2011A162614E3D9001902FB /* NSManagedObjectContext+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F42614E3D9001902FB /* NSManagedObjectContext+Utils.swift */; }; - D2011A172614E3D9001902FB /* NSManagedObjectContext+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F42614E3D9001902FB /* NSManagedObjectContext+Utils.swift */; }; - D2011A182614E3D9001902FB /* NSManagedObjectContext+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F42614E3D9001902FB /* NSManagedObjectContext+Utils.swift */; }; - D2011A192614E3D9001902FB /* NSBatchUpdateResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F52614E3D9001902FB /* NSBatchUpdateResult+Utils.swift */; }; - D2011A1A2614E3D9001902FB /* NSBatchUpdateResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F52614E3D9001902FB /* NSBatchUpdateResult+Utils.swift */; }; - D2011A1B2614E3D9001902FB /* NSBatchUpdateResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F52614E3D9001902FB /* NSBatchUpdateResult+Utils.swift */; }; - D2011A1C2614E3D9001902FB /* NSBatchUpdateResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F52614E3D9001902FB /* NSBatchUpdateResult+Utils.swift */; }; - D2011A1D2614E3D9001902FB /* NSBatchInsertResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F62614E3D9001902FB /* NSBatchInsertResult+Utils.swift */; }; - D2011A1E2614E3D9001902FB /* NSBatchInsertResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F62614E3D9001902FB /* NSBatchInsertResult+Utils.swift */; }; - D2011A1F2614E3D9001902FB /* NSBatchInsertResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F62614E3D9001902FB /* NSBatchInsertResult+Utils.swift */; }; - D2011A202614E3D9001902FB /* NSBatchInsertResult+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F62614E3D9001902FB /* NSBatchInsertResult+Utils.swift */; }; - D2011A212614E3D9001902FB /* Collection+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F72614E3D9001902FB /* Collection+CoreData.swift */; }; - D2011A222614E3D9001902FB /* Collection+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F72614E3D9001902FB /* Collection+CoreData.swift */; }; - D2011A232614E3D9001902FB /* Collection+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F72614E3D9001902FB /* Collection+CoreData.swift */; }; - D2011A242614E3D9001902FB /* Collection+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F72614E3D9001902FB /* Collection+CoreData.swift */; }; - D2011A252614E3D9001902FB /* CoreDataPlus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F82614E3D9001902FB /* CoreDataPlus.swift */; }; - D2011A262614E3D9001902FB /* CoreDataPlus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F82614E3D9001902FB /* CoreDataPlus.swift */; }; - D2011A272614E3D9001902FB /* CoreDataPlus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F82614E3D9001902FB /* CoreDataPlus.swift */; }; - D2011A282614E3D9001902FB /* CoreDataPlus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F82614E3D9001902FB /* CoreDataPlus.swift */; }; - D2011A292614E3D9001902FB /* FetchedResultsChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F92614E3D9001902FB /* FetchedResultsChanges.swift */; }; - D2011A2A2614E3D9001902FB /* FetchedResultsChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F92614E3D9001902FB /* FetchedResultsChanges.swift */; }; - D2011A2B2614E3D9001902FB /* FetchedResultsChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F92614E3D9001902FB /* FetchedResultsChanges.swift */; }; - D2011A2C2614E3D9001902FB /* FetchedResultsChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119F92614E3D9001902FB /* FetchedResultsChanges.swift */; }; - D2011A2D2614E3D9001902FB /* NSSet+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FA2614E3D9001902FB /* NSSet+CoreData.swift */; }; - D2011A2E2614E3D9001902FB /* NSSet+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FA2614E3D9001902FB /* NSSet+CoreData.swift */; }; - D2011A2F2614E3D9001902FB /* NSSet+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FA2614E3D9001902FB /* NSSet+CoreData.swift */; }; - D2011A302614E3D9001902FB /* NSSet+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FA2614E3D9001902FB /* NSSet+CoreData.swift */; }; - D2011A352614E3D9001902FB /* NSManagedObjectContext+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FC2614E3D9001902FB /* NSManagedObjectContext+History.swift */; }; - D2011A362614E3D9001902FB /* NSManagedObjectContext+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FC2614E3D9001902FB /* NSManagedObjectContext+History.swift */; }; - D2011A372614E3D9001902FB /* NSManagedObjectContext+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FC2614E3D9001902FB /* NSManagedObjectContext+History.swift */; }; - D2011A382614E3D9001902FB /* NSManagedObjectContext+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FC2614E3D9001902FB /* NSManagedObjectContext+History.swift */; }; - D2011A392614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FD2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift */; }; - D2011A3A2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FD2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift */; }; - D2011A3B2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FD2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift */; }; - D2011A3C2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FD2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift */; }; - D2011A3D2614E3D9001902FB /* NSEntityDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FE2614E3D9001902FB /* NSEntityDescription+Utils.swift */; }; - D2011A3E2614E3D9001902FB /* NSEntityDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FE2614E3D9001902FB /* NSEntityDescription+Utils.swift */; }; - D2011A3F2614E3D9001902FB /* NSEntityDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FE2614E3D9001902FB /* NSEntityDescription+Utils.swift */; }; - D2011A402614E3D9001902FB /* NSEntityDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FE2614E3D9001902FB /* NSEntityDescription+Utils.swift */; }; - D2011A412614E3D9001902FB /* NSManagedObject+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FF2614E3D9001902FB /* NSManagedObject+Utils.swift */; }; - D2011A422614E3D9001902FB /* NSManagedObject+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FF2614E3D9001902FB /* NSManagedObject+Utils.swift */; }; - D2011A432614E3D9001902FB /* NSManagedObject+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FF2614E3D9001902FB /* NSManagedObject+Utils.swift */; }; - D2011A442614E3D9001902FB /* NSManagedObject+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20119FF2614E3D9001902FB /* NSManagedObject+Utils.swift */; }; - D2011A452614E3D9001902FB /* NSFetchRequestResult+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A002614E3D9001902FB /* NSFetchRequestResult+CoreData.swift */; }; - D2011A462614E3D9001902FB /* NSFetchRequestResult+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A002614E3D9001902FB /* NSFetchRequestResult+CoreData.swift */; }; - D2011A472614E3D9001902FB /* NSFetchRequestResult+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A002614E3D9001902FB /* NSFetchRequestResult+CoreData.swift */; }; - D2011A482614E3D9001902FB /* NSFetchRequestResult+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A002614E3D9001902FB /* NSFetchRequestResult+CoreData.swift */; }; - D2011A492614E3D9001902FB /* ModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A022614E3D9001902FB /* ModelVersion.swift */; }; - D2011A4A2614E3D9001902FB /* ModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A022614E3D9001902FB /* ModelVersion.swift */; }; - D2011A4B2614E3D9001902FB /* ModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A022614E3D9001902FB /* ModelVersion.swift */; }; - D2011A4C2614E3D9001902FB /* ModelVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A022614E3D9001902FB /* ModelVersion.swift */; }; - D2011A512614E3D9001902FB /* MigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A042614E3D9001902FB /* MigrationStep.swift */; }; - D2011A522614E3D9001902FB /* MigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A042614E3D9001902FB /* MigrationStep.swift */; }; - D2011A532614E3D9001902FB /* MigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A042614E3D9001902FB /* MigrationStep.swift */; }; - D2011A542614E3D9001902FB /* MigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A042614E3D9001902FB /* MigrationStep.swift */; }; - D2011A552614E3D9001902FB /* Notification+Payloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A062614E3D9001902FB /* Notification+Payloads.swift */; }; - D2011A562614E3D9001902FB /* Notification+Payloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A062614E3D9001902FB /* Notification+Payloads.swift */; }; - D2011A572614E3D9001902FB /* Notification+Payloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A062614E3D9001902FB /* Notification+Payloads.swift */; }; - D2011A582614E3D9001902FB /* Notification+Payloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A062614E3D9001902FB /* Notification+Payloads.swift */; }; - D2011A592614E3D9001902FB /* Notification+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A072614E3D9001902FB /* Notification+Utils.swift */; }; - D2011A5A2614E3D9001902FB /* Notification+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A072614E3D9001902FB /* Notification+Utils.swift */; }; - D2011A5B2614E3D9001902FB /* Notification+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A072614E3D9001902FB /* Notification+Utils.swift */; }; - D2011A5C2614E3D9001902FB /* Notification+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A072614E3D9001902FB /* Notification+Utils.swift */; }; - D2011A5D2614E3D9001902FB /* NSFetchRequest+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A082614E3D9001902FB /* NSFetchRequest+Utils.swift */; }; - D2011A5E2614E3D9001902FB /* NSFetchRequest+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A082614E3D9001902FB /* NSFetchRequest+Utils.swift */; }; - D2011A5F2614E3D9001902FB /* NSFetchRequest+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A082614E3D9001902FB /* NSFetchRequest+Utils.swift */; }; - D2011A602614E3D9001902FB /* NSFetchRequest+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2011A082614E3D9001902FB /* NSFetchRequest+Utils.swift */; }; - D20F4BB82621E71500F0CB25 /* NSAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4BB72621E71500F0CB25 /* NSAttributeDescription+Utils.swift */; }; - D20F4BB92621E71500F0CB25 /* NSAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4BB72621E71500F0CB25 /* NSAttributeDescription+Utils.swift */; }; - D20F4BBA2621E71500F0CB25 /* NSAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4BB72621E71500F0CB25 /* NSAttributeDescription+Utils.swift */; }; - D20F4BBB2621E71500F0CB25 /* NSAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4BB72621E71500F0CB25 /* NSAttributeDescription+Utils.swift */; }; - D20F4C012621F3B400F0CB25 /* SampleModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4BC62621F3B000F0CB25 /* SampleModel2.swift */; }; - D20F4C0B2621F3B400F0CB25 /* SampleModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4BC62621F3B000F0CB25 /* SampleModel2.swift */; }; - D20F4C152621F3B500F0CB25 /* SampleModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4BC62621F3B000F0CB25 /* SampleModel2.swift */; }; - D20F4C202622029400F0CB25 /* ProgrammaticMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4C1F2622029400F0CB25 /* ProgrammaticMigrationTests.swift */; }; - D20F4C212622029400F0CB25 /* ProgrammaticMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4C1F2622029400F0CB25 /* ProgrammaticMigrationTests.swift */; }; - D20F4C222622029400F0CB25 /* ProgrammaticMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F4C1F2622029400F0CB25 /* ProgrammaticMigrationTests.swift */; }; - D214B9A5263B0FDE000BBD13 /* NSMappingModel+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A4263B0FDE000BBD13 /* NSMappingModel+Utils.swift */; }; - D214B9A6263B0FDE000BBD13 /* NSMappingModel+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A4263B0FDE000BBD13 /* NSMappingModel+Utils.swift */; }; - D214B9A7263B0FDE000BBD13 /* NSMappingModel+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A4263B0FDE000BBD13 /* NSMappingModel+Utils.swift */; }; - D214B9A8263B0FDE000BBD13 /* NSMappingModel+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A4263B0FDE000BBD13 /* NSMappingModel+Utils.swift */; }; - D214B9AA263BFA22000BBD13 /* Migrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A9263BFA22000BBD13 /* Migrator.swift */; }; - D214B9AB263BFA22000BBD13 /* Migrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A9263BFA22000BBD13 /* Migrator.swift */; }; - D214B9AC263BFA22000BBD13 /* Migrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A9263BFA22000BBD13 /* Migrator.swift */; }; - D214B9AD263BFA22000BBD13 /* Migrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B9A9263BFA22000BBD13 /* Migrator.swift */; }; - D27A2E36255F29D20043B43F /* SampleModelV2.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D27A2E2D255F29D20043B43F /* SampleModelV2.sqlite */; }; - D27A2E37255F29D20043B43F /* SampleModelV2.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D27A2E2D255F29D20043B43F /* SampleModelV2.sqlite */; }; - D27A2E38255F29D20043B43F /* SampleModelV2.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D27A2E2D255F29D20043B43F /* SampleModelV2.sqlite */; }; - D282323B2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282323A2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift */; }; - D282323C2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282323A2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift */; }; - D282323D2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282323A2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift */; }; - D282325B26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282325A26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift */; }; - D282325C26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282325A26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift */; }; - D282325D26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282325A26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift */; }; - D282326926244D3A00F37442 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282326826244D3A00F37442 /* Author.swift */; }; - D282326A26244D3A00F37442 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282326826244D3A00F37442 /* Author.swift */; }; - D282326B26244D3A00F37442 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282326826244D3A00F37442 /* Author.swift */; }; - D282327F2624569400F37442 /* Book.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282327E2624569400F37442 /* Book.swift */; }; - D28232802624569400F37442 /* Book.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282327E2624569400F37442 /* Book.swift */; }; - D28232812624569400F37442 /* Book.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282327E2624569400F37442 /* Book.swift */; }; - D28232952624754700F37442 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232942624754700F37442 /* Page.swift */; }; - D28232962624754700F37442 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232942624754700F37442 /* Page.swift */; }; - D28232972624754700F37442 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232942624754700F37442 /* Page.swift */; }; - D28232AC26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232AB26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift */; }; - D28232AD26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232AB26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift */; }; - D28232AE26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232AB26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift */; }; - D28232AF26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232AB26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift */; }; - D28232C32624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232C22624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift */; }; - D28232C42624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232C22624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift */; }; - D28232C52624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28232C22624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift */; }; - D282330F2624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282330E2624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift */; }; - D28233102624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282330E2624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift */; }; - D28233112624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282330E2624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift */; }; - D28233122624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282330E2624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift */; }; - D282331F26257A7400F37442 /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282331E26257A7400F37442 /* Feedback.swift */; }; - D282332026257A7400F37442 /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282331E26257A7400F37442 /* Feedback.swift */; }; - D282332126257A7400F37442 /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282331E26257A7400F37442 /* Feedback.swift */; }; - D282332C2625AB2300F37442 /* CoreDataErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282332B2625AB2300F37442 /* CoreDataErrors.swift */; }; - D282332D2625AB2300F37442 /* CoreDataErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282332B2625AB2300F37442 /* CoreDataErrors.swift */; }; - D282332E2625AB2300F37442 /* CoreDataErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282332B2625AB2300F37442 /* CoreDataErrors.swift */; }; - D28233422625C23F00F37442 /* SampleModel2+V1.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233412625C23F00F37442 /* SampleModel2+V1.swift */; }; - D28233432625C23F00F37442 /* SampleModel2+V1.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233412625C23F00F37442 /* SampleModel2+V1.swift */; }; - D28233442625C23F00F37442 /* SampleModel2+V1.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233412625C23F00F37442 /* SampleModel2+V1.swift */; }; - D282334F2625C2A200F37442 /* SampleModel2+V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282334E2625C2A200F37442 /* SampleModel2+V2.swift */; }; - D28233502625C2A200F37442 /* SampleModel2+V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282334E2625C2A200F37442 /* SampleModel2+V2.swift */; }; - D28233512625C2A200F37442 /* SampleModel2+V2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282334E2625C2A200F37442 /* SampleModel2+V2.swift */; }; - D28233652625C55100F37442 /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233642625C55100F37442 /* Content.swift */; }; - D28233662625C55100F37442 /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233642625C55100F37442 /* Content.swift */; }; - D28233672625C55100F37442 /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233642625C55100F37442 /* Content.swift */; }; - D28233722625C56A00F37442 /* Cover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233712625C56A00F37442 /* Cover.swift */; }; - D28233732625C56A00F37442 /* Cover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233712625C56A00F37442 /* Cover.swift */; }; - D28233742625C56A00F37442 /* Cover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28233712625C56A00F37442 /* Cover.swift */; }; - D29AA4E324D99DEA005CE7F4 /* NSManagedObject+UpdateTimestampable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808B1F94FFF600A365C0 /* NSManagedObject+UpdateTimestampable.swift */; }; - D29AA4E424D99DEA005CE7F4 /* NSManagedObject+UpdateTimestampable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808B1F94FFF600A365C0 /* NSManagedObject+UpdateTimestampable.swift */; }; - D29AA4E524D99DEA005CE7F4 /* NSManagedObject+UpdateTimestampable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808B1F94FFF600A365C0 /* NSManagedObject+UpdateTimestampable.swift */; }; - D29AA4E624D99DEA005CE7F4 /* NSManagedObject+DelayedDeletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808D1F94FFF600A365C0 /* NSManagedObject+DelayedDeletable.swift */; }; - D29AA4E724D99DEA005CE7F4 /* NSManagedObject+DelayedDeletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808D1F94FFF600A365C0 /* NSManagedObject+DelayedDeletable.swift */; }; - D29AA4E824D99DEA005CE7F4 /* NSManagedObject+DelayedDeletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23B5808D1F94FFF600A365C0 /* NSManagedObject+DelayedDeletable.swift */; }; - D2A448252631B937003059A7 /* SampleModel2+V3.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A448242631B937003059A7 /* SampleModel2+V3.swift */; }; - D2A448262631B937003059A7 /* SampleModel2+V3.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A448242631B937003059A7 /* SampleModel2+V3.swift */; }; - D2A448272631B937003059A7 /* SampleModel2+V3.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A448242631B937003059A7 /* SampleModel2+V3.swift */; }; - D2B7E1552448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift */; }; - D2B7E1562448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift */; }; - D2B7E1572448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift */; }; - D2C5CE36244762FA00AD19D8 /* NSSetCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CE35244762FA00AD19D8 /* NSSetCoreDataTests.swift */; }; - D2C5CE37244762FA00AD19D8 /* NSSetCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CE35244762FA00AD19D8 /* NSSetCoreDataTests.swift */; }; - D2C5CE38244762FA00AD19D8 /* NSSetCoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5CE35244762FA00AD19D8 /* NSSetCoreDataTests.swift */; }; + 06DED80B2BF36795005EFEA5 /* SampleModel3.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 06DED8072BF36795005EFEA5 /* SampleModel3.xcdatamodeld */; }; + 06F9DDDF2BF3627900D80B8F /* SampleModel3_V1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 06F9DDD02BF3627900D80B8F /* SampleModel3_V1.sqlite */; }; + 06F9DDE12BF3627900D80B8F /* SampleModel3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F9DDD32BF3627900D80B8F /* SampleModel3.swift */; }; + 06F9DDE32BF3627900D80B8F /* SampleModelVersion3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F9DDD52BF3627900D80B8F /* SampleModelVersion3.swift */; }; + D214B17C2BECF67C0090F18A /* StagedMigrationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B17B2BECF67C0090F18A /* StagedMigrationStep.swift */; }; + D214B1812BEE088B0090F18A /* V2toV3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 2377287021490B9D00FDAF32 /* V2toV3.xcmappingmodel */; }; + D214B1DB2BEECAE90090F18A /* SampleModel2_V1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D214B1D82BEECAE90090F18A /* SampleModel2_V1.sqlite */; }; + D214B1E02BEECAE90090F18A /* SampleModel2_V2.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D214B1D92BEECAE90090F18A /* SampleModel2_V2.sqlite */; }; + D214B1E62BF23E4B0090F18A /* StagedMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B1E52BF23E4B0090F18A /* StagedMigration.swift */; }; + D214B1EC2BF23E5A0090F18A /* LegacyMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214B1EB2BF23E5A0090F18A /* LegacyMigration.swift */; }; + D262F0DD2BECBE1D006C57A6 /* StagedMigrations_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D262F0DC2BECBE1D006C57A6 /* StagedMigrations_Tests.swift */; }; + D2DEC77D2B626D38006EA172 /* NSCompositeAttributeDescription+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DEC77C2B626D38006EA172 /* NSCompositeAttributeDescription+Utils.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 06968AD426454AFF00088D76 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 23B580611F94FEDF00A365C0 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 236350731F95EC1A00B3A16A; - remoteInfo = "CoreDataPlus watchOS"; - }; 06AF615126F091370090A61B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 23B580611F94FEDF00A365C0 /* Project object */; @@ -479,27 +148,6 @@ remoteGlobalIDString = 06AF614726F091370090A61B; remoteInfo = CoreDataPlus; }; - 23EFDE931F95FE730038BE75 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 23B580611F94FEDF00A365C0 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 23B580691F94FEDF00A365C0; - remoteInfo = "CoreDataPlus iOS"; - }; - 23EFDEA21F95FE990038BE75 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 23B580611F94FEDF00A365C0 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 236350801F95EC3000B3A16A; - remoteInfo = "CoreDataPlus tvOS"; - }; - 23EFDEB11F95FEB50038BE75 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 23B580611F94FEDF00A365C0 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 2363509C1F95EC5600B3A16A; - remoteInfo = "CoreDataPlus macOS"; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -508,70 +156,66 @@ 061C689226209443000BF0A2 /* CustomTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTransformer.swift; sourceTree = ""; }; 063E100D264BC2F90050E84C /* Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deprecated.swift; sourceTree = ""; }; 065094052643E8BC00B3EE12 /* MigrationProgressReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationProgressReporter.swift; sourceTree = ""; }; - 0650940A2643E8EB00B3EE12 /* LightweightMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightweightMigrationManager.swift; sourceTree = ""; }; 0693F915237C58A7008E4F31 /* workflows */ = {isa = PBXFileReference; lastKnownFileType = text; name = workflows; path = .github/workflows; sourceTree = SOURCE_ROOT; }; - 06968ACE26454AFF00088D76 /* CoreDataPlus Tests watchOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CoreDataPlus Tests watchOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 06968AD926454BA300088D76 /* CoreDataPlus watchOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CoreDataPlus watchOS.xctestplan"; sourceTree = ""; }; 06968B0C26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookCoverToCoverMigrationPolicy.swift; sourceTree = ""; }; 06968B112645685F00088D76 /* FeedbackMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackMigrationManager.swift; sourceTree = ""; }; + 06A1D11E2BF34C6300F62CA1 /* LegacyMigrationStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyMigrationStep.swift; sourceTree = ""; }; 06A3C1D3239FD36100E08D45 /* CoreDataPlus macOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CoreDataPlus macOS.xctestplan"; sourceTree = ""; }; 06A3C1D4239FD3E900E08D45 /* CoreDataPlus tvOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CoreDataPlus tvOS.xctestplan"; sourceTree = ""; }; 06A3C1D5239FD83D00E08D45 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = SOURCE_ROOT; }; 06A4584E239FD22C007BA7C9 /* CoreDataPlus iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CoreDataPlus iOS.xctestplan"; sourceTree = ""; }; 06A6CD9F261DE0E7000563F0 /* Transformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transformer.swift; sourceTree = ""; }; - 06A6CDC6261DFC8F000563F0 /* TransformerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformerTests.swift; sourceTree = ""; }; - 06A6CE25261E0961000563F0 /* SampleModelV1.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = SampleModelV1.sqlite; sourceTree = ""; }; + 06A6CDC6261DFC8F000563F0 /* Transformer_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transformer_Tests.swift; sourceTree = ""; }; + 06A6CE25261E0961000563F0 /* SampleModel_V1.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = SampleModel_V1.sqlite; sourceTree = ""; }; 06AF614826F091370090A61B /* CoreDataPlus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataPlus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 06AF614F26F091370090A61B /* CoreDataPlus Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CoreDataPlus Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 06AF61B126F095070090A61B /* CoreDataPlus.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CoreDataPlus.xctestplan; sourceTree = ""; }; - 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgrammaticallyDefinedModelTests.swift; sourceTree = ""; }; + 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgrammaticallyDefinedModel_Tests.swift; sourceTree = ""; }; 06B71CB72629DB04008DFD11 /* PersistentStoreOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentStoreOptions.swift; sourceTree = ""; }; - 06E06365264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchesWithAffectedStoresTests.swift; sourceTree = ""; }; + 06DED8082BF36795005EFEA5 /* SampleModel3_v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SampleModel3_v2.xcdatamodel; sourceTree = ""; }; + 06DED8092BF36795005EFEA5 /* SampleModel3_v3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SampleModel3_v3.xcdatamodel; sourceTree = ""; }; + 06DED80A2BF36795005EFEA5 /* SampleModel3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SampleModel3.xcdatamodel; sourceTree = ""; }; + 06DED80C2BF36C9F005EFEA5 /* SampleModel3.momd */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SampleModel3.momd; sourceTree = ""; }; + 06E06365264ABC0D009145B6 /* FetchesWithAffectedStores_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchesWithAffectedStores_Tests.swift; sourceTree = ""; }; 06F7D55C26E5F85D00929CA6 /* NSPredicate+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPredicate+Utils.swift"; sourceTree = ""; }; - 06F7D56626E5F8DB00929CA6 /* NSPredicateUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPredicateUtilsTests.swift; sourceTree = ""; }; + 06F7D56626E5F8DB00929CA6 /* NSPredicateUtils_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPredicateUtils_Tests.swift; sourceTree = ""; }; + 06F9DDD02BF3627900D80B8F /* SampleModel3_V1.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = SampleModel3_V1.sqlite; sourceTree = ""; }; + 06F9DDD32BF3627900D80B8F /* SampleModel3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleModel3.swift; sourceTree = ""; }; + 06F9DDD52BF3627900D80B8F /* SampleModelVersion3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleModelVersion3.swift; sourceTree = ""; }; 230C087A214179D400A1B6CB /* SampleModel2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SampleModel2.xcdatamodel; sourceTree = ""; }; - 230C087B21417E1900A1B6CB /* MigrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationsTests.swift; sourceTree = ""; }; + 230C087B21417E1900A1B6CB /* Migrations_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migrations_Tests.swift; sourceTree = ""; }; 231C313E22F5E3BB00BF7129 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; - 232440B622D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextInvestigationTests.swift; sourceTree = ""; }; + 232440B622D2546100A04649 /* NSManagedObjectContextInvestigation_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextInvestigation_Tests.swift; sourceTree = ""; }; 233654361F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+SampleModel.swift"; sourceTree = ""; }; 234DA7AA214A5D2B00D5C24F /* Maker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Maker.swift; sourceTree = ""; }; - 2351687122A2DD4500340611 /* NotificationPayloadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPayloadTests.swift; sourceTree = ""; }; - 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextHistoryTests.swift; sourceTree = ""; }; - 236350741F95EC1A00B3A16A /* CoreDataPlus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataPlus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 236350811F95EC3000B3A16A /* CoreDataPlus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataPlus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 236350841F95EC3000B3A16A /* Info-tvOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-tvOS.plist"; sourceTree = ""; }; - 2363509D1F95EC5600B3A16A /* CoreDataPlus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataPlus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 236350E11F95F1A900B3A16A /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Tests.plist"; sourceTree = ""; }; - 236351031F95F28B00B3A16A /* NSManagedObjectContextUtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextUtilsTests.swift; sourceTree = ""; }; - 236351041F95F28B00B3A16A /* ModelVersionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelVersionTests.swift; sourceTree = ""; }; - 236351061F95F28C00B3A16A /* NSManagedObjectUtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectUtilsTests.swift; sourceTree = ""; }; - 236351071F95F28C00B3A16A /* NSFetchRequestUtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSFetchRequestUtilsTests.swift; sourceTree = ""; }; + 2351687122A2DD4500340611 /* NotificationPayload_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPayload_Tests.swift; sourceTree = ""; }; + 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistory_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextHistory_Tests.swift; sourceTree = ""; }; + 236351031F95F28B00B3A16A /* NSManagedObjectContextUtils_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextUtils_Tests.swift; sourceTree = ""; }; + 236351041F95F28B00B3A16A /* ModelVersion_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelVersion_Tests.swift; sourceTree = ""; }; + 236351061F95F28C00B3A16A /* NSManagedObjectUtils_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectUtils_Tests.swift; sourceTree = ""; }; + 236351071F95F28C00B3A16A /* NSFetchRequestUtils_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSFetchRequestUtils_Tests.swift; sourceTree = ""; }; 2363510B1F95F28C00B3A16A /* SampleModelVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleModelVersion.swift; sourceTree = ""; }; 236351241F95F53900B3A16A /* IDETemplateMacros.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = IDETemplateMacros.plist; path = CoreDataPlus.xcodeproj/xcshareddata/IDETemplateMacros.plist; sourceTree = SOURCE_ROOT; }; 236526AA215A46C200A51C9F /* InMemoryTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryTestCase.swift; sourceTree = ""; }; 237728642148FACE00FDAF32 /* V2to3MakerPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2to3MakerPolicy.swift; sourceTree = ""; }; 2377287021490B9D00FDAF32 /* V2toV3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = V2toV3.xcmappingmodel; sourceTree = ""; }; - 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectDelayedDeletableTests.swift; sourceTree = ""; }; - 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEntityDescriptionUtilsTests.swift; sourceTree = ""; }; + 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletable_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectDelayedDeletable_Tests.swift; sourceTree = ""; }; + 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtils_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEntityDescriptionUtils_Tests.swift; sourceTree = ""; }; 239F4CBF22F49F2F007888BA /* OnDiskTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDiskTestCase.swift; sourceTree = ""; }; 23A38AE72147C08E0086C22A /* SampleModel3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SampleModel3.xcdatamodel; sourceTree = ""; }; - 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFetchRequestResultCoreDataTests.swift; sourceTree = ""; }; - 23B5806A1F94FEDF00A365C0 /* CoreDataPlus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataPlus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 23B5806D1F94FEDF00A365C0 /* CoreDataPlus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataPlus.h; sourceTree = ""; }; + 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreData_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFetchRequestResultCoreData_Tests.swift; sourceTree = ""; }; 23B5806E1F94FEDF00A365C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23B5808B1F94FFF600A365C0 /* NSManagedObject+UpdateTimestampable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+UpdateTimestampable.swift"; sourceTree = ""; }; 23B5808D1F94FFF600A365C0 /* NSManagedObject+DelayedDeletable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+DelayedDeletable.swift"; sourceTree = ""; }; 23BAC1181FA7A7D8002EE4F1 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; - 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectUpdateTimestampableTests.swift; sourceTree = ""; }; - 23C6493920396FFA00AA514A /* NotificationMergeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMergeTests.swift; sourceTree = ""; }; - 23D1056A20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchedResultsChangesTests.swift; sourceTree = ""; }; + 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampable_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectUpdateTimestampable_Tests.swift; sourceTree = ""; }; + 23C6493920396FFA00AA514A /* NotificationMerge_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMerge_Tests.swift; sourceTree = ""; }; + 23D1056A20D7EFEF00AE84CC /* FetchedResultsChanges_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchedResultsChanges_Tests.swift; sourceTree = ""; }; 23DAFA241F9641BD0058654B /* SampleModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SampleModel.xcdatamodel; sourceTree = ""; }; - 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFetchRequestResultUtilsTests.swift; sourceTree = ""; }; + 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtils_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFetchRequestResultUtils_Tests.swift; sourceTree = ""; }; 23EEF5441F962E5700A2E72F /* Car.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Car.swift; sourceTree = ""; }; 23EEF5481F962E6A00A2E72F /* Person.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = ""; }; - 23EFDE8D1F95FE730038BE75 /* CoreDataPlus Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CoreDataPlus Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 23EFDE9C1F95FE990038BE75 /* CoreDataPlus Tests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CoreDataPlus Tests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 23EFDEAB1F95FEB40038BE75 /* CoreDataPlus Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CoreDataPlus Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 23FFB86122EC948C00391D40 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; D20119F32614E3D9001902FB /* NSBatchDeleteResult+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSBatchDeleteResult+Utils.swift"; sourceTree = ""; }; D20119F42614E3D9001902FB /* NSManagedObjectContext+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Utils.swift"; sourceTree = ""; }; @@ -587,20 +231,25 @@ D20119FF2614E3D9001902FB /* NSManagedObject+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Utils.swift"; sourceTree = ""; }; D2011A002614E3D9001902FB /* NSFetchRequestResult+CoreData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSFetchRequestResult+CoreData.swift"; sourceTree = ""; }; D2011A022614E3D9001902FB /* ModelVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelVersion.swift; sourceTree = ""; }; - D2011A042614E3D9001902FB /* MigrationStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationStep.swift; sourceTree = ""; }; D2011A062614E3D9001902FB /* Notification+Payloads.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+Payloads.swift"; sourceTree = ""; }; D2011A072614E3D9001902FB /* Notification+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+Utils.swift"; sourceTree = ""; }; D2011A082614E3D9001902FB /* NSFetchRequest+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSFetchRequest+Utils.swift"; sourceTree = ""; }; D20F4BB72621E71500F0CB25 /* NSAttributeDescription+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributeDescription+Utils.swift"; sourceTree = ""; }; D20F4BC62621F3B000F0CB25 /* SampleModel2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleModel2.swift; sourceTree = ""; }; - D20F4C1F2622029400F0CB25 /* ProgrammaticMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgrammaticMigrationTests.swift; sourceTree = ""; }; + D20F4C1F2622029400F0CB25 /* ProgrammaticMigration_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgrammaticMigration_Tests.swift; sourceTree = ""; }; + D214B17B2BECF67C0090F18A /* StagedMigrationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StagedMigrationStep.swift; sourceTree = ""; }; + D214B1D82BEECAE90090F18A /* SampleModel2_V1.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = SampleModel2_V1.sqlite; sourceTree = ""; }; + D214B1D92BEECAE90090F18A /* SampleModel2_V2.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = SampleModel2_V2.sqlite; sourceTree = ""; }; + D214B1E52BF23E4B0090F18A /* StagedMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StagedMigration.swift; sourceTree = ""; }; + D214B1EB2BF23E5A0090F18A /* LegacyMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyMigration.swift; sourceTree = ""; }; D214B9A4263B0FDE000BBD13 /* NSMappingModel+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMappingModel+Utils.swift"; sourceTree = ""; }; D214B9A9263BFA22000BBD13 /* Migrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migrator.swift; sourceTree = ""; }; D254E22A23C3BA7F00A1586E /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = SOURCE_ROOT; }; - D27A2E2D255F29D20043B43F /* SampleModelV2.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = SampleModelV2.sqlite; sourceTree = ""; }; + D262F0DC2BECBE1D006C57A6 /* StagedMigrations_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StagedMigrations_Tests.swift; sourceTree = ""; }; + D27A2E2D255F29D20043B43F /* SampleModel_V2.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = SampleModel_V2.sqlite; sourceTree = ""; }; D27A2E2E255F29D20043B43F /* V2toV3.cdm */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = V2toV3.cdm; sourceTree = ""; }; D27A2E9F255F2A3C0043B43F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - D282323A2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributeDescriptionUtilsTests.swift; sourceTree = ""; }; + D282323A2624342400F37442 /* NSAttributeDescriptionUtils_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributeDescriptionUtils_Tests.swift; sourceTree = ""; }; D282325A26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+SampleModel2.swift"; sourceTree = ""; }; D282326826244D3A00F37442 /* Author.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = ""; }; D282327E2624569400F37442 /* Book.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Book.swift; sourceTree = ""; }; @@ -615,19 +264,12 @@ D28233642625C55100F37442 /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = ""; }; D28233712625C56A00F37442 /* Cover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cover.swift; sourceTree = ""; }; D2A448242631B937003059A7 /* SampleModel2+V3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SampleModel2+V3.swift"; sourceTree = ""; }; - D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPersistentStoreCoordinatorUtilsTests.swift; sourceTree = ""; }; - D2C5CE35244762FA00AD19D8 /* NSSetCoreDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSetCoreDataTests.swift; sourceTree = ""; }; + D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtils_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPersistentStoreCoordinatorUtils_Tests.swift; sourceTree = ""; }; + D2C5CE35244762FA00AD19D8 /* NSSetCoreData_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSetCoreData_Tests.swift; sourceTree = ""; }; + D2DEC77C2B626D38006EA172 /* NSCompositeAttributeDescription+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSCompositeAttributeDescription+Utils.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 06968ACB26454AFF00088D76 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 06968AD326454AFF00088D76 /* CoreDataPlus.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 06AF614526F091370090A61B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -643,58 +285,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 236350701F95EC1A00B3A16A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2363507D1F95EC3000B3A16A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 236350991F95EC5600B3A16A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23B580661F94FEDF00A365C0 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDE8A1F95FE730038BE75 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 23EFDE921F95FE730038BE75 /* CoreDataPlus.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDE991F95FE990038BE75 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 23EFDEA11F95FE990038BE75 /* CoreDataPlus.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDEA81F95FEB40038BE75 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 23EFDEB01F95FEB50038BE75 /* CoreDataPlus.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -719,6 +309,26 @@ path = TestPlans; sourceTree = ""; }; + 06F9DDD12BF3627900D80B8F /* Fixtures */ = { + isa = PBXGroup; + children = ( + 06DED80C2BF36C9F005EFEA5 /* SampleModel3.momd */, + 06F9DDD02BF3627900D80B8F /* SampleModel3_V1.sqlite */, + ); + path = Fixtures; + sourceTree = ""; + }; + 06F9DDD62BF3627900D80B8F /* SampleModel3 */ = { + isa = PBXGroup; + children = ( + 06F9DDD12BF3627900D80B8F /* Fixtures */, + 06F9DDD32BF3627900D80B8F /* SampleModel3.swift */, + 06DED8072BF36795005EFEA5 /* SampleModel3.xcdatamodeld */, + 06F9DDD52BF3627900D80B8F /* SampleModelVersion3.swift */, + ); + path = SampleModel3; + sourceTree = ""; + }; 2325898122F34FDD00E39FEA /* Utils */ = { isa = PBXGroup; children = ( @@ -767,14 +377,6 @@ 23B5806B1F94FEDF00A365C0 /* Products */ = { isa = PBXGroup; children = ( - 23B5806A1F94FEDF00A365C0 /* CoreDataPlus.framework */, - 236350741F95EC1A00B3A16A /* CoreDataPlus.framework */, - 236350811F95EC3000B3A16A /* CoreDataPlus.framework */, - 2363509D1F95EC5600B3A16A /* CoreDataPlus.framework */, - 23EFDE8D1F95FE730038BE75 /* CoreDataPlus Tests iOS.xctest */, - 23EFDE9C1F95FE990038BE75 /* CoreDataPlus Tests tvOS.xctest */, - 23EFDEAB1F95FEB40038BE75 /* CoreDataPlus Tests macOS.xctest */, - 06968ACE26454AFF00088D76 /* CoreDataPlus Tests watchOS.xctest */, 06AF614826F091370090A61B /* CoreDataPlus.framework */, 06AF614F26F091370090A61B /* CoreDataPlus Tests.xctest */, ); @@ -785,10 +387,7 @@ isa = PBXGroup; children = ( 06A3C1D5239FD83D00E08D45 /* CHANGELOG.md */, - 23B5806D1F94FEDF00A365C0 /* CoreDataPlus.h */, 236351241F95F53900B3A16A /* IDETemplateMacros.plist */, - 236350E11F95F1A900B3A16A /* Info-Tests.plist */, - 236350841F95EC3000B3A16A /* Info-tvOS.plist */, 23B5806E1F94FEDF00A365C0 /* Info.plist */, D254E22A23C3BA7F00A1586E /* LICENSE.md */, 23BAC1181FA7A7D8002EE4F1 /* Package.swift */, @@ -808,6 +407,7 @@ D20119F32614E3D9001902FB /* NSBatchDeleteResult+Utils.swift */, D20119F62614E3D9001902FB /* NSBatchInsertResult+Utils.swift */, D20119F52614E3D9001902FB /* NSBatchUpdateResult+Utils.swift */, + D2DEC77C2B626D38006EA172 /* NSCompositeAttributeDescription+Utils.swift */, D28232AB26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift */, D20119FE2614E3D9001902FB /* NSEntityDescription+Utils.swift */, D282330E2624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift */, @@ -831,27 +431,28 @@ 23B580861F94FF0E00A365C0 /* Tests */ = { isa = PBXGroup; children = ( - 23D1056A20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift */, - 06E06365264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift */, - 230C087B21417E1900A1B6CB /* MigrationsTests.swift */, - 236351041F95F28B00B3A16A /* ModelVersionTests.swift */, - D282323A2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift */, - 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift */, - 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift */, - 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift */, - 236351071F95F28C00B3A16A /* NSFetchRequestUtilsTests.swift */, - 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift */, - 232440B622D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift */, - 236351031F95F28B00B3A16A /* NSManagedObjectContextUtilsTests.swift */, - 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift */, - 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift */, - 236351061F95F28C00B3A16A /* NSManagedObjectUtilsTests.swift */, - D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift */, - 06F7D56626E5F8DB00929CA6 /* NSPredicateUtilsTests.swift */, - D2C5CE35244762FA00AD19D8 /* NSSetCoreDataTests.swift */, - 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift */, - D20F4C1F2622029400F0CB25 /* ProgrammaticMigrationTests.swift */, - 06A6CDC6261DFC8F000563F0 /* TransformerTests.swift */, + 23D1056A20D7EFEF00AE84CC /* FetchedResultsChanges_Tests.swift */, + 06E06365264ABC0D009145B6 /* FetchesWithAffectedStores_Tests.swift */, + 230C087B21417E1900A1B6CB /* Migrations_Tests.swift */, + 236351041F95F28B00B3A16A /* ModelVersion_Tests.swift */, + D282323A2624342400F37442 /* NSAttributeDescriptionUtils_Tests.swift */, + 23948D1E1F9934F800B3738D /* NSEntityDescriptionUtils_Tests.swift */, + 23A8A44C1F9F536D0038DE3A /* NSFetchRequestResultCoreData_Tests.swift */, + 23DAFA291F964DBB0058654B /* NSFetchRequestResultUtils_Tests.swift */, + 236351071F95F28C00B3A16A /* NSFetchRequestUtils_Tests.swift */, + 23541AF922ECAB0000678A96 /* NSManagedObjectContextHistory_Tests.swift */, + 232440B622D2546100A04649 /* NSManagedObjectContextInvestigation_Tests.swift */, + 236351031F95F28B00B3A16A /* NSManagedObjectContextUtils_Tests.swift */, + 2383AC631FB079420085625C /* NSManagedObjectDelayedDeletable_Tests.swift */, + 23C3B2B51FB0A1C000799E72 /* NSManagedObjectUpdateTimestampable_Tests.swift */, + 236351061F95F28C00B3A16A /* NSManagedObjectUtils_Tests.swift */, + D2B7E1542448BDEA001763BF /* NSPersistentStoreCoordinatorUtils_Tests.swift */, + 06F7D56626E5F8DB00929CA6 /* NSPredicateUtils_Tests.swift */, + D2C5CE35244762FA00AD19D8 /* NSSetCoreData_Tests.swift */, + 06B71C732629CB4F008DFD11 /* ProgrammaticallyDefinedModel_Tests.swift */, + D20F4C1F2622029400F0CB25 /* ProgrammaticMigration_Tests.swift */, + D262F0DC2BECBE1D006C57A6 /* StagedMigrations_Tests.swift */, + 06A6CDC6261DFC8F000563F0 /* Transformer_Tests.swift */, D23A9E3824E2D4DF006C2964 /* Notifications */, D2708842255EF13600309E7A /* Resources */, 06A4584D239FD20C007BA7C9 /* TestPlans */, @@ -876,11 +477,13 @@ D2011A012614E3D9001902FB /* Migration */ = { isa = PBXGroup; children = ( - D2011A022614E3D9001902FB /* ModelVersion.swift */, - D2011A042614E3D9001902FB /* MigrationStep.swift */, - D214B9A9263BFA22000BBD13 /* Migrator.swift */, + D214B1EB2BF23E5A0090F18A /* LegacyMigration.swift */, + 06A1D11E2BF34C6300F62CA1 /* LegacyMigrationStep.swift */, 065094052643E8BC00B3EE12 /* MigrationProgressReporter.swift */, - 0650940A2643E8EB00B3EE12 /* LightweightMigrationManager.swift */, + D214B9A9263BFA22000BBD13 /* Migrator.swift */, + D2011A022614E3D9001902FB /* ModelVersion.swift */, + D214B1E52BF23E4B0090F18A /* StagedMigration.swift */, + D214B17B2BECF67C0090F18A /* StagedMigrationStep.swift */, ); path = Migration; sourceTree = ""; @@ -897,6 +500,7 @@ D20F4BC52621F39600F0CB25 /* SampleModel2 */ = { isa = PBXGroup; children = ( + D214B1DA2BEECAE90090F18A /* Fixtures */, D282326726244D2D00F37442 /* Entities */, D282325A26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift */, D20F4BC62621F3B000F0CB25 /* SampleModel2.swift */, @@ -909,11 +513,20 @@ path = SampleModel2; sourceTree = ""; }; + D214B1DA2BEECAE90090F18A /* Fixtures */ = { + isa = PBXGroup; + children = ( + D214B1D82BEECAE90090F18A /* SampleModel2_V1.sqlite */, + D214B1D92BEECAE90090F18A /* SampleModel2_V2.sqlite */, + ); + path = Fixtures; + sourceTree = ""; + }; D23A9E3824E2D4DF006C2964 /* Notifications */ = { isa = PBXGroup; children = ( - 23C6493920396FFA00AA514A /* NotificationMergeTests.swift */, - 2351687122A2DD4500340611 /* NotificationPayloadTests.swift */, + 23C6493920396FFA00AA514A /* NotificationMerge_Tests.swift */, + 2351687122A2DD4500340611 /* NotificationPayload_Tests.swift */, ); path = Notifications; sourceTree = ""; @@ -923,6 +536,7 @@ children = ( 23C660BA22F3542F00A3EB59 /* SampleModel */, D20F4BC52621F39600F0CB25 /* SampleModel2 */, + 06F9DDD62BF3627900D80B8F /* SampleModel3 */, ); path = Resources; sourceTree = ""; @@ -931,9 +545,9 @@ isa = PBXGroup; children = ( 061C6843261E100E000BF0A2 /* SampleModel.momd */, - 06A6CE25261E0961000563F0 /* SampleModelV1.sqlite */, + 06A6CE25261E0961000563F0 /* SampleModel_V1.sqlite */, D27A2E9F255F2A3C0043B43F /* README.md */, - D27A2E2D255F29D20043B43F /* SampleModelV2.sqlite */, + D27A2E2D255F29D20043B43F /* SampleModel_V2.sqlite */, D27A2E2E255F29D20043B43F /* V2toV3.cdm */, ); path = Fixtures; @@ -954,73 +568,11 @@ }; /* End PBXGroup section */ -/* Begin PBXHeadersBuildPhase section */ - 06AF614326F091370090A61B /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 06AF615D26F092350090A61B /* CoreDataPlus.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 236350711F95EC1A00B3A16A /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 236350B51F95F0ED00B3A16A /* CoreDataPlus.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2363507E1F95EC3000B3A16A /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 236350B61F95F0EF00B3A16A /* CoreDataPlus.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2363509A1F95EC5600B3A16A /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 236350B71F95F0F200B3A16A /* CoreDataPlus.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23B580671F94FEDF00A365C0 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 23B5807B1F94FEDF00A365C0 /* CoreDataPlus.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - /* Begin PBXNativeTarget section */ - 06968ACD26454AFF00088D76 /* CoreDataPlus Tests watchOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 06968AD826454AFF00088D76 /* Build configuration list for PBXNativeTarget "CoreDataPlus Tests watchOS" */; - buildPhases = ( - 06968ACA26454AFF00088D76 /* Sources */, - 06968ACB26454AFF00088D76 /* Frameworks */, - 06968ACC26454AFF00088D76 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 06968AD526454AFF00088D76 /* PBXTargetDependency */, - ); - name = "CoreDataPlus Tests watchOS"; - productName = "CoreDataPlus Tests watchOS"; - productReference = 06968ACE26454AFF00088D76 /* CoreDataPlus Tests watchOS.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 06AF614726F091370090A61B /* CoreDataPlus */ = { isa = PBXNativeTarget; buildConfigurationList = 06AF615B26F091370090A61B /* Build configuration list for PBXNativeTarget "CoreDataPlus" */; buildPhases = ( - 06AF614326F091370090A61B /* Headers */, 06AF614426F091370090A61B /* Sources */, 06AF614526F091370090A61B /* Frameworks */, 06AF614626F091370090A61B /* Resources */, @@ -1052,165 +604,25 @@ productReference = 06AF614F26F091370090A61B /* CoreDataPlus Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 236350731F95EC1A00B3A16A /* CoreDataPlus watchOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 236350791F95EC1A00B3A16A /* Build configuration list for PBXNativeTarget "CoreDataPlus watchOS" */; - buildPhases = ( - 2363506F1F95EC1A00B3A16A /* Sources */, - 236350701F95EC1A00B3A16A /* Frameworks */, - 236350711F95EC1A00B3A16A /* Headers */, - 236350721F95EC1A00B3A16A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CoreDataPlus watchOS"; - productName = CoreDataPlus; - productReference = 236350741F95EC1A00B3A16A /* CoreDataPlus.framework */; - productType = "com.apple.product-type.framework"; - }; - 236350801F95EC3000B3A16A /* CoreDataPlus tvOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 236350921F95EC3000B3A16A /* Build configuration list for PBXNativeTarget "CoreDataPlus tvOS" */; - buildPhases = ( - 2363507C1F95EC3000B3A16A /* Sources */, - 2363507D1F95EC3000B3A16A /* Frameworks */, - 2363507E1F95EC3000B3A16A /* Headers */, - 2363507F1F95EC3000B3A16A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CoreDataPlus tvOS"; - productName = "CoreDataPlus tvOS"; - productReference = 236350811F95EC3000B3A16A /* CoreDataPlus.framework */; - productType = "com.apple.product-type.framework"; - }; - 2363509C1F95EC5600B3A16A /* CoreDataPlus macOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 236350AE1F95EC5600B3A16A /* Build configuration list for PBXNativeTarget "CoreDataPlus macOS" */; - buildPhases = ( - 236350981F95EC5600B3A16A /* Sources */, - 236350991F95EC5600B3A16A /* Frameworks */, - 2363509A1F95EC5600B3A16A /* Headers */, - 2363509B1F95EC5600B3A16A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CoreDataPlus macOS"; - productName = "CoreDataPlus macOS"; - productReference = 2363509D1F95EC5600B3A16A /* CoreDataPlus.framework */; - productType = "com.apple.product-type.framework"; - }; - 23B580691F94FEDF00A365C0 /* CoreDataPlus iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 23B5807E1F94FEDF00A365C0 /* Build configuration list for PBXNativeTarget "CoreDataPlus iOS" */; - buildPhases = ( - 23B580651F94FEDF00A365C0 /* Sources */, - 23B580661F94FEDF00A365C0 /* Frameworks */, - 23B580671F94FEDF00A365C0 /* Headers */, - 23B580681F94FEDF00A365C0 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CoreDataPlus iOS"; - productName = CoreDataPlus; - productReference = 23B5806A1F94FEDF00A365C0 /* CoreDataPlus.framework */; - productType = "com.apple.product-type.framework"; - }; - 23EFDE8C1F95FE730038BE75 /* CoreDataPlus Tests iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 23EFDE951F95FE730038BE75 /* Build configuration list for PBXNativeTarget "CoreDataPlus Tests iOS" */; - buildPhases = ( - 23EFDE891F95FE730038BE75 /* Sources */, - 23EFDE8A1F95FE730038BE75 /* Frameworks */, - 23EFDE8B1F95FE730038BE75 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 23EFDE941F95FE730038BE75 /* PBXTargetDependency */, - ); - name = "CoreDataPlus Tests iOS"; - productName = "CoreDataPlus Tests iOS"; - productReference = 23EFDE8D1F95FE730038BE75 /* CoreDataPlus Tests iOS.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 23EFDE9B1F95FE990038BE75 /* CoreDataPlus Tests tvOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 23EFDEA41F95FE990038BE75 /* Build configuration list for PBXNativeTarget "CoreDataPlus Tests tvOS" */; - buildPhases = ( - 23EFDE981F95FE990038BE75 /* Sources */, - 23EFDE991F95FE990038BE75 /* Frameworks */, - 23EFDE9A1F95FE990038BE75 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 23EFDEA31F95FE990038BE75 /* PBXTargetDependency */, - ); - name = "CoreDataPlus Tests tvOS"; - productName = "CoreDataPlus Tests tvOS"; - productReference = 23EFDE9C1F95FE990038BE75 /* CoreDataPlus Tests tvOS.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 23EFDEAA1F95FEB40038BE75 /* CoreDataPlus Tests macOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 23EFDEB31F95FEB50038BE75 /* Build configuration list for PBXNativeTarget "CoreDataPlus Tests macOS" */; - buildPhases = ( - 23EFDEA71F95FEB40038BE75 /* Sources */, - 23EFDEA81F95FEB40038BE75 /* Frameworks */, - 23EFDEA91F95FEB40038BE75 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 23EFDEB21F95FEB50038BE75 /* PBXTargetDependency */, - ); - name = "CoreDataPlus Tests macOS"; - productName = "CoreDataPlus Tests macOS"; - productReference = 23EFDEAB1F95FEB40038BE75 /* CoreDataPlus Tests macOS.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 23B580611F94FEDF00A365C0 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1200; + LastUpgradeCheck = 1530; ORGANIZATIONNAME = "Alessandro Marzoli"; TargetAttributes = { - 06968ACD26454AFF00088D76 = { - CreatedOnToolsVersion = 12.5; - }; 06AF614726F091370090A61B = { CreatedOnToolsVersion = 13.0; }; 06AF614E26F091370090A61B = { CreatedOnToolsVersion = 13.0; }; - 236350731F95EC1A00B3A16A = { - CreatedOnToolsVersion = 9.1; - LastSwiftMigration = 1240; - ProvisioningStyle = Automatic; - }; - 236350801F95EC3000B3A16A = { - CreatedOnToolsVersion = 9.1; - LastSwiftMigration = 1240; - ProvisioningStyle = Automatic; - }; - 2363509C1F95EC5600B3A16A = { - CreatedOnToolsVersion = 9.1; - LastSwiftMigration = 1240; - ProvisioningStyle = Automatic; + 06B3A5A62BD7E447009217C0 = { + CreatedOnToolsVersion = 15.3; }; 2364CE511FA383EF0076F2B8 = { CreatedOnToolsVersion = 9.1; @@ -1220,24 +632,6 @@ CreatedOnToolsVersion = 9.1; ProvisioningStyle = Automatic; }; - 23B580691F94FEDF00A365C0 = { - CreatedOnToolsVersion = 9.1; - LastSwiftMigration = 1240; - ProvisioningStyle = Automatic; - }; - 23EFDE8C1F95FE730038BE75 = { - CreatedOnToolsVersion = 9.1; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - }; - 23EFDE9B1F95FE990038BE75 = { - CreatedOnToolsVersion = 9.1; - ProvisioningStyle = Automatic; - }; - 23EFDEAA1F95FEB40038BE75 = { - CreatedOnToolsVersion = 9.1; - ProvisioningStyle = Automatic; - }; }; }; buildConfigurationList = 23B580641F94FEDF00A365C0 /* Build configuration list for PBXProject "CoreDataPlus" */; @@ -1254,31 +648,15 @@ projectRoot = ""; targets = ( 06AF614726F091370090A61B /* CoreDataPlus */, - 23B580691F94FEDF00A365C0 /* CoreDataPlus iOS */, - 236350731F95EC1A00B3A16A /* CoreDataPlus watchOS */, - 236350801F95EC3000B3A16A /* CoreDataPlus tvOS */, - 2363509C1F95EC5600B3A16A /* CoreDataPlus macOS */, 06AF614E26F091370090A61B /* CoreDataPlus Tests */, - 23EFDE8C1F95FE730038BE75 /* CoreDataPlus Tests iOS */, - 23EFDE9B1F95FE990038BE75 /* CoreDataPlus Tests tvOS */, - 23EFDEAA1F95FEB40038BE75 /* CoreDataPlus Tests macOS */, - 06968ACD26454AFF00088D76 /* CoreDataPlus Tests watchOS */, 2364CE511FA383EF0076F2B8 /* SwiftLint */, 2364CE561FA3843D0076F2B8 /* Cleanup Whitespace */, + 06B3A5A62BD7E447009217C0 /* Swift-Format Lint */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 06968ACC26454AFF00088D76 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 06968AFA26454CC700088D76 /* SampleModelV1.sqlite in Resources */, - 06968AFB26454CCB00088D76 /* SampleModelV2.sqlite in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 06AF614626F091370090A61B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1290,178 +668,89 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 06AF619226F093EE0090A61B /* SampleModelV1.sqlite in Resources */, - 06AF619326F093F20090A61B /* SampleModelV2.sqlite in Resources */, + D214B1DB2BEECAE90090F18A /* SampleModel2_V1.sqlite in Resources */, + 06AF619226F093EE0090A61B /* SampleModel_V1.sqlite in Resources */, + 06AF619326F093F20090A61B /* SampleModel_V2.sqlite in Resources */, + D214B1E02BEECAE90090F18A /* SampleModel2_V2.sqlite in Resources */, + 06F9DDDF2BF3627900D80B8F /* SampleModel3_V1.sqlite in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 236350721F95EC1A00B3A16A /* Resources */ = { - isa = PBXResourcesBuildPhase; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 06B3A5AA2BD7E459009217C0 /* Lint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = Lint; + outputFileListPaths = ( + ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swift-format >/dev/null; then\nswift-format lint ./Sources ./Tests --recursive --parallel --configuration ./.swift-format\nelse\necho \"swift-format not installed\"\nfi\n"; }; - 2363507F1F95EC3000B3A16A /* Resources */ = { - isa = PBXResourcesBuildPhase; + 2364CE551FA384090076F2B8 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputPaths = ( + ); + name = SwiftLint; + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; - 2363509B1F95EC5600B3A16A /* Resources */ = { - isa = PBXResourcesBuildPhase; + 2364CE5A1FA384490076F2B8 /* Cleanup Whitespace */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputPaths = ( + ); + name = "Cleanup Whitespace"; + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/sh\ndirectories=(Sources Tests)\n\nfor directory in \"${directories[@]}\"\ndo\necho \"Cleaning whitespace in directory: $directory\"\nfind $directory -iregex '.*\\.swift' -exec sed -E -i '' -e 's/[[:blank:]]*$//' {} \\;\ndone\n"; }; - 23B580681F94FEDF00A365C0 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDE8B1F95FE730038BE75 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 06A6CE26261E0966000563F0 /* SampleModelV1.sqlite in Resources */, - D27A2E36255F29D20043B43F /* SampleModelV2.sqlite in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDE9A1F95FE990038BE75 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 06A6CE30261E0966000563F0 /* SampleModelV1.sqlite in Resources */, - D27A2E37255F29D20043B43F /* SampleModelV2.sqlite in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDEA91F95FEB40038BE75 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 06A6CE3A261E0966000563F0 /* SampleModelV1.sqlite in Resources */, - D27A2E38255F29D20043B43F /* SampleModelV2.sqlite in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 2364CE551FA384090076F2B8 /* SwiftLint */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = SwiftLint; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; - }; - 2364CE5A1FA384490076F2B8 /* Cleanup Whitespace */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Cleanup Whitespace"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/bin/sh\ndirectories=(Sources Tests)\n\nfor directory in \"${directories[@]}\"\ndo\necho \"Cleaning whitespace in directory: $directory\"\nfind $directory -iregex '.*\\.swift' -exec sed -E -i '' -e 's/[[:blank:]]*$//' {} \\;\ndone\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 06968ACA26454AFF00088D76 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 06968B1026454FBF00088D76 /* BookCoverToCoverMigrationPolicy.swift in Sources */, - 06968AE126454CAC00088D76 /* FetchedResultsChangesTests.swift in Sources */, - 06968ADF26454CAC00088D76 /* MigrationsTests.swift in Sources */, - 06968AE826454CAC00088D76 /* NSFetchRequestUtilsTests.swift in Sources */, - 063E1011264BC2F90050E84C /* Deprecated.swift in Sources */, - 06968B0026454CD900088D76 /* Maker.swift in Sources */, - 06968AF726454CBF00088D76 /* SampleModel2+V3.swift in Sources */, - 06968B0926454CE500088D76 /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */, - 06968AFC26454CD300088D76 /* V2to3MakerPolicy.swift in Sources */, - 06968AE026454CAC00088D76 /* TransformerTests.swift in Sources */, - 06968ADA26454CAC00088D76 /* NSFetchRequestResultCoreDataTests.swift in Sources */, - 06968AF826454CBF00088D76 /* NSManagedObjectContext+SampleModel2.swift in Sources */, - 06968B0426454CDD00088D76 /* SampleModel.xcdatamodeld in Sources */, - 06968AED26454CB200088D76 /* NotificationPayloadTests.swift in Sources */, - 06968AEF26454CBC00088D76 /* Page.swift in Sources */, - 06968AF526454CBF00088D76 /* SampleModel2+V1.swift in Sources */, - 06968AFF26454CD900088D76 /* NSManagedObject+DelayedDeletable.swift in Sources */, - 06968B0626454CE500088D76 /* BaseTestCase.swift in Sources */, - 06968B152645685F00088D76 /* FeedbackMigrationManager.swift in Sources */, - 06968AFE26454CD600088D76 /* SampleModelVersion.swift in Sources */, - 06968AF026454CBC00088D76 /* Author.swift in Sources */, - 06968ADB26454CAC00088D76 /* NSManagedObjectUtilsTests.swift in Sources */, - 06968AEB26454CAC00088D76 /* NSSetCoreDataTests.swift in Sources */, - 06968B0226454CD900088D76 /* Car.swift in Sources */, - 06968ADD26454CAC00088D76 /* NSAttributeDescriptionUtilsTests.swift in Sources */, - 06968AE926454CAC00088D76 /* NSManagedObjectDelayedDeletableTests.swift in Sources */, - 06968AEA26454CAC00088D76 /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */, - 06968AEE26454CB200088D76 /* NotificationMergeTests.swift in Sources */, - 06968AF126454CBC00088D76 /* Feedback.swift in Sources */, - 06968AF926454CBF00088D76 /* SampleModel2.swift in Sources */, - 06968AF626454CBF00088D76 /* SampleModel2+V2.swift in Sources */, - 06968AE526454CAC00088D76 /* NSManagedObjectContextHistoryTests.swift in Sources */, - 06968B0826454CE500088D76 /* OnDiskTestCase.swift in Sources */, - 06968B0126454CD900088D76 /* NSManagedObject+UpdateTimestampable.swift in Sources */, - 06E06369264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift in Sources */, - 06968B0B26454CE500088D76 /* CoreDataErrors.swift in Sources */, - 06968AE226454CAC00088D76 /* NSManagedObjectContextUtilsTests.swift in Sources */, - 06968AFD26454CD300088D76 /* V2toV3.xcmappingmodel in Sources */, - 06968ADC26454CAC00088D76 /* NSFetchRequestResultUtilsTests.swift in Sources */, - 06968B0326454CD900088D76 /* Person.swift in Sources */, - 06968AF226454CBC00088D76 /* Content.swift in Sources */, - 06968AF426454CBC00088D76 /* Cover.swift in Sources */, - 06968AE726454CAC00088D76 /* NSManagedObjectUpdateTimestampableTests.swift in Sources */, - 06968B0526454CE000088D76 /* NSManagedObjectContext+SampleModel.swift in Sources */, - 06968B0726454CE500088D76 /* InMemoryTestCase.swift in Sources */, - 06968AE426454CAC00088D76 /* ProgrammaticallyDefinedModelTests.swift in Sources */, - 06968AE326454CAC00088D76 /* NSEntityDescriptionUtilsTests.swift in Sources */, - 06968B0A26454CE500088D76 /* Utils.swift in Sources */, - 06968ADE26454CAC00088D76 /* ModelVersionTests.swift in Sources */, - 06968AE626454CAC00088D76 /* NSManagedObjectContextInvestigationTests.swift in Sources */, - 06968AF326454CBC00088D76 /* Book.swift in Sources */, - 06968AEC26454CAC00088D76 /* ProgrammaticMigrationTests.swift in Sources */, - 06F7D56A26E5F8DB00929CA6 /* NSPredicateUtilsTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 06AF614426F091370090A61B /* Sources */ = { - isa = PBXSourcesBuildPhase; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 06AF614426F091370090A61B /* Sources */ = { + isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 06AF617326F092C30090A61B /* ModelVersion.swift in Sources */, - 06AF617226F092C30090A61B /* MigrationStep.swift in Sources */, 06AF616326F092BB0090A61B /* NSAttributeDescription+Utils.swift in Sources */, 06AF617926F092CC0090A61B /* Transformer.swift in Sources */, 06AF616A26F092BB0090A61B /* NSPredicate+Utils.swift in Sources */, + D214B1E62BF23E4B0090F18A /* StagedMigration.swift in Sources */, 06AF616826F092BB0090A61B /* NSBatchInsertResult+Utils.swift in Sources */, 06AF616126F092BB0090A61B /* FetchedResultsChanges.swift in Sources */, 06AF617826F092C80090A61B /* Notification+Payloads.swift in Sources */, 06AF616926F092BB0090A61B /* NSManagedObjectContext+Utils.swift in Sources */, 06AF616F26F092BB0090A61B /* NSManagedObject+Utils.swift in Sources */, - 06AF617426F092C30090A61B /* LightweightMigrationManager.swift in Sources */, 06AF616E26F092BB0090A61B /* PersistentStoreOptions.swift in Sources */, 06AF616D26F092BB0090A61B /* NSFetchRequest+Utils.swift in Sources */, 06AF616C26F092BB0090A61B /* NSEntityDescription+Utils.swift in Sources */, + D2DEC77D2B626D38006EA172 /* NSCompositeAttributeDescription+Utils.swift in Sources */, + 06A1D11F2BF34C6300F62CA1 /* LegacyMigrationStep.swift in Sources */, + D214B1EC2BF23E5A0090F18A /* LegacyMigration.swift in Sources */, 06AF617726F092C80090A61B /* Notification+Utils.swift in Sources */, 06AF617026F092BB0090A61B /* NSManagedObjectContext+History.swift in Sources */, 06AF617626F092C30090A61B /* Migrator.swift in Sources */, @@ -1471,6 +760,7 @@ 06AF616426F092BB0090A61B /* NSFetchedPropertyDescription+Utils.swift in Sources */, 06AF616726F092BB0090A61B /* CoreDataPlus.swift in Sources */, 06AF617A26F092CC0090A61B /* CustomTransformer.swift in Sources */, + D214B17C2BECF67C0090F18A /* StagedMigrationStep.swift in Sources */, 06AF615F26F092BB0090A61B /* NSBatchDeleteResult+Utils.swift in Sources */, 06AF616226F092BB0090A61B /* NSBatchUpdateResult+Utils.swift in Sources */, 06AF617526F092C30090A61B /* MigrationProgressReporter.swift in Sources */, @@ -1486,458 +776,75 @@ files = ( 06AF61AD26F0941A0090A61B /* OnDiskTestCase.swift in Sources */, 06AF619A26F093FE0090A61B /* Person.swift in Sources */, - 06AF617F26F093E20090A61B /* NSAttributeDescriptionUtilsTests.swift in Sources */, - 06AF619026F093E60090A61B /* NotificationMergeTests.swift in Sources */, + 06DED80B2BF36795005EFEA5 /* SampleModel3.xcdatamodeld in Sources */, + 06AF617F26F093E20090A61B /* NSAttributeDescriptionUtils_Tests.swift in Sources */, + 06AF619026F093E60090A61B /* NotificationMerge_Tests.swift in Sources */, 06AF61A926F0940F0090A61B /* SampleModel2+V2.swift in Sources */, 06AF61A726F0940F0090A61B /* SampleModel2.swift in Sources */, - 06AF618826F093E20090A61B /* TransformerTests.swift in Sources */, - 06AF618126F093E20090A61B /* FetchesWithAffectedStoresTests.swift in Sources */, + 06AF618826F093E20090A61B /* Transformer_Tests.swift in Sources */, + 06AF618126F093E20090A61B /* FetchesWithAffectedStores_Tests.swift in Sources */, 06AF619926F093FE0090A61B /* NSManagedObject+UpdateTimestampable.swift in Sources */, 06AF61A126F0940B0090A61B /* Cover.swift in Sources */, - 06AF618526F093E20090A61B /* NSSetCoreDataTests.swift in Sources */, - 06AF617E26F093E20090A61B /* FetchedResultsChangesTests.swift in Sources */, - 06AF618626F093E20090A61B /* ProgrammaticMigrationTests.swift in Sources */, + 06AF618526F093E20090A61B /* NSSetCoreData_Tests.swift in Sources */, + 06AF617E26F093E20090A61B /* FetchedResultsChanges_Tests.swift in Sources */, + 06AF618626F093E20090A61B /* ProgrammaticMigration_Tests.swift in Sources */, 06AF61AC26F0941A0090A61B /* Deprecated.swift in Sources */, 06AF61AA26F094170090A61B /* BaseTestCase.swift in Sources */, 06AF619426F093F60090A61B /* V2to3MakerPolicy.swift in Sources */, - 06AF618926F093E20090A61B /* NSManagedObjectDelayedDeletableTests.swift in Sources */, + 06AF618926F093E20090A61B /* NSManagedObjectDelayedDeletable_Tests.swift in Sources */, 06AF61A226F0940B0090A61B /* Page.swift in Sources */, 06AF619C26F094050090A61B /* NSManagedObjectContext+SampleModel.swift in Sources */, 06AF619F26F0940B0090A61B /* Feedback.swift in Sources */, - 06AF618226F093E20090A61B /* NSFetchRequestResultCoreDataTests.swift in Sources */, + 06AF618226F093E20090A61B /* NSFetchRequestResultCoreData_Tests.swift in Sources */, + 06F9DDE32BF3627900D80B8F /* SampleModelVersion3.swift in Sources */, 06AF61A326F0940F0090A61B /* BookCoverToCoverMigrationPolicy.swift in Sources */, 06AF61A526F0940F0090A61B /* SampleModel2+V1.swift in Sources */, - 06AF618B26F093E20090A61B /* NSManagedObjectUpdateTimestampableTests.swift in Sources */, - 06AF618026F093E20090A61B /* NSEntityDescriptionUtilsTests.swift in Sources */, - 06AF617B26F093E20090A61B /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */, + 06AF618B26F093E20090A61B /* NSManagedObjectUpdateTimestampable_Tests.swift in Sources */, + 06AF618026F093E20090A61B /* NSEntityDescriptionUtils_Tests.swift in Sources */, + 06AF617B26F093E20090A61B /* NSPersistentStoreCoordinatorUtils_Tests.swift in Sources */, 06AF61AF26F0941A0090A61B /* Utils.swift in Sources */, 06AF619E26F0940B0090A61B /* Book.swift in Sources */, 06AF619626F093FE0090A61B /* NSManagedObject+DelayedDeletable.swift in Sources */, 06AF619726F093FE0090A61B /* Car.swift in Sources */, 06AF619D26F0940B0090A61B /* Author.swift in Sources */, 06AF61A826F0940F0090A61B /* NSManagedObjectContext+SampleModel2.swift in Sources */, - 06AF618326F093E20090A61B /* MigrationsTests.swift in Sources */, - 06AF618C26F093E20090A61B /* NSManagedObjectContextHistoryTests.swift in Sources */, + 06AF618326F093E20090A61B /* Migrations_Tests.swift in Sources */, + 06AF618C26F093E20090A61B /* NSManagedObjectContextHistory_Tests.swift in Sources */, 06AF61A626F0940F0090A61B /* FeedbackMigrationManager.swift in Sources */, 06AF61A026F0940B0090A61B /* Content.swift in Sources */, - 06AF618726F093E20090A61B /* ModelVersionTests.swift in Sources */, + 06AF618726F093E20090A61B /* ModelVersion_Tests.swift in Sources */, + 06F9DDE12BF3627900D80B8F /* SampleModel3.swift in Sources */, 06AF619B26F094020090A61B /* SampleModel.xcdatamodeld in Sources */, + D262F0DD2BECBE1D006C57A6 /* StagedMigrations_Tests.swift in Sources */, 06AF619826F093FE0090A61B /* Maker.swift in Sources */, - 06AF618A26F093E20090A61B /* NSPredicateUtilsTests.swift in Sources */, - 06AF617C26F093E20090A61B /* NSManagedObjectUtilsTests.swift in Sources */, - 06AF618D26F093E20090A61B /* NSManagedObjectContextUtilsTests.swift in Sources */, - 06AF61B226F0959F0090A61B /* V2toV3.xcmappingmodel in Sources */, + 06AF618A26F093E20090A61B /* NSPredicateUtils_Tests.swift in Sources */, + D214B1812BEE088B0090F18A /* V2toV3.xcmappingmodel in Sources */, + 06AF617C26F093E20090A61B /* NSManagedObjectUtils_Tests.swift in Sources */, + 06AF618D26F093E20090A61B /* NSManagedObjectContextUtils_Tests.swift in Sources */, 06AF61A426F0940F0090A61B /* SampleModel2+V3.swift in Sources */, - 06AF619126F093E60090A61B /* NotificationPayloadTests.swift in Sources */, + 06AF619126F093E60090A61B /* NotificationPayload_Tests.swift in Sources */, 06AF61AE26F0941A0090A61B /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */, - 06AF618F26F093E20090A61B /* NSFetchRequestResultUtilsTests.swift in Sources */, + 06AF618F26F093E20090A61B /* NSFetchRequestResultUtils_Tests.swift in Sources */, 06AF619526F093FB0090A61B /* SampleModelVersion.swift in Sources */, - 06AF618E26F093E20090A61B /* ProgrammaticallyDefinedModelTests.swift in Sources */, - 06AF618426F093E20090A61B /* NSManagedObjectContextInvestigationTests.swift in Sources */, + 06AF618E26F093E20090A61B /* ProgrammaticallyDefinedModel_Tests.swift in Sources */, + 06AF618426F093E20090A61B /* NSManagedObjectContextInvestigation_Tests.swift in Sources */, 06AF61AB26F0941A0090A61B /* InMemoryTestCase.swift in Sources */, - 06AF617D26F093E20090A61B /* NSFetchRequestUtilsTests.swift in Sources */, + 06AF617D26F093E20090A61B /* NSFetchRequestUtils_Tests.swift in Sources */, 06AF61B026F0941A0090A61B /* CoreDataErrors.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 2363506F1F95EC1A00B3A16A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D2011A162614E3D9001902FB /* NSManagedObjectContext+Utils.swift in Sources */, - D2011A462614E3D9001902FB /* NSFetchRequestResult+CoreData.swift in Sources */, - D28232AD26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift in Sources */, - D2011A1E2614E3D9001902FB /* NSBatchInsertResult+Utils.swift in Sources */, - D2011A122614E3D9001902FB /* NSBatchDeleteResult+Utils.swift in Sources */, - D2011A2E2614E3D9001902FB /* NSSet+CoreData.swift in Sources */, - D2011A562614E3D9001902FB /* Notification+Payloads.swift in Sources */, - D2011A3E2614E3D9001902FB /* NSEntityDescription+Utils.swift in Sources */, - D2011A522614E3D9001902FB /* MigrationStep.swift in Sources */, - D2011A2A2614E3D9001902FB /* FetchedResultsChanges.swift in Sources */, - D2011A3A2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift in Sources */, - 06A6CDA1261DE0E7000563F0 /* Transformer.swift in Sources */, - 06F7D55E26E5F85D00929CA6 /* NSPredicate+Utils.swift in Sources */, - D2011A422614E3D9001902FB /* NSManagedObject+Utils.swift in Sources */, - D214B9AB263BFA22000BBD13 /* Migrator.swift in Sources */, - D2011A5E2614E3D9001902FB /* NSFetchRequest+Utils.swift in Sources */, - D2011A362614E3D9001902FB /* NSManagedObjectContext+History.swift in Sources */, - D2011A5A2614E3D9001902FB /* Notification+Utils.swift in Sources */, - D2011A262614E3D9001902FB /* CoreDataPlus.swift in Sources */, - 0650940C2643E8EB00B3EE12 /* LightweightMigrationManager.swift in Sources */, - D20F4BB92621E71500F0CB25 /* NSAttributeDescription+Utils.swift in Sources */, - D214B9A6263B0FDE000BBD13 /* NSMappingModel+Utils.swift in Sources */, - D2011A222614E3D9001902FB /* Collection+CoreData.swift in Sources */, - D28233102624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift in Sources */, - 061C689426209443000BF0A2 /* CustomTransformer.swift in Sources */, - D2011A1A2614E3D9001902FB /* NSBatchUpdateResult+Utils.swift in Sources */, - 065094072643E8BC00B3EE12 /* MigrationProgressReporter.swift in Sources */, - 06B71CB92629DB04008DFD11 /* PersistentStoreOptions.swift in Sources */, - D2011A4A2614E3D9001902FB /* ModelVersion.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2363507C1F95EC3000B3A16A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D2011A172614E3D9001902FB /* NSManagedObjectContext+Utils.swift in Sources */, - D2011A472614E3D9001902FB /* NSFetchRequestResult+CoreData.swift in Sources */, - D28232AE26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift in Sources */, - D2011A1F2614E3D9001902FB /* NSBatchInsertResult+Utils.swift in Sources */, - D2011A132614E3D9001902FB /* NSBatchDeleteResult+Utils.swift in Sources */, - D2011A2F2614E3D9001902FB /* NSSet+CoreData.swift in Sources */, - D2011A572614E3D9001902FB /* Notification+Payloads.swift in Sources */, - D2011A3F2614E3D9001902FB /* NSEntityDescription+Utils.swift in Sources */, - D2011A532614E3D9001902FB /* MigrationStep.swift in Sources */, - D2011A2B2614E3D9001902FB /* FetchedResultsChanges.swift in Sources */, - D2011A3B2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift in Sources */, - 06A6CDA2261DE0E7000563F0 /* Transformer.swift in Sources */, - 06F7D55F26E5F85D00929CA6 /* NSPredicate+Utils.swift in Sources */, - D2011A432614E3D9001902FB /* NSManagedObject+Utils.swift in Sources */, - D214B9AC263BFA22000BBD13 /* Migrator.swift in Sources */, - D2011A5F2614E3D9001902FB /* NSFetchRequest+Utils.swift in Sources */, - D2011A372614E3D9001902FB /* NSManagedObjectContext+History.swift in Sources */, - D2011A5B2614E3D9001902FB /* Notification+Utils.swift in Sources */, - D2011A272614E3D9001902FB /* CoreDataPlus.swift in Sources */, - 0650940D2643E8EB00B3EE12 /* LightweightMigrationManager.swift in Sources */, - D20F4BBA2621E71500F0CB25 /* NSAttributeDescription+Utils.swift in Sources */, - D214B9A7263B0FDE000BBD13 /* NSMappingModel+Utils.swift in Sources */, - D2011A232614E3D9001902FB /* Collection+CoreData.swift in Sources */, - D28233112624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift in Sources */, - 061C689526209443000BF0A2 /* CustomTransformer.swift in Sources */, - D2011A1B2614E3D9001902FB /* NSBatchUpdateResult+Utils.swift in Sources */, - 065094082643E8BC00B3EE12 /* MigrationProgressReporter.swift in Sources */, - 06B71CBA2629DB05008DFD11 /* PersistentStoreOptions.swift in Sources */, - D2011A4B2614E3D9001902FB /* ModelVersion.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 236350981F95EC5600B3A16A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D2011A182614E3D9001902FB /* NSManagedObjectContext+Utils.swift in Sources */, - D2011A482614E3D9001902FB /* NSFetchRequestResult+CoreData.swift in Sources */, - D28232AF26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift in Sources */, - D2011A202614E3D9001902FB /* NSBatchInsertResult+Utils.swift in Sources */, - D2011A142614E3D9001902FB /* NSBatchDeleteResult+Utils.swift in Sources */, - D2011A302614E3D9001902FB /* NSSet+CoreData.swift in Sources */, - D2011A582614E3D9001902FB /* Notification+Payloads.swift in Sources */, - D2011A402614E3D9001902FB /* NSEntityDescription+Utils.swift in Sources */, - D2011A542614E3D9001902FB /* MigrationStep.swift in Sources */, - D2011A2C2614E3D9001902FB /* FetchedResultsChanges.swift in Sources */, - D2011A3C2614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift in Sources */, - 06A6CDA3261DE0E7000563F0 /* Transformer.swift in Sources */, - 06F7D56026E5F85D00929CA6 /* NSPredicate+Utils.swift in Sources */, - D2011A442614E3D9001902FB /* NSManagedObject+Utils.swift in Sources */, - D214B9AD263BFA22000BBD13 /* Migrator.swift in Sources */, - D2011A602614E3D9001902FB /* NSFetchRequest+Utils.swift in Sources */, - D2011A382614E3D9001902FB /* NSManagedObjectContext+History.swift in Sources */, - D2011A5C2614E3D9001902FB /* Notification+Utils.swift in Sources */, - D2011A282614E3D9001902FB /* CoreDataPlus.swift in Sources */, - 0650940E2643E8EB00B3EE12 /* LightweightMigrationManager.swift in Sources */, - D20F4BBB2621E71500F0CB25 /* NSAttributeDescription+Utils.swift in Sources */, - D214B9A8263B0FDE000BBD13 /* NSMappingModel+Utils.swift in Sources */, - D2011A242614E3D9001902FB /* Collection+CoreData.swift in Sources */, - D28233122624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift in Sources */, - 061C689626209443000BF0A2 /* CustomTransformer.swift in Sources */, - D2011A1C2614E3D9001902FB /* NSBatchUpdateResult+Utils.swift in Sources */, - 065094092643E8BC00B3EE12 /* MigrationProgressReporter.swift in Sources */, - 06B71CBB2629DB05008DFD11 /* PersistentStoreOptions.swift in Sources */, - D2011A4C2614E3D9001902FB /* ModelVersion.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23B580651F94FEDF00A365C0 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D2011A152614E3D9001902FB /* NSManagedObjectContext+Utils.swift in Sources */, - D2011A452614E3D9001902FB /* NSFetchRequestResult+CoreData.swift in Sources */, - D28232AC26249FAF00F37442 /* NSDerivedAttributeDescription+Utils.swift in Sources */, - D2011A1D2614E3D9001902FB /* NSBatchInsertResult+Utils.swift in Sources */, - D2011A112614E3D9001902FB /* NSBatchDeleteResult+Utils.swift in Sources */, - D2011A2D2614E3D9001902FB /* NSSet+CoreData.swift in Sources */, - D2011A552614E3D9001902FB /* Notification+Payloads.swift in Sources */, - D2011A3D2614E3D9001902FB /* NSEntityDescription+Utils.swift in Sources */, - D2011A512614E3D9001902FB /* MigrationStep.swift in Sources */, - D2011A292614E3D9001902FB /* FetchedResultsChanges.swift in Sources */, - D2011A392614E3D9001902FB /* NSPersistentStoreCoordinator+Utils.swift in Sources */, - 06A6CDA0261DE0E7000563F0 /* Transformer.swift in Sources */, - 06F7D55D26E5F85D00929CA6 /* NSPredicate+Utils.swift in Sources */, - D2011A412614E3D9001902FB /* NSManagedObject+Utils.swift in Sources */, - D214B9AA263BFA22000BBD13 /* Migrator.swift in Sources */, - D2011A5D2614E3D9001902FB /* NSFetchRequest+Utils.swift in Sources */, - D2011A352614E3D9001902FB /* NSManagedObjectContext+History.swift in Sources */, - D2011A592614E3D9001902FB /* Notification+Utils.swift in Sources */, - D2011A252614E3D9001902FB /* CoreDataPlus.swift in Sources */, - 0650940B2643E8EB00B3EE12 /* LightweightMigrationManager.swift in Sources */, - D20F4BB82621E71500F0CB25 /* NSAttributeDescription+Utils.swift in Sources */, - D214B9A5263B0FDE000BBD13 /* NSMappingModel+Utils.swift in Sources */, - D2011A212614E3D9001902FB /* Collection+CoreData.swift in Sources */, - D282330F2624A4FE00F37442 /* NSFetchedPropertyDescription+Utils.swift in Sources */, - 061C689326209443000BF0A2 /* CustomTransformer.swift in Sources */, - D2011A192614E3D9001902FB /* NSBatchUpdateResult+Utils.swift in Sources */, - 065094062643E8BC00B3EE12 /* MigrationProgressReporter.swift in Sources */, - 06B71CB82629DB04008DFD11 /* PersistentStoreOptions.swift in Sources */, - D2011A492614E3D9001902FB /* ModelVersion.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDE891F95FE730038BE75 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 06968B0D26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift in Sources */, - D2B7E1552448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */, - 23948D1F1F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift in Sources */, - 23FFB86222EC948C00391D40 /* Utils.swift in Sources */, - 063E100E264BC2F90050E84C /* Deprecated.swift in Sources */, - 233654371F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift in Sources */, - 061C684E261EF969000BF0A2 /* BaseTestCase.swift in Sources */, - 23C6493A20396FFA00AA514A /* NotificationMergeTests.swift in Sources */, - 239F4CC022F49F2F007888BA /* OnDiskTestCase.swift in Sources */, - 23DAFA2A1F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift in Sources */, - 23EEF5491F962E6A00A2E72F /* Person.swift in Sources */, - D2A448252631B937003059A7 /* SampleModel2+V3.swift in Sources */, - 06A6CDC7261DFC8F000563F0 /* TransformerTests.swift in Sources */, - 230C087C21417E1900A1B6CB /* MigrationsTests.swift in Sources */, - D282326926244D3A00F37442 /* Author.swift in Sources */, - 2351687222A2DD4500340611 /* NotificationPayloadTests.swift in Sources */, - D2C5CE36244762FA00AD19D8 /* NSSetCoreDataTests.swift in Sources */, - D28232952624754700F37442 /* Page.swift in Sources */, - 06968B122645685F00088D76 /* FeedbackMigrationManager.swift in Sources */, - D20F4C202622029400F0CB25 /* ProgrammaticMigrationTests.swift in Sources */, - D29AA4E824D99DEA005CE7F4 /* NSManagedObject+DelayedDeletable.swift in Sources */, - 237728652148FACE00FDAF32 /* V2to3MakerPolicy.swift in Sources */, - 23F8E9E01F965B2000C65565 /* SampleModel.xcdatamodeld in Sources */, - D282332C2625AB2300F37442 /* CoreDataErrors.swift in Sources */, - 2383AC641FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift in Sources */, - 232440B722D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift in Sources */, - 23541AFA22ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift in Sources */, - D28233652625C55100F37442 /* Content.swift in Sources */, - 06B71C742629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift in Sources */, - 234130681F95FF90002BE7FC /* NSFetchRequestUtilsTests.swift in Sources */, - D282334F2625C2A200F37442 /* SampleModel2+V2.swift in Sources */, - D28233422625C23F00F37442 /* SampleModel2+V1.swift in Sources */, - 236526AB215A46C200A51C9F /* InMemoryTestCase.swift in Sources */, - 234DA7AB214A5D2B00D5C24F /* Maker.swift in Sources */, - 06E06366264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift in Sources */, - 23A8A44D1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift in Sources */, - 23D1056B20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift in Sources */, - D282331F26257A7400F37442 /* Feedback.swift in Sources */, - D20F4C012621F3B400F0CB25 /* SampleModel2.swift in Sources */, - 2341306A1F95FF90002BE7FC /* NSManagedObjectContextUtilsTests.swift in Sources */, - 23C3B2B61FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift in Sources */, - 2377287121490B9D00FDAF32 /* V2toV3.xcmappingmodel in Sources */, - D282327F2624569400F37442 /* Book.swift in Sources */, - 23EEF5451F962E5700A2E72F /* Car.swift in Sources */, - D282325B26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift in Sources */, - D28233722625C56A00F37442 /* Cover.swift in Sources */, - D282323B2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift in Sources */, - 2341306B1F95FF90002BE7FC /* NSManagedObjectUtilsTests.swift in Sources */, - D28232C32624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */, - D29AA4E524D99DEA005CE7F4 /* NSManagedObject+UpdateTimestampable.swift in Sources */, - 234130661F95FF90002BE7FC /* SampleModelVersion.swift in Sources */, - 234130671F95FF90002BE7FC /* ModelVersionTests.swift in Sources */, - 06F7D56726E5F8DB00929CA6 /* NSPredicateUtilsTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDE981F95FE990038BE75 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 06968B0E26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift in Sources */, - D2B7E1562448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */, - 23948D201F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift in Sources */, - 23FFB86322EC948C00391D40 /* Utils.swift in Sources */, - 063E100F264BC2F90050E84C /* Deprecated.swift in Sources */, - 233654381F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift in Sources */, - 061C684F261EF969000BF0A2 /* BaseTestCase.swift in Sources */, - 23C6493B20396FFA00AA514A /* NotificationMergeTests.swift in Sources */, - 239F4CC122F49F2F007888BA /* OnDiskTestCase.swift in Sources */, - 23DAFA2B1F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift in Sources */, - 23EEF54A1F962E6A00A2E72F /* Person.swift in Sources */, - D2A448262631B937003059A7 /* SampleModel2+V3.swift in Sources */, - 06A6CDC8261DFC8F000563F0 /* TransformerTests.swift in Sources */, - 230C087D21417E1900A1B6CB /* MigrationsTests.swift in Sources */, - D282326A26244D3A00F37442 /* Author.swift in Sources */, - 2351687322A2DD4500340611 /* NotificationPayloadTests.swift in Sources */, - D2C5CE37244762FA00AD19D8 /* NSSetCoreDataTests.swift in Sources */, - D28232962624754700F37442 /* Page.swift in Sources */, - 06968B132645685F00088D76 /* FeedbackMigrationManager.swift in Sources */, - D20F4C212622029400F0CB25 /* ProgrammaticMigrationTests.swift in Sources */, - D29AA4E624D99DEA005CE7F4 /* NSManagedObject+DelayedDeletable.swift in Sources */, - 237728662148FACE00FDAF32 /* V2to3MakerPolicy.swift in Sources */, - 23F8E9E11F965B2100C65565 /* SampleModel.xcdatamodeld in Sources */, - D282332D2625AB2300F37442 /* CoreDataErrors.swift in Sources */, - 2383AC651FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift in Sources */, - 232440B822D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift in Sources */, - 23541AFB22ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift in Sources */, - D28233662625C55100F37442 /* Content.swift in Sources */, - 06B71C752629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift in Sources */, - 2341306F1F95FF91002BE7FC /* NSFetchRequestUtilsTests.swift in Sources */, - D28233502625C2A200F37442 /* SampleModel2+V2.swift in Sources */, - D28233432625C23F00F37442 /* SampleModel2+V1.swift in Sources */, - 236526AC215A46C200A51C9F /* InMemoryTestCase.swift in Sources */, - 234DA7AC214A5D2B00D5C24F /* Maker.swift in Sources */, - 06E06367264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift in Sources */, - 23A8A44E1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift in Sources */, - 23D1056C20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift in Sources */, - D282332026257A7400F37442 /* Feedback.swift in Sources */, - D20F4C0B2621F3B400F0CB25 /* SampleModel2.swift in Sources */, - 234130711F95FF91002BE7FC /* NSManagedObjectContextUtilsTests.swift in Sources */, - 23C3B2B71FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift in Sources */, - 2377287221490B9D00FDAF32 /* V2toV3.xcmappingmodel in Sources */, - D28232802624569400F37442 /* Book.swift in Sources */, - 23EEF5461F962E5700A2E72F /* Car.swift in Sources */, - D282325C26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift in Sources */, - D28233732625C56A00F37442 /* Cover.swift in Sources */, - D282323C2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift in Sources */, - 234130721F95FF91002BE7FC /* NSManagedObjectUtilsTests.swift in Sources */, - D28232C42624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */, - D29AA4E324D99DEA005CE7F4 /* NSManagedObject+UpdateTimestampable.swift in Sources */, - 2341306D1F95FF91002BE7FC /* SampleModelVersion.swift in Sources */, - 2341306E1F95FF91002BE7FC /* ModelVersionTests.swift in Sources */, - 06F7D56826E5F8DB00929CA6 /* NSPredicateUtilsTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 23EFDEA71F95FEB40038BE75 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 06968B0F26454FBE00088D76 /* BookCoverToCoverMigrationPolicy.swift in Sources */, - D2B7E1572448BDEA001763BF /* NSPersistentStoreCoordinatorUtilsTests.swift in Sources */, - 23948D211F9934F800B3738D /* NSEntityDescriptionUtilsTests.swift in Sources */, - 23FFB86422EC948C00391D40 /* Utils.swift in Sources */, - 063E1010264BC2F90050E84C /* Deprecated.swift in Sources */, - 233654391F9E1644007F8D3D /* NSManagedObjectContext+SampleModel.swift in Sources */, - 061C6850261EF969000BF0A2 /* BaseTestCase.swift in Sources */, - 23C6493C20396FFA00AA514A /* NotificationMergeTests.swift in Sources */, - 239F4CC222F49F2F007888BA /* OnDiskTestCase.swift in Sources */, - 23DAFA2C1F964DBB0058654B /* NSFetchRequestResultUtilsTests.swift in Sources */, - 23EEF54B1F962E6A00A2E72F /* Person.swift in Sources */, - D2A448272631B937003059A7 /* SampleModel2+V3.swift in Sources */, - 06A6CDC9261DFC8F000563F0 /* TransformerTests.swift in Sources */, - 230C087E21417E1900A1B6CB /* MigrationsTests.swift in Sources */, - D282326B26244D3A00F37442 /* Author.swift in Sources */, - 2351687422A2DD4500340611 /* NotificationPayloadTests.swift in Sources */, - D2C5CE38244762FA00AD19D8 /* NSSetCoreDataTests.swift in Sources */, - D28232972624754700F37442 /* Page.swift in Sources */, - 06968B142645685F00088D76 /* FeedbackMigrationManager.swift in Sources */, - D20F4C222622029400F0CB25 /* ProgrammaticMigrationTests.swift in Sources */, - D29AA4E724D99DEA005CE7F4 /* NSManagedObject+DelayedDeletable.swift in Sources */, - 237728672148FACE00FDAF32 /* V2to3MakerPolicy.swift in Sources */, - 23F8E9E21F965B2200C65565 /* SampleModel.xcdatamodeld in Sources */, - D282332E2625AB2300F37442 /* CoreDataErrors.swift in Sources */, - 2383AC661FB079420085625C /* NSManagedObjectDelayedDeletableTests.swift in Sources */, - 232440B922D2546100A04649 /* NSManagedObjectContextInvestigationTests.swift in Sources */, - 23541AFC22ECAB0000678A96 /* NSManagedObjectContextHistoryTests.swift in Sources */, - D28233672625C55100F37442 /* Content.swift in Sources */, - 06B71C762629CB4F008DFD11 /* ProgrammaticallyDefinedModelTests.swift in Sources */, - 234130761F95FF91002BE7FC /* NSFetchRequestUtilsTests.swift in Sources */, - D28233512625C2A200F37442 /* SampleModel2+V2.swift in Sources */, - D28233442625C23F00F37442 /* SampleModel2+V1.swift in Sources */, - 236526AD215A46C200A51C9F /* InMemoryTestCase.swift in Sources */, - 234DA7AD214A5D2B00D5C24F /* Maker.swift in Sources */, - 06E06368264ABC0D009145B6 /* FetchesWithAffectedStoresTests.swift in Sources */, - 23A8A44F1F9F536D0038DE3A /* NSFetchRequestResultCoreDataTests.swift in Sources */, - 23D1056D20D7EFEF00AE84CC /* FetchedResultsChangesTests.swift in Sources */, - D282332126257A7400F37442 /* Feedback.swift in Sources */, - D20F4C152621F3B500F0CB25 /* SampleModel2.swift in Sources */, - 234130781F95FF91002BE7FC /* NSManagedObjectContextUtilsTests.swift in Sources */, - 23C3B2B81FB0A1C000799E72 /* NSManagedObjectUpdateTimestampableTests.swift in Sources */, - 2377287321490B9D00FDAF32 /* V2toV3.xcmappingmodel in Sources */, - D28232812624569400F37442 /* Book.swift in Sources */, - 23EEF5471F962E5700A2E72F /* Car.swift in Sources */, - D282325D26244AE200F37442 /* NSManagedObjectContext+SampleModel2.swift in Sources */, - D28233742625C56A00F37442 /* Cover.swift in Sources */, - D282323D2624342400F37442 /* NSAttributeDescriptionUtilsTests.swift in Sources */, - 234130791F95FF91002BE7FC /* NSManagedObjectUtilsTests.swift in Sources */, - D28232C52624A15000F37442 /* OnDiskWithProgrammaticallyModelTestCase.swift in Sources */, - D29AA4E424D99DEA005CE7F4 /* NSManagedObject+UpdateTimestampable.swift in Sources */, - 234130741F95FF91002BE7FC /* SampleModelVersion.swift in Sources */, - 234130751F95FF91002BE7FC /* ModelVersionTests.swift in Sources */, - 06F7D56926E5F8DB00929CA6 /* NSPredicateUtilsTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 06968AD526454AFF00088D76 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 236350731F95EC1A00B3A16A /* CoreDataPlus watchOS */; - targetProxy = 06968AD426454AFF00088D76 /* PBXContainerItemProxy */; - }; 06AF615226F091370090A61B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 06AF614726F091370090A61B /* CoreDataPlus */; targetProxy = 06AF615126F091370090A61B /* PBXContainerItemProxy */; }; - 23EFDE941F95FE730038BE75 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 23B580691F94FEDF00A365C0 /* CoreDataPlus iOS */; - targetProxy = 23EFDE931F95FE730038BE75 /* PBXContainerItemProxy */; - }; - 23EFDEA31F95FE990038BE75 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 236350801F95EC3000B3A16A /* CoreDataPlus tvOS */; - targetProxy = 23EFDEA21F95FE990038BE75 /* PBXContainerItemProxy */; - }; - 23EFDEB21F95FEB50038BE75 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 2363509C1F95EC5600B3A16A /* CoreDataPlus macOS */; - targetProxy = 23EFDEB11F95FEB50038BE75 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 06968AD626454AFF00088D76 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = YES; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "Support/Info-Tests.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.alessandromarzoli.CoreDataPlus-Tests-watchOS"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 7.4; - }; - name = Debug; - }; - 06968AD726454AFF00088D76 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = YES; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "Support/Info-Tests.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.alessandromarzoli.CoreDataPlus-Tests-watchOS"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 7.4; - }; - name = Release; - }; 06AF615726F091370090A61B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1952,26 +859,32 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 5.0.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator driverkit iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,3,4,5,6"; + TARGETED_DEVICE_FAMILY = "1,2,3,4,5,6,7"; + TVOS_DEPLOYMENT_TARGET = 16.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -1989,25 +902,31 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 5.0.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator driverkit iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,3,4,5,6"; + TARGETED_DEVICE_FAMILY = "1,2,3,4,5,6,7"; + TVOS_DEPLOYMENT_TARGET = 16.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; @@ -2022,22 +941,25 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = XQEAXK7Y68; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlusTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator driverkit iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,4,6"; + TARGETED_DEVICE_FAMILY = "1,2,4,6,7"; + TVOS_DEPLOYMENT_TARGET = 16.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -2052,224 +974,42 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = XQEAXK7Y68; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlusTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator driverkit iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,4,6"; - }; - name = Release; - }; - 2363507A1F95EC1A00B3A16A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Support/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; - PRODUCT_NAME = CoreDataPlus; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Debug; - }; - 2363507B1F95EC1A00B3A16A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Support/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; - PRODUCT_NAME = CoreDataPlus; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Release; - }; - 236350931F95EC3000B3A16A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "Support/Info-tvOS.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; - PRODUCT_NAME = CoreDataPlus; - SDKROOT = appletvos; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Debug; - }; - 236350941F95EC3000B3A16A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "Support/Info-tvOS.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; - PRODUCT_NAME = CoreDataPlus; - SDKROOT = appletvos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,4,6,7"; + TVOS_DEPLOYMENT_TARGET = 16.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; - 236350AF1F95EC5600B3A16A /* Debug */ = { + 06B3A5A72BD7E447009217C0 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = Support/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; - PRODUCT_NAME = CoreDataPlus; - SDKROOT = macosx; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; + DEVELOPMENT_TEAM = TUY3F9922N; + PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; - 236350B01F95EC5600B3A16A /* Release */ = { + 06B3A5A82BD7E447009217C0 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = Support/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; - PRODUCT_NAME = CoreDataPlus; - SDKROOT = macosx; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; + DEVELOPMENT_TEAM = TUY3F9922N; + PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; @@ -2342,8 +1082,10 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; + DRIVERKIT_DEPLOYMENT_TARGET = 23.2; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -2358,8 +1100,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MACOSX_DEPLOYMENT_TARGET = 10.14; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -2367,10 +1109,11 @@ SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 12.0; + TVOS_DEPLOYMENT_TARGET = 16.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 5.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; + XROS_DEPLOYMENT_TARGET = 1.0; }; name = Debug; }; @@ -2411,8 +1154,10 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DRIVERKIT_DEPLOYMENT_TARGET = 23.2; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -2421,247 +1166,26 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MACOSX_DEPLOYMENT_TARGET = 10.14; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 12.0; + TVOS_DEPLOYMENT_TARGET = 16.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Release; - }; - 23B5807F1F94FEDF00A365C0 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - BITCODE_GENERATION_MODE = marker; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Support/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 5.0.0; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; - PRODUCT_NAME = CoreDataPlus; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Debug; - }; - 23B580801F94FEDF00A365C0 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - BITCODE_GENERATION_MODE = bitcode; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = Support/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 5.0.0; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.alessandromarzoli.CoreDataPlus; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; - PRODUCT_NAME = CoreDataPlus; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Release; - }; - 23EFDE961F95FE730038BE75 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "Support/Info-Tests.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = "com.alessandromarzoli.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Debug; - }; - 23EFDE971F95FE730038BE75 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "Support/Info-Tests.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = "com.alessandromarzoli.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Release; - }; - 23EFDEA51F95FE990038BE75 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "Support/Info-Tests.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = "com.alessandromarzoli.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = appletvos; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Debug; - }; - 23EFDEA61F95FE990038BE75 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "Support/Info-Tests.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = "com.alessandromarzoli.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = appletvos; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Release; - }; - 23EFDEB41F95FEB50038BE75 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "Support/Info-Tests.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = "com.alessandromarzoli.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; - }; - name = Debug; - }; - 23EFDEB51F95FEB50038BE75 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "Support/Info-Tests.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = "com.alessandromarzoli.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 12.0; - WATCHOS_DEPLOYMENT_TARGET = 5.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; + XROS_DEPLOYMENT_TARGET = 1.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 06968AD826454AFF00088D76 /* Build configuration list for PBXNativeTarget "CoreDataPlus Tests watchOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 06968AD626454AFF00088D76 /* Debug */, - 06968AD726454AFF00088D76 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 06AF615B26F091370090A61B /* Build configuration list for PBXNativeTarget "CoreDataPlus" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2680,29 +1204,11 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 236350791F95EC1A00B3A16A /* Build configuration list for PBXNativeTarget "CoreDataPlus watchOS" */ = { + 06B3A5A92BD7E447009217C0 /* Build configuration list for PBXAggregateTarget "Swift-Format Lint" */ = { isa = XCConfigurationList; buildConfigurations = ( - 2363507A1F95EC1A00B3A16A /* Debug */, - 2363507B1F95EC1A00B3A16A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 236350921F95EC3000B3A16A /* Build configuration list for PBXNativeTarget "CoreDataPlus tvOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 236350931F95EC3000B3A16A /* Debug */, - 236350941F95EC3000B3A16A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 236350AE1F95EC5600B3A16A /* Build configuration list for PBXNativeTarget "CoreDataPlus macOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 236350AF1F95EC5600B3A16A /* Debug */, - 236350B01F95EC5600B3A16A /* Release */, + 06B3A5A72BD7E447009217C0 /* Debug */, + 06B3A5A82BD7E447009217C0 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -2734,45 +1240,21 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 23B5807E1F94FEDF00A365C0 /* Build configuration list for PBXNativeTarget "CoreDataPlus iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 23B5807F1F94FEDF00A365C0 /* Debug */, - 23B580801F94FEDF00A365C0 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 23EFDE951F95FE730038BE75 /* Build configuration list for PBXNativeTarget "CoreDataPlus Tests iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 23EFDE961F95FE730038BE75 /* Debug */, - 23EFDE971F95FE730038BE75 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 23EFDEA41F95FE990038BE75 /* Build configuration list for PBXNativeTarget "CoreDataPlus Tests tvOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 23EFDEA51F95FE990038BE75 /* Debug */, - 23EFDEA61F95FE990038BE75 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 23EFDEB31F95FEB50038BE75 /* Build configuration list for PBXNativeTarget "CoreDataPlus Tests macOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 23EFDEB41F95FEB50038BE75 /* Debug */, - 23EFDEB51F95FEB50038BE75 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ + 06DED8072BF36795005EFEA5 /* SampleModel3.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 06DED8082BF36795005EFEA5 /* SampleModel3_v2.xcdatamodel */, + 06DED8092BF36795005EFEA5 /* SampleModel3_v3.xcdatamodel */, + 06DED80A2BF36795005EFEA5 /* SampleModel3.xcdatamodel */, + ); + currentVersion = 06DED80A2BF36795005EFEA5 /* SampleModel3.xcdatamodel */; + path = SampleModel3.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; 23DAFA231F9641BD0058654B /* SampleModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( diff --git a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/Cleanup Whitespace.xcscheme b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/Cleanup Whitespace.xcscheme index 0ff47193..2667bc2f 100644 --- a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/Cleanup Whitespace.xcscheme +++ b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/Cleanup Whitespace.xcscheme @@ -1,6 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus macOS.xcscheme b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus macOS.xcscheme deleted file mode 100644 index 305b487d..00000000 --- a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus macOS.xcscheme +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus tvOS.xcscheme b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus tvOS.xcscheme deleted file mode 100644 index 8e534ff1..00000000 --- a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus tvOS.xcscheme +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus watchOS.xcscheme b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus watchOS.xcscheme deleted file mode 100644 index 2b53f536..00000000 --- a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus watchOS.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus.xcscheme b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus.xcscheme index 3c126c81..91720fb4 100644 --- a/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus.xcscheme +++ b/CoreDataPlus.xcodeproj/xcshareddata/xcschemes/CoreDataPlus.xcscheme @@ -1,6 +1,6 @@ ' do @@ -78,7 +78,7 @@ Once you have your Swift package set up, adding CoreDataPlus as a dependency is ```swift dependencies: [ - .package(url: "https://github.com/alemar11/CoreDataPlus.git", .upToNextMajor(from: "5.0.0")) + .package(url: "https://github.com/alemar11/CoreDataPlus.git", .upToNextMajor(from: "6.0.0")) ] ``` diff --git a/Sources/Collection+CoreData.swift b/Sources/Collection+CoreData.swift index 91a4f6be..61fe820c 100644 --- a/Sources/Collection+CoreData.swift +++ b/Sources/Collection+CoreData.swift @@ -35,22 +35,25 @@ extension Collection where Element: NSManagedObject { // objects without context can trigger their fault one by one guard let context = context else { // important bits - objects.forEach { $0.materialize() } + for object in objects { + object.materialize() + } continue } // objects not yet saved can trigger their fault one by one let temporaryObjects = objects.filter { $0.hasTemporaryID } if !temporaryObjects.isEmpty { - temporaryObjects.forEach { $0.materialize() } + for object in temporaryObjects { + object.materialize() + } } // avoid multiple fetches for subclass entities. let entities = objects.entities().entitiesKeepingOnlyCommonAncestorEntities() for entity in entities { - // important bits - // about batch faulting: + // important bits about batch faulting: // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/Performance.html let request = NSFetchRequest() request.entity = entity @@ -63,7 +66,7 @@ extension Collection where Element: NSManagedObject { /// Returns all the different `NSEntityDescription` defined in the collection. public func entities() -> Set { - return Set(self.map { $0.entity }) + Set(self.map { $0.entity }) } } @@ -72,10 +75,10 @@ extension Collection where Element: NSManagedObject { extension Collection where Element: NSEntityDescription { /// Returns a collection of `NSEntityDescription` with only the commong entity ancestors. internal func entitiesKeepingOnlyCommonAncestorEntities() -> Set { - let grouped = Dictionary(grouping: self) { return $0.topMostAncestorEntity } + let grouped = Dictionary(grouping: self) { $0.topMostAncestorEntity } var result = [NSEntityDescription]() - grouped.forEach { _, entities in + for (_, entities) in grouped { let set = Set(entities) let test = set.reduce([]) { (result, entity) -> [NSEntityDescription] in var newResult = result @@ -87,7 +90,7 @@ extension Collection where Element: NSEntityDescription { newResult.remove(at: index) newResult.append(ancestor) } - } else { // this condition should be never verified + } else { // this condition should be never verified newResult.append(entity) } } diff --git a/Sources/FetchedResultsChanges.swift b/Sources/FetchedResultsChanges.swift index 46ec0bc8..f8cc3130 100644 --- a/Sources/FetchedResultsChanges.swift +++ b/Sources/FetchedResultsChanges.swift @@ -36,7 +36,10 @@ extension FetchedResultsObjectChange { /// - indexPath: The old index patch for the object /// - type: The type of the reported change /// - newIndexPath: The new index path for the object - public init?(object: Any, indexPath: IndexPath?, changeType type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + public init?(object: Any, + indexPath: IndexPath?, + changeType type: NSFetchedResultsChangeType, + newIndexPath: IndexPath?) { guard let object = object as? T else { return nil } switch (type, indexPath, newIndexPath) { @@ -46,17 +49,17 @@ extension FetchedResultsObjectChange { // For more discussion, see https://forums.developer.apple.com/thread/12184 return nil - case let (.insert, nil, newIndexPath?): + case (.insert, nil, let newIndexPath?): self = .insert(object: object, indexPath: newIndexPath) - case let (.delete, indexPath?, nil): + case (.delete, let indexPath?, nil): self = .delete(object: object, indexPath: indexPath) - case let (.update, indexPath?, _): + case (.update, let indexPath?, _): // before iOS 9, a newIndexPath value was also passed in. self = .update(object: object, indexPath: indexPath) - case let (.move, fromIndexPath?, toIndexPath?): + case (.move, let fromIndexPath?, let toIndexPath?): // There are at least two different .move-related bugs running on Xcode 7.3.1: // - iOS 8.4 sometimes reports both an .update and a .move (with identical index paths) for the same object. // - iOS 9.3 sometimes reports just a .move (with identical index paths) and no .update for an object. @@ -94,10 +97,10 @@ public struct FetchedResultsSectionInfo { /// Create a new element of `FetchedResultsSectionInfo` for a given `NSFetchedResultsSectionInfo` object. public init(_ info: NSFetchedResultsSectionInfo) { - objects = (info.objects as? [T]) ?? [] - name = info.name - indexTitle = info.indexTitle - numberOfObjects = info.numberOfObjects + self.objects = (info.objects as? [T]) ?? [] + self.name = info.name + self.indexTitle = info.indexTitle + self.numberOfObjects = info.numberOfObjects } } @@ -121,7 +124,9 @@ extension FetchedResultsSectionChange { /// - sectionInfo: The `NSFetchedResultsSectionInfo` instance /// - sectionIndex: The section index /// - type: The type of the reported change - public init?(section sectionInfo: NSFetchedResultsSectionInfo, index sectionIndex: Int, changeType type: NSFetchedResultsChangeType) { + public init?(section sectionInfo: NSFetchedResultsSectionInfo, + index sectionIndex: Int, + changeType type: NSFetchedResultsChangeType) { let info = FetchedResultsSectionInfo(sectionInfo) switch type { diff --git a/Sources/Migration/LegacyMigration.swift b/Sources/Migration/LegacyMigration.swift new file mode 100644 index 00000000..be5bf282 --- /dev/null +++ b/Sources/Migration/LegacyMigration.swift @@ -0,0 +1,102 @@ +// CoreDataPlus + +import CoreData + +/// Handles migrations with the old `NSMigrationManager`. +public protocol LegacyMigration { + /// Returns a list of mapping models needed to migrate the current version of the database to the next one. + func mappingModelsToNextModelVersion() -> [NSMappingModel]? +} + +// MARK: - MigrationStep + +extension ModelVersion where Self: LegacyMigration { + /// Returns a list of `MigrationStep` needed to mirate to the next `version` of the store. + public func migrationSteps(to version: Self) -> [LegacyMigrationStep] { + guard self != version else { + return [] + } + + guard let nextVersion = next else { + return [] + } + + guard let step = LegacyMigrationStep(sourceVersion: self, destinationVersion: nextVersion) else { + fatalError("Couldn't find any mapping models.") + } + + return [step] + nextVersion.migrationSteps(to: version) + } + + /// Returns a `NSMappingModel` that specifies how to map a model to the next version model. + public func mappingModelToNextModelVersion() -> NSMappingModel? { + guard let nextVersion = next else { + return nil + } + + guard + let mappingModel = NSMappingModel( + from: [modelBundle], forSourceModel: managedObjectModel(), destinationModel: nextVersion.managedObjectModel()) + else { + fatalError("No NSMappingModel found for \(self) to \(nextVersion).") + } + + return mappingModel + } + + /// Returns a newly created mapping model that will migrate data from the source to the destination model. + /// + /// - Note: + /// A model will be created only if all changes are simple enough to be able to reasonably infer a mapping such as: + /// + /// - Adding, removing, and renaming attributes + /// - Adding, removing, and renaming relationships + /// - Adding, removing, and renaming entities + /// - Changing the optional status of attributes + /// - Adding or removing indexes on attributes + /// - Adding, removing, or changing compound indexes on entities + /// - Adding, removing, or changing unique constraints on entities + /// + /// There are a few gotchas to this list: + /// + /// - if you change an attribute from optional to non-optional, specify a default value. + /// - changing indexes (on attributes as well as compound indexes) won’t be picked up as a model change; specify a hash modifier on the changed + /// attributes or entities in order to force Core Data to do the right thing during migration. + public func inferredMappingModelToNextModelVersion() -> NSMappingModel? { + guard let nextVersion = next else { + return nil + } + + return try? NSMappingModel.inferredMappingModel(forSourceModel: managedObjectModel(), + destinationModel: nextVersion.managedObjectModel()) + } + + /// - Returns: a list of `NSMappingModel` from a list of mapping model names. + /// - Note: The mapping models must be inside the NSBundle object containing the model file. + public func mappingModels(for mappingModelNames: [String]) -> [NSMappingModel] { + var results = [NSMappingModel]() + + guard mappingModelNames.count > 0 else { + return results + } + + guard + let allMappingModelsURLs = modelBundle.urls(forResourcesWithExtension: ModelVersionFileExtension.cdm, + subdirectory: nil), + allMappingModelsURLs.count > 0 + else { + return results + } + + for name in mappingModelNames { + let expectedFileName = "\(name).\(ModelVersionFileExtension.cdm)" + if let url = allMappingModelsURLs.first(where: { $0.lastPathComponent == expectedFileName }), + let mappingModel = NSMappingModel(contentsOf: url) + { + results.append(mappingModel) + } + } + + return results + } +} diff --git a/Sources/Migration/MigrationStep.swift b/Sources/Migration/LegacyMigrationStep.swift similarity index 83% rename from Sources/Migration/MigrationStep.swift rename to Sources/Migration/LegacyMigrationStep.swift index 05190604..4b39b3e1 100644 --- a/Sources/Migration/MigrationStep.swift +++ b/Sources/Migration/LegacyMigrationStep.swift @@ -2,8 +2,8 @@ import CoreData -// Representation of a Core Data migration step. -public final class MigrationStep { +// Representation of a Core Data legacy migration step. +public final class LegacyMigrationStep { public let sourceVersion: Version public let sourceModel: NSManagedObjectModel public let destinationVersion: Version diff --git a/Sources/Migration/LightweightMigrationManager.swift b/Sources/Migration/LightweightMigrationManager.swift deleted file mode 100644 index debd34da..00000000 --- a/Sources/Migration/LightweightMigrationManager.swift +++ /dev/null @@ -1,146 +0,0 @@ -// CoreDataPlus - -import CoreData - -/// A `NSMigrationManager` proxy for lightweight migrations with a customizable faking `migrationProgress`. -public final class LightweightMigrationManager: NSMigrationManager { - /// An estimated interval (with a 10% tolerance) to carry out the migration (default: 60 seconds). - public var estimatedTime: TimeInterval = 60 - /// How often the progress is updated (default: 1 second). - public var updateProgressInterval: TimeInterval = 1 - - private let manager: NSMigrationManager - private let totalUnitCount: Int64 = 100 - private lazy var fakeTotalUnitCount: Float = { Float(totalUnitCount) * 0.9 }() // 10% tolerance - private var fakeProgress: Float = 0 // 0 to 1 - - public override var usesStoreSpecificMigrationManager: Bool { - get { manager.usesStoreSpecificMigrationManager } - // swiftlint:disable:next unused_setter_value - set { fatalError("usesStoreSpecificMigrationManager can't be set for lightweight migrations.") } - } - - public override var mappingModel: NSMappingModel { manager.mappingModel } - public override var sourceModel: NSManagedObjectModel { manager.sourceModel } - public override var destinationModel: NSManagedObjectModel { manager.destinationModel } - public override var sourceContext: NSManagedObjectContext { manager.sourceContext } - public override var destinationContext: NSManagedObjectContext { manager.destinationContext } - - public override init(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel) { - self.manager = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel) - self.manager.usesStoreSpecificMigrationManager = true // default - super.init() - } - - public override func migrateStore(from sourceURL: URL, - sourceType sStoreType: String, - options sOptions: [AnyHashable: Any]? = nil, - with mappings: NSMappingModel?, - toDestinationURL dURL: URL, - destinationType dStoreType: String, - destinationOptions dOptions: [AnyHashable: Any]? = nil) throws { - let tick = Float(updateProgressInterval / estimatedTime) // progress increment tick - let queue = DispatchQueue(label: "\(bundleIdentifier).\(String(describing: Self.self)).Progress", qos: .utility) - var progressUpdater: () -> Void = {} - progressUpdater = { [weak self] in - guard let self = self else { return } - guard self.fakeProgress < 1 else { return } - - if self.fakeProgress > 0 { - let fakeCompletedUnitCount = self.fakeTotalUnitCount * self.fakeProgress - self.migrationProgress = fakeCompletedUnitCount / self.fakeTotalUnitCount - } - self.fakeProgress += tick - - queue.asyncAfter(deadline: .now() + self.updateProgressInterval, execute: progressUpdater) - } - queue.async(execute: progressUpdater) - - do { - try manager.migrateStore(from: sourceURL, - sourceType: sStoreType, - options: sOptions, - with: mappings, - toDestinationURL: dURL, - destinationType: dStoreType, - destinationOptions: dOptions) - } catch { - // stop the fake progress - queue.sync { fakeProgress = 1 } - // reset the migrationProgress (as expected for NSMigrationManager instances) - // before throwing the error without firing KVO. - // Although cancelling ligthweight migrations is ignored; - // see comments in cancelMigrationWithError(_:) - migrationProgress = 0 - throw error - } - - queue.sync { fakeProgress = 1 } - migrationProgress = 1.0 - // a NSMigrationManager instance may be used for multiple migrations; - // the migrationProgress should be reset without firing KVO. - migrationProgress = 0 - } - - public override func sourceEntity(for mEntity: NSEntityMapping) -> NSEntityDescription? { - manager.sourceEntity(for: mEntity) - } - - public override func destinationEntity(for mEntity: NSEntityMapping) -> NSEntityDescription? { - manager.destinationEntity(for: mEntity) - } - - public override func reset() { - manager.reset() - } - - public override func associate(sourceInstance: NSManagedObject, withDestinationInstance destinationInstance: NSManagedObject, for entityMapping: NSEntityMapping) { - manager.associate(sourceInstance: sourceInstance, withDestinationInstance: destinationInstance, for: entityMapping) - } - - public override func destinationInstances(forEntityMappingName mappingName: String, sourceInstances: [NSManagedObject]?) -> [NSManagedObject] { - manager.destinationInstances(forEntityMappingName: mappingName, sourceInstances: sourceInstances) - } - - public override func sourceInstances(forEntityMappingName mappingName: String, destinationInstances: [NSManagedObject]?) -> [NSManagedObject] { - manager.sourceInstances(forEntityMappingName: mappingName, destinationInstances: destinationInstances) - } - - public override var currentEntityMapping: NSEntityMapping { manager.currentEntityMapping } - - public override class func automaticallyNotifiesObservers(forKey key: String) -> Bool { - // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-SW3 - if key == #keyPath(NSMigrationManager.migrationProgress) { - return false - } - return super.automaticallyNotifiesObservers(forKey: key) - } - - private var _migrationProgress: Float = 0.0 - - public override var migrationProgress: Float { - get { _migrationProgress } - set { - guard _migrationProgress != newValue else { return } - - if newValue == 0 { // reset if manager is reused - _migrationProgress = newValue - } else { - willChangeValue(forKey: #keyPath(NSMigrationManager.migrationProgress)) - _migrationProgress = newValue - didChangeValue(forKey: #keyPath(NSMigrationManager.migrationProgress)) - } - } - } - - public override var userInfo: [AnyHashable: Any]? { - get { manager.userInfo } - set { manager.userInfo = newValue } - } - - public override func cancelMigrationWithError(_ error: Error) { - // During my tests, cancelling a lightweight migration doesn't work - // probably due to performance optimizations - manager.cancelMigrationWithError(error) - } -} diff --git a/Sources/Migration/MigrationProgressReporter.swift b/Sources/Migration/MigrationProgressReporter.swift index 259cd039..f5328338 100644 --- a/Sources/Migration/MigrationProgressReporter.swift +++ b/Sources/Migration/MigrationProgressReporter.swift @@ -1,24 +1,26 @@ // CoreDataPlus import CoreData +import os.lock /// Provides a `Progress` instance during a `NSMigrationManager` migration phase. -public final class MigrationProgressReporter: NSObject, ProgressReporting { +internal final class MigrationProgressReporter: NSObject, ProgressReporting, @unchecked Sendable { /// Migration progress. public private(set) lazy var progress: Progress = { let progress = Progress(totalUnitCount: Int64(totalUnitCount)) progress.cancellationHandler = { [weak self] in self?.cancel() } - progress.pausingHandler = nil // not supported + progress.pausingHandler = nil // not supported return progress }() private let totalUnitCount: Int64 = 100 private let manager: NSMigrationManager private var token: NSKeyValueObservation? + private let lock = OSAllocatedUnfairLock() - public init(manager: NSMigrationManager) { + fileprivate init(manager: NSMigrationManager) { self.manager = manager super.init() self.token = manager.observe(\.migrationProgress, options: [.new]) { [weak self] (_, change) in @@ -40,19 +42,25 @@ public final class MigrationProgressReporter: NSObject, ProgressReporting { /// Marks the progress as finished if it's not already. /// - Note: Since lightweight migrations don't support progress, this method ensures that a lightweight migration is at least finished. - public func markAsFinishedIfNeeded() { + internal func markAsFinishedIfNeeded() { + lock.lock() + defer { lock.unlock() } + if !progress.isFinished { progress.completedUnitCount = progress.totalUnitCount } } - func cancel() { + internal func cancel() { + lock.lock() + defer { lock.unlock() } + let error = NSError.migrationCancelled manager.cancelMigrationWithError(error) } } -public extension NSMigrationManager { +extension NSMigrationManager { /// Creates a new `MigrationProgressReporter` for the migration manager. func makeProgressReporter() -> MigrationProgressReporter { MigrationProgressReporter(manager: self) diff --git a/Sources/Migration/Migrator.swift b/Sources/Migration/Migrator.swift index b307b301..687e95ba 100644 --- a/Sources/Migration/Migrator.swift +++ b/Sources/Migration/Migrator.swift @@ -38,7 +38,7 @@ import CoreData import os.log /// An object that handles a multi step CoreData migration for a `SQLite` store. -public final class Migrator: NSObject, ProgressReporting { +public final class Migrator: NSObject, ProgressReporting { /// Multi step migration progress. public private(set) lazy var progress: Progress = { // We don't need to manage any cancellations here: @@ -53,42 +53,46 @@ public final class Migrator: NSObject, ProgressReporting public var enableLog: Bool = false { didSet { if enableLog { - log = OSLog(subsystem: bundleIdentifier, category: "Migrator") + log = Logger(subsystem: bundleIdentifier, category: "Migrator") } else { - log = .disabled + log = .init(.disabled) } } } - private var log: OSLog = .disabled + private var log: Logger = .init(.disabled) /// Source description used as starting point for the migration steps. - let sourceStoreDescription: NSPersistentStoreDescription + internal let sourceStoreDescription: NSPersistentStoreDescription /// Desitnation description used as final point for the migrations steps. - let destinationStoreDescription: NSPersistentStoreDescription + internal let destinationStoreDescription: NSPersistentStoreDescription /// `Version` to which the database needs to be migrated. - let targetVersion: Version + internal let targetVersion: Version /// Creates a `Migrator` instance to handle a multi step migration to a given `Version` /// - Parameters: /// - sourceStoreDescription: Initial persistent store description. /// - destinationStoreDescription: Final persistent store description. /// - targetVersion: `Version` to which the database needs to be migrated. - public required init(sourceStoreDescription: NSPersistentStoreDescription, - destinationStoreDescription: NSPersistentStoreDescription, - targetVersion: Version) { + public required init( + sourceStoreDescription: NSPersistentStoreDescription, + destinationStoreDescription: NSPersistentStoreDescription, + targetVersion: Version + ) { self.sourceStoreDescription = sourceStoreDescription self.destinationStoreDescription = destinationStoreDescription self.targetVersion = targetVersion super.init() - _ = progress // lazy init for implicit progress support + _ = progress // lazy init for implicit progress support } /// Creates a `Migrator` instance to handle a multi step migration at `targetStoreDescription` to a given `Version`. public convenience init(targetStoreDescription: NSPersistentStoreDescription, targetVersion: Version) { - self.init(sourceStoreDescription: targetStoreDescription, destinationStoreDescription: targetStoreDescription, targetVersion: targetVersion) + self.init( + sourceStoreDescription: targetStoreDescription, destinationStoreDescription: targetStoreDescription, + targetVersion: targetVersion) } /// Migrates the store to a given `Version`, performing a WAL checkpoint if opted in. @@ -98,144 +102,170 @@ public final class Migrator: NSObject, ProgressReporting /// A dead lock can occur if a NSPersistentStore with a different journaling mode is currently active and using the database file. /// - managerProvider: Closure to provide a custom `NSMigrationManager` instance. /// - Throws: It throws an error if the migration fails. - public func migrate(enableWALCheckpoint: Bool = false, managerProvider: ((Metadata) -> NSMigrationManager)? = nil) throws { - guard let sourceURL = sourceStoreDescription.url else { fatalError("Source NSPersistentStoreDescription requires a URL.") } - guard let destinationURL = destinationStoreDescription.url else { fatalError("Destination NSPersistentStoreDescription requires a URL.") } - - try migrateStore(from: sourceURL, - sourceOptions: sourceStoreDescription.options, - to: destinationURL, - destinationOptions: destinationStoreDescription.options, - targetVersion: targetVersion, - enableWALCheckpoint: enableWALCheckpoint, - managerProvider: managerProvider) + public func migrate(enableWALCheckpoint: Bool = false, managerProvider: ((Metadata) -> NSMigrationManager)? = nil) + throws + { + guard let sourceURL = sourceStoreDescription.url else { + fatalError("Source NSPersistentStoreDescription requires a URL.") + } + guard let destinationURL = destinationStoreDescription.url else { + fatalError("Destination NSPersistentStoreDescription requires a URL.") + } + + try migrateStore( + from: sourceURL, + sourceOptions: sourceStoreDescription.options, + to: destinationURL, + destinationOptions: destinationStoreDescription.options, + targetVersion: targetVersion, + enableWALCheckpoint: enableWALCheckpoint, + managerProvider: managerProvider) } } extension Migrator { /// Migrates the store at a given source URL to the store at a given destination URL, performing all the migration steps to the target version. - fileprivate func migrateStore(from sourceURL: URL, - sourceOptions: PersistentStoreOptions? = nil, - to destinationURL: URL, - destinationOptions: PersistentStoreOptions? = nil, - targetVersion: Version, - enableWALCheckpoint: Bool = false, - managerProvider: ((Metadata) -> NSMigrationManager)? = nil) throws { - os_log(.info, log: log, "Migrator has started, initial store at: %{public}@.", sourceURL as CVarArg) + fileprivate func migrateStore( + from sourceURL: URL, + sourceOptions: PersistentStoreOptions? = nil, + to destinationURL: URL, + destinationOptions: PersistentStoreOptions? = nil, + targetVersion: Version, + enableWALCheckpoint: Bool = false, + managerProvider: ((Metadata) -> NSMigrationManager)? = nil + ) throws { + log.info("Migrator has started, initial store at: \(sourceURL, privacy: .public)") let start = DispatchTime.now() guard let sourceVersion = try Version(persistentStoreURL: sourceURL) else { - let message = "A ModelVersion could not be found for the initial store at: \(sourceURL)." - os_log(.error, log: log, "%{public}s", message) - fatalError(message) + log.error("A ModelVersion could not be found for the initial store at: \(sourceURL).") + fatalError("A ModelVersion could not be found for the initial store at: \(sourceURL).") } guard try CoreDataPlus.isMigrationNecessary(for: sourceURL, to: targetVersion) else { - os_log(.info, log: log, "Migration to %{public}s is not necessary.", "\(targetVersion.rawValue)") + log.info("Migration to \(targetVersion.debugDescription, privacy: .public) is not necessary.") return } if enableWALCheckpoint { - os_log(.debug, log: log, "Performing a WAL checkpoint.") + log.debug("Performing a WAL checkpoint.") // A dead lock can occur if a NSPersistentStore with a different journaling mode // is currently active and using the database file. // You need to remove it before performing a WAL checkpoint. - try performWALCheckpointForStore(at: sourceURL, storeOptions: sourceOptions, model: sourceVersion.managedObjectModel()) + try performWALCheckpointForStore(at: sourceURL, + storeOptions: sourceOptions, + model: sourceVersion.managedObjectModel()) } let steps = sourceVersion.migrationSteps(to: targetVersion) - os_log(.debug, log: log, "Number of steps: %{public}d", steps.count) + log.debug("Number of steps: \(steps.count, privacy: .public)") guard steps.count > 0 else { return } - let migrationStepsProgress = Progress(totalUnitCount: Int64(steps.count), parent: progress, pendingUnitCount: progress.totalUnitCount) + let migrationStepsProgress = Progress( + totalUnitCount: Int64(steps.count), parent: progress, pendingUnitCount: progress.totalUnitCount) var currentURL = sourceURL - try steps.enumerated().forEach { (stepIndex, step) in - os_log(.info, log: log, "Step %{public}d (of %{public}d) started; %{public}s to %{public}s.", stepIndex + 1, steps.count, "\(step.sourceVersion)", "\(step.destinationVersion)") + + for (stepIndex, step) in steps.enumerated() { + // swiftlint:disable:next line_length + log.info( + "Step \(stepIndex + 1, privacy: .public) (of \(steps.count, privacy: .public)) started: \(step.sourceVersion.debugDescription, privacy: .public) to \(step.destinationVersion.debugDescription, privacy: .public)" + ) try autoreleasepool { - let temporaryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString).appendingPathExtension("sqlite") + let temporaryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent( + UUID().uuidString + ) + .appendingPathExtension("sqlite") let mappingModelMigrationProgress = Progress(totalUnitCount: Int64(step.mappingModels.count)) migrationStepsProgress.addChild(mappingModelMigrationProgress, withPendingUnitCount: 1) + for (mappingModelIndex, mappingModel) in step.mappingModels.enumerated() { - os_log(.info, log: log, "Starting migration for mapping model %{public}d.", mappingModelIndex + 1) - os_log(.debug, log: log, "The store at: %{public}@ will be migrated in a temporary store at: %{public}@.", currentURL as CVarArg, temporaryURL as CVarArg) - let metadata = Metadata(sourceVersion: step.sourceVersion, - sourceModel: step.sourceModel, - destinationVersion: step.destinationVersion, - destinationModel: step.destinationModel, - mappingModel: mappingModel) - let manager = managerProvider?(metadata) ?? NSMigrationManager(sourceModel: step.sourceModel, destinationModel: step.destinationModel) - mappingModelMigrationProgress.becomeCurrent(withPendingUnitCount: 1) + log.info("Starting migration for mapping model \(mappingModelIndex + 1, privacy: .public).") + log.debug( + "The store at: \(currentURL, privacy: .public) will be migrated in a temporary store at: \(temporaryURL, privacy: .public)" + ) + + let metadata = Metadata( + sourceVersion: step.sourceVersion, + sourceModel: step.sourceModel, + destinationVersion: step.destinationVersion, + destinationModel: step.destinationModel, + mappingModel: mappingModel) + let manager = + managerProvider?(metadata) + ?? NSMigrationManager(sourceModel: step.sourceModel, destinationModel: step.destinationModel) // a progress reporter handles a parent progress cancellations automatically let progressReporter = manager.makeProgressReporter() - mappingModelMigrationProgress.resignCurrent() + mappingModelMigrationProgress.addChild(progressReporter.progress, withPendingUnitCount: 1) + let start = DispatchTime.now() + do { - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - try manager.migrateStore(from: currentURL, - type: .sqlite, - options: sourceOptions, - mapping: mappingModel, - to: temporaryURL, - type: .sqlite, - options: destinationOptions) - } else { - try manager.migrateStore(from: currentURL, - sourceType: NSSQLiteStoreType, - options: sourceOptions, - with: mappingModel, - toDestinationURL: temporaryURL, - destinationType: NSSQLiteStoreType, - destinationOptions: destinationOptions) - } + try manager.migrateStore( + from: currentURL, + type: .sqlite, + options: sourceOptions, + mapping: mappingModel, + to: temporaryURL, + type: .sqlite, + options: destinationOptions) } catch { - os_log(.error, "Migration for mapping model %{public}d failed: %{private}@", mappingModelIndex + 1, error as NSError) + log.error( + "Migration for mapping model \(mappingModelIndex + 1, privacy: .public) failed: \(error, privacy: .private)" + ) throw error } let end = DispatchTime.now() // Ligthweight migrations don't report progress, the report needs to be marked as finished to proper adjust its progress status. progressReporter.markAsFinishedIfNeeded() let nanoseconds = end.uptimeNanoseconds - start.uptimeNanoseconds - let timeInterval = Double(nanoseconds) / 1_000_000_000 - os_log(.info, log: log, "Migration for mapping model %{public}d finished in %{public}.2f seconds.", mappingModelIndex + 1, timeInterval) + let timeInterval = Double(nanoseconds) / Double(NSEC_PER_SEC) + log.info( + "Migration for mapping model \(mappingModelIndex + 1, privacy: .public) finished in \(timeInterval, format: .fixed(precision: 2), privacy: .public) seconds." + ) } // once the migration is done (and the store is migrated to temporaryURL) // the store at currentURL can be safely destroyed unless it is the // initial store if currentURL != sourceURL { - os_log(.debug, log: log, "Destroying store at %{public}@.", currentURL as CVarArg) + log.debug("Destroying store at \(currentURL, privacy: .public)") try NSPersistentStoreCoordinator.destroyStore(at: currentURL) } currentURL = temporaryURL } - os_log(.info, log: log, "Step %{public}d (of %{public}d) completed.", stepIndex + 1, steps.count) + log.info("Step \(stepIndex + 1, privacy: .public) (of \(steps.count, privacy: .public) completed.") } // move the store at currentURL to (final) destinationURL - os_log(.debug, log: log, "Moving the store at: %{public}@ to final store: %{public}@.", currentURL as CVarArg, destinationURL as CVarArg) + log.debug( + "Moving the store at: \(currentURL, privacy: .public) to final store: \(destinationURL, privacy: .public)") try NSPersistentStoreCoordinator.replaceStore(at: destinationURL, withPersistentStoreFrom: currentURL) // delete the store at currentURL if it's not the initial store if currentURL != sourceURL { - os_log(.debug, log: log, "Destroying store at %{public}@.", currentURL as CVarArg) + log.debug("Destroying store at: \(currentURL, privacy: .public)") try NSPersistentStoreCoordinator.destroyStore(at: currentURL) } // delete the initial store only if the option is set to true if destinationURL != sourceURL { - os_log(.debug, log: log, "Destroying initial store at %{public}@.", sourceURL as CVarArg) + log.debug("Destroying initial store at: \(sourceURL, privacy: .public)") try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } let end = DispatchTime.now() let nanoseconds = end.uptimeNanoseconds - start.uptimeNanoseconds - let timeInterval = Double(nanoseconds) / 1_000_000_000 - os_log(.info, log: log, "Migrator has finished in %{public}.2f seconds, final store at: %{public}@.", timeInterval, destinationURL as CVarArg) + let timeInterval = Double(nanoseconds) / Double(NSEC_PER_SEC) + log.info( + "Migrator has finished in \(timeInterval, format: .fixed(precision: 2), privacy: .public) seconds, final store at: \(destinationURL, privacy: .public)" + ) } } // MARK: - WAL Checkpoint /// Forces Core Data to perform a checkpoint operation, which merges the data in the `-wal` file to the store file. -private func performWALCheckpointForStore(at storeURL: URL, storeOptions: PersistentStoreOptions? = nil, model: NSManagedObjectModel) throws { +private func performWALCheckpointForStore(at storeURL: URL, + storeOptions: PersistentStoreOptions? = nil, + model: NSManagedObjectModel) throws { // "If the -wal file is not present, using this approach to add the store won't cause any exceptions, // but the transactions recorded in the missing -wal file will be lost." (from: https://developer.apple.com/library/archive/qa/qa1809/_index.html) // credits: @@ -244,9 +274,9 @@ private func performWALCheckpointForStore(at storeURL: URL, storeOptions: Persis // https://www.avanderlee.com/swift/write-ahead-logging-wal/ let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: model) var options: PersistentStoreOptions = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]] - if - let persistentHistoryTokenKey = storeOptions?[NSPersistentHistoryTrackingKey] as? NSNumber, - persistentHistoryTokenKey.boolValue { + if let persistentHistoryTokenKey = storeOptions?[NSPersistentHistoryTrackingKey] as? NSNumber, + persistentHistoryTokenKey.boolValue + { // Once NSPersistentHistoryTrackingKey is enabled, it can't be reverted back. // During a WAL checkpoint this step prevents this warning in the console: // "Store opened without NSPersistentHistoryTrackingKey but previously had been opened @@ -255,12 +285,11 @@ private func performWALCheckpointForStore(at storeURL: URL, storeOptions: Persis options[NSPersistentHistoryTrackingKey] = [NSPersistentHistoryTrackingKey: true as NSNumber] } - let store: NSPersistentStore - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - store = try persistentStoreCoordinator.addPersistentStore(type: .sqlite, configuration: nil, at: storeURL, options: options) - } else { - store = try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) - } + let store = try persistentStoreCoordinator.addPersistentStore(type: .sqlite, + configuration: nil, + at: storeURL, + options: options) + try persistentStoreCoordinator.remove(store) } @@ -273,11 +302,13 @@ extension Migrator { let destinationModel: NSManagedObjectModel let mappingModel: NSMappingModel - fileprivate init(sourceVersion: Version, - sourceModel: NSManagedObjectModel, - destinationVersion: Version, - destinationModel: NSManagedObjectModel, - mappingModel: NSMappingModel) { + fileprivate init( + sourceVersion: Version, + sourceModel: NSManagedObjectModel, + destinationVersion: Version, + destinationModel: NSManagedObjectModel, + mappingModel: NSMappingModel + ) { self.sourceVersion = sourceVersion self.sourceModel = sourceModel self.destinationVersion = destinationVersion diff --git a/Sources/Migration/ModelVersion.swift b/Sources/Migration/ModelVersion.swift index 99b2b4b8..f08d58f1 100644 --- a/Sources/Migration/ModelVersion.swift +++ b/Sources/Migration/ModelVersion.swift @@ -10,19 +10,19 @@ import CoreData /// and an Info.plist file that contains the version information. /// The model is compiled into a runtime formatβ€”a file package with a `.momd` extension that contains individually compiled model files with a `.mom` extension /// documentation. -private enum ModelVersionFileExtension { +internal enum ModelVersionFileExtension { /// Extension for a compiled version of a model file package (`.xcdatamodeld`). static let momd = "momd" /// Extension for a compiled version of a *versioned* model file (`.xcdatamodel`). - static let mom = "mom" + static let mom = "mom" /// Extension for an optimized version for the '.mom' file. - static let omo = "omo" + static let omo = "omo" /// Extension for a compiled version of a mapping model file (`.xcmappingmodel`). - static let cdm = "cdm" + static let cdm = "cdm" } /// Types adopting the `ModelVersion` protocol can be used to describe a Core Data Model and its versioning. -public protocol ModelVersion: Equatable, RawRepresentable { +public protocol ModelVersion: Equatable, RawRepresentable, CustomDebugStringConvertible { /// Protocol `ModelVersion`. /// /// List with all versions until now. @@ -41,7 +41,7 @@ public protocol ModelVersion: Equatable, RawRepresentable { /// Protocol `ModelVersion`. /// /// The next `ModelVersion` in the progressive migration. - var successor: Self? { get } + var next: Self? { get } /// Protocol `ModelVersion`. /// @@ -53,17 +53,16 @@ public protocol ModelVersion: Equatable, RawRepresentable { /// Model name. var modelName: String { get } - /// Protocol `ModelVersions`. + /// Protocol `ModelVersion`. /// /// Return the NSManagedObjectModel for this `ModelVersion`. func managedObjectModel() -> NSManagedObjectModel - - /// Protocol `ModelVersion`. - /// - /// Returns a list of mapping models needed to migrate the current version of the database to the next one. - func mappingModelsToNextModelVersion() -> [NSMappingModel]? } + + + + extension ModelVersion { /// Protocol `ModelVersion`. /// @@ -71,6 +70,12 @@ extension ModelVersion { var momd: String { "\(modelName).\(ModelVersionFileExtension.momd)" } } +extension ModelVersion { + public var debugDescription: String { + "\(modelName)β€£\(versionName)" + } +} + extension ModelVersion { /// Searches for the first ModelVersion whose model is compatible with the persistent store metedata public static subscript(_ metadata: [String: Any]) -> Self? { @@ -84,11 +89,9 @@ extension ModelVersion { /// - Throws: It throws an error if no store is found at `persistentStoreURL` or if there is a problem accessing its contents. public init?(persistentStoreURL: URL) throws { let metadata: [String: Any] - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(type: .sqlite, at: persistentStoreURL, options: nil) - } else { - metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: persistentStoreURL, options: nil) - } + metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(type: .sqlite, + at: persistentStoreURL, + options: nil) let version = Self[metadata] guard let modelVersion = version else { @@ -102,12 +105,14 @@ extension ModelVersion { /// /// Returns the NSManagedObjectModel for this `ModelVersion`. public func managedObjectModel() -> NSManagedObjectModel { - return _managedObjectModel() + _managedObjectModel() } - + // swiftlint:disable:next identifier_name internal func _managedObjectModel() -> NSManagedObjectModel { - let momURL = modelBundle.url(forResource: versionName, withExtension: "\(ModelVersionFileExtension.mom)", subdirectory: momd) + let momURL = modelBundle.url(forResource: versionName, + withExtension: "\(ModelVersionFileExtension.mom)", + subdirectory: momd) // As of iOS 11, Apple is advising that opening the .omo file for a managed object model is not supported, since the file format can change from release to release // let omoURL = modelBundle.url(forResource: versionName, withExtension: "\(ModelVersionExtension.omo)", subdirectory: momd) @@ -124,11 +129,11 @@ extension ModelVersion { return model } - /// `AnyIterator` to iterate all the successors steps after `self`. - func successorsIterator() -> AnyIterator { + /// `AnyIterator` to iterate all the nexts steps after `self`. + func makeIterator() -> AnyIterator { var version: Self = self return AnyIterator { - guard let next = version.successor else { return nil } + guard let next = version.next else { return nil } version = next return version @@ -136,6 +141,37 @@ extension ModelVersion { } } +extension ModelVersion { + /// Returns`true` if a lightweight migration to the next model version is possible + /// + /// - Note: + /// Lightweight migrations are possible only if all changes are simple enough to be automaticaly inferred such as: + /// + /// - Adding, removing, and renaming attributes + /// - Adding, removing, and renaming relationships + /// - Adding, removing, and renaming entities + /// - Changing the optional status of attributes + /// - Adding or removing indexes on attributes + /// - Adding, removing, or changing compound indexes on entities + /// - Adding, removing, or changing unique constraints on entities + /// + /// There are a few gotchas to this list: + /// + /// - if you change an attribute from optional to non-optional, specify a default value. + /// - changing indexes (on attributes as well as compound indexes) won’t be picked up as a model change; specify a hash modifier on the changed + /// attributes or entities in order to force Core Data to do the right thing during migration. + public func isLightWeightMigrationPossibleToNextModelVersion() -> Bool { + guard let nextVersion = next else { + return false + } + + let mappingModel = try? NSMappingModel.inferredMappingModel(forSourceModel: managedObjectModel(), + destinationModel: nextVersion.managedObjectModel()) + + return mappingModel != nil + } +} + // MARK: - Migration /// Returns `true` if a migration to a given `ModelVersion` is necessary for the persistent store at a given `URL`. @@ -148,12 +184,9 @@ public func isMigrationNecessary(for storeURL: URL, to ve // Before you initiate a migration process, you should first determine whether it is necessary. // If the target model configuration is compatible with the persistent store metadata, there is no need to migrate // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmCustomizing.html#//apple_ref/doc/uid/TP40004399-CH8-SW2 - let metadata: [String: Any] - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(type: .sqlite, at: storeURL, options: nil) - } else { - metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: storeURL, options: nil) - } + let metadata: [String: Any] = try NSPersistentStoreCoordinator.metadataForPersistentStore(type: .sqlite, + at: storeURL, + options: nil) let targetModel = version.managedObjectModel() // https://vimeo.com/164904652 @@ -172,13 +205,13 @@ public func isMigrationNecessary(for storeURL: URL, to ve let isCompatible = targetModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) if isCompatible { - return false // current and target versions are the same + return false // current and target versions are the same } else if let currentVersion = Version[metadata] { - let iterator = currentVersion.successorsIterator() + let iterator = currentVersion.makeIterator() while let nextVersion = iterator.next() { if nextVersion == version { return true } } - // can't migrate to a version not defined as a successor step + // can't migrate to a version not defined as a next step // (target version is probably a prior version of the current one) return false } else { @@ -186,88 +219,3 @@ public func isMigrationNecessary(for storeURL: URL, to ve return false } } - -extension ModelVersion { - /// Returns a list of `MigrationStep` needed to mirate to the next `version` of the store. - public func migrationSteps(to version: Self) -> [MigrationStep] { - guard self != version else { - return [] - } - - guard let nextVersion = successor else { - return [] - } - - guard let step = MigrationStep(sourceVersion: self, destinationVersion: nextVersion) else { - fatalError("Couldn't find any mapping models.") - } - - return [step] + nextVersion.migrationSteps(to: version) - } - - /// Returns a `NSMappingModel` that specifies how to map a model to the next version model. - public func mappingModelToNextModelVersion() -> NSMappingModel? { - guard let nextVersion = successor else { - return nil - } - - guard let mappingModel = NSMappingModel(from: [modelBundle], forSourceModel: managedObjectModel(), destinationModel: nextVersion.managedObjectModel()) else { - fatalError("No NSMappingModel found for \(self) to \(nextVersion).") - } - - return mappingModel - } - - /// Returns a newly created mapping model that will migrate data from the source to the destination model. - /// - /// - Note: - /// A model will be created only if all changes are simple enough to be able to reasonably infer a mapping such as: - /// - /// - Adding, removing, and renaming attributes - /// - Adding, removing, and renaming relationships - /// - Adding, removing, and renaming entities - /// - Changing the optional status of attributes - /// - Adding or removing indexes on attributes - /// - Adding, removing, or changing compound indexes on entities - /// - Adding, removing, or changing unique constraints on entities - /// - /// There are a few gotchas to this list: - /// - /// - if you change an attribute from optional to non-optional, specify a default value. - /// - changing indexes (on attributes as well as compound indexes) won’t be picked up as a model change; specify a hash modifier on the changed - /// attributes or entities in order to force Core Data to do the right thing during migration. - public func inferredMappingModelToNextModelVersion() -> NSMappingModel? { - guard let nextVersion = successor else { - return nil - } - - return try? NSMappingModel.inferredMappingModel(forSourceModel: managedObjectModel(), destinationModel: nextVersion.managedObjectModel()) - } - - /// - Returns: Returns a list of `NSMappingModel` given a list of mapping model names. - /// - Note: The mapping models must be inside the NSBundle object containing the model file. - public func mappingModels(for mappingModelNames: [String]) -> [NSMappingModel] { - var results = [NSMappingModel]() - - guard mappingModelNames.count > 0 else { - return results - } - - guard - let allMappingModelsURLs = modelBundle.urls(forResourcesWithExtension: ModelVersionFileExtension.cdm, subdirectory: nil), - allMappingModelsURLs.count > 0 else { - return results - } - - mappingModelNames.forEach { name in - let expectedFileName = "\(name).\(ModelVersionFileExtension.cdm)" - if - let url = allMappingModelsURLs.first(where: { $0.lastPathComponent == expectedFileName }), - let mappingModel = NSMappingModel(contentsOf: url) { - results.append(mappingModel) - } - } - - return results - } -} diff --git a/Sources/Migration/StagedMigration.swift b/Sources/Migration/StagedMigration.swift new file mode 100644 index 00000000..54f6e532 --- /dev/null +++ b/Sources/Migration/StagedMigration.swift @@ -0,0 +1,66 @@ +// CoreDataPlus + +import CoreData + +/// Handles migrations with the new `NSStagedMigrationManager`. +/// - Note: `NSStagedMigrationManager` requires `NSMigratePersistentStoresAutomaticallyOption` and `NSInferMappingModelAutomaticallyOption` set to to *true*. +public protocol StagedMigration { + /// Returns the current `NSManagedObjectModelReference`. + @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + func managedObjectModelReference() -> NSManagedObjectModelReference + + /// The Base64-encoded 128-bit model version hash. + @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + var versionChecksum: String { get } + + /// - Returns a `NSMigrationStage` needed to migrate to the next `version` of the store. + @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + func migrationStageToNextModelVersion() -> NSMigrationStage? +} + +// MARK: - NSMigrationStage + +extension ModelVersion where Self: StagedMigration { + @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + public var versionChecksum: String { + managedObjectModel().versionChecksum + } + + /// Protocol `StagedMigration`. + /// + /// Returns the `NSManagedObjectModelReference` for this `ModelVersion`. + @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + public func managedObjectModelReference() -> NSManagedObjectModelReference { + .init(model: managedObjectModel(), versionChecksum: versionChecksum) + } + + /// Protocol `StagedMigration`. + /// + /// Returns a `NSMigrationStage` needed to migrate to the next `version` of the store. + @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + public func migrationStageToNextModelVersion() -> NSMigrationStage? { + return nil + } +} + +// MARK: - StagedMigrationStep + +extension ModelVersion where Self: StagedMigration { + /// Returns a list of `StagedMigrationStep` needed to mirate to the next `version` of the store. + @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + public func stagedMigrationSteps(to version: Self) -> [StagedMigrationStep] { + guard self != version else { + return [] + } + + guard let nextVersion = next else { + return [] + } + + guard let step = StagedMigrationStep(sourceVersion: self, destinationVersion: nextVersion) else { + fatalError("Couldn't find any mapping stages.") + } + + return [step] + nextVersion.stagedMigrationSteps(to: version) + } +} diff --git a/Sources/Migration/StagedMigrationStep.swift b/Sources/Migration/StagedMigrationStep.swift new file mode 100644 index 00000000..7d84a598 --- /dev/null +++ b/Sources/Migration/StagedMigrationStep.swift @@ -0,0 +1,24 @@ +// CoreDataPlus + +import CoreData + +// Representation of a Core Data staged migration step. +@available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) +public struct StagedMigrationStep { + public let sourceVersion: Version + public let sourceModelReference: NSManagedObjectModelReference + public let destinationVersion: Version + public let destinationModelReference: NSManagedObjectModelReference + public let stage: NSMigrationStage + + init?(sourceVersion: Version, destinationVersion: Version) { + guard let stage = sourceVersion.migrationStageToNextModelVersion() else { + return nil + } + self.sourceVersion = sourceVersion + self.sourceModelReference = sourceVersion.managedObjectModelReference() + self.destinationVersion = destinationVersion + self.destinationModelReference = destinationVersion.managedObjectModelReference() + self.stage = stage + } +} diff --git a/Sources/NSAttributeDescription+Utils.swift b/Sources/NSAttributeDescription+Utils.swift index e4b38ee8..e8ce4d8a 100644 --- a/Sources/NSAttributeDescription+Utils.swift +++ b/Sources/NSAttributeDescription+Utils.swift @@ -15,7 +15,7 @@ import CoreData extension NSEntityDescription { - /// Creates a new NSEntityDescription instance. + /// Creates a new `NSEntityDescription` instance. /// - Parameters: /// - aClass: The class that represents the entity. /// - name: The entity name (defaults to the class name). @@ -40,204 +40,149 @@ extension NSAttributeDescription { /// - defaultValue: The default value of the attribute. /// - Returns: Returns a *Int16* attribute description. public static func int16(name: String, defaultValue: Int16? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .integer16 - } else { - attributes = NSAttributeDescription(name: name, type: .integer16AttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .integer16 attributes.defaultValue = defaultValue.map { NSNumber(value: $0) } return attributes } public static func int32(name: String, defaultValue: Int32? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .integer32 - } else { - attributes = NSAttributeDescription(name: name, type: .integer32AttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .integer32 attributes.defaultValue = defaultValue.map { NSNumber(value: $0) } return attributes } public static func int64(name: String, defaultValue: Int64? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .integer64 - } else { - attributes = NSAttributeDescription(name: name, type: .integer64AttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .integer64 attributes.defaultValue = defaultValue.map { NSNumber(value: $0) } return attributes } public static func decimal(name: String, defaultValue: Decimal? = nil) -> NSAttributeDescription { // https://stackoverflow.com/questions/2376853/core-data-decimal-type-for-currency - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .decimal - } else { - attributes = NSAttributeDescription(name: name, type: .decimalAttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .decimal attributes.defaultValue = defaultValue.map { NSDecimalNumber(decimal: $0) } return attributes } public static func float(name: String, defaultValue: Float? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .float - } else { - attributes = NSAttributeDescription(name: name, type: .floatAttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .float attributes.defaultValue = defaultValue.map { NSNumber(value: $0) } return attributes } public static func double(name: String, defaultValue: Double? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .double - } else { - attributes = NSAttributeDescription(name: name, type: .doubleAttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .double attributes.defaultValue = defaultValue.map { NSNumber(value: $0) } return attributes } public static func string(name: String, defaultValue: String? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .string - } else { - attributes = NSAttributeDescription(name: name, type: .stringAttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .string attributes.defaultValue = defaultValue return attributes } public static func bool(name: String, defaultValue: Bool? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .boolean - } else { - attributes = NSAttributeDescription(name: name, type: .booleanAttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .boolean attributes.defaultValue = defaultValue.map { NSNumber(value: $0) } return attributes } public static func date(name: String, defaultValue: Date? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .date - } else { - attributes = NSAttributeDescription(name: name, type: .dateAttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .date attributes.defaultValue = defaultValue return attributes } public static func uuid(name: String, defaultValue: UUID? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .uuid - } else { - attributes = NSAttributeDescription(name: name, type: .UUIDAttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .uuid attributes.defaultValue = defaultValue return attributes } public static func uri(name: String, defaultValue: URL? = nil) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .uri - } else { - attributes = NSAttributeDescription(name: name, type: .URIAttributeType) - } + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .uri attributes.defaultValue = defaultValue return attributes } - public static func binaryData(name: String, defaultValue: Data? = nil, allowsExternalBinaryDataStorage: Bool = false) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .binaryData - } else { - attributes = NSAttributeDescription(name: name, type: .binaryDataAttributeType) - } + public static func binaryData(name: String, defaultValue: Data? = nil, allowsExternalBinaryDataStorage: Bool = false) + -> NSAttributeDescription + { + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .binaryData attributes.defaultValue = defaultValue attributes.allowsExternalBinaryDataStorage = allowsExternalBinaryDataStorage return attributes } // transformerName needs to be unique - private static func transformable(for aClass: T.Type, - name: String, - defaultValue: T? = nil, - valueTransformerName: String) -> NSAttributeDescription { - let attributes: NSAttributeDescription - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - attributes = NSAttributeDescription() - attributes.name = name - attributes.type = .transformable - } else { - attributes = NSAttributeDescription(name: name, type: .transformableAttributeType) - } + private static func transformable( + for aClass: T.Type, + name: String, + defaultValue: T? = nil, + valueTransformerName: String + ) -> NSAttributeDescription { + let attributes = NSAttributeDescription() + attributes.name = name + attributes.type = .transformable attributes.defaultValue = defaultValue attributes.attributeValueClassName = "\(T.self.classForCoder())" attributes.valueTransformerName = valueTransformerName return attributes } - public static func customTransformable(for aClass: T.Type, - name: String, - defaultValue: T? = nil, - transform: @escaping CustomTransformer.Transform, - reverse: @escaping CustomTransformer.ReverseTransform) -> NSAttributeDescription { + public static func customTransformable( + for aClass: T.Type, + name: String, + defaultValue: T? = nil, + transform: @escaping CustomTransformer.Transform, + reverse: @escaping CustomTransformer.ReverseTransform + ) -> NSAttributeDescription { CustomTransformer.register(transform: transform, reverseTransform: reverse) - let attributes = NSAttributeDescription.transformable(for: T.self, - name: name, - defaultValue: defaultValue, - valueTransformerName: CustomTransformer.transformerName.rawValue) + let attributes = NSAttributeDescription.transformable( + for: T.self, + name: name, + defaultValue: defaultValue, + valueTransformerName: CustomTransformer.transformerName.rawValue) return attributes } - public static func transformable(for aClass: T.Type, - name: String, - defaultValue: T? = nil) -> NSAttributeDescription { + public static func transformable( + for aClass: T.Type, + name: String, + defaultValue: T? = nil + ) -> NSAttributeDescription { Transformer.register() - let attributes = NSAttributeDescription.transformable(for: T.self, - name: name, - defaultValue: defaultValue, - valueTransformerName: Transformer.transformerName.rawValue) + let attributes = NSAttributeDescription.transformable( + for: T.self, + name: name, + defaultValue: defaultValue, + valueTransformerName: Transformer.transformerName.rawValue) return attributes } diff --git a/Sources/NSBatchInsertResult+Utils.swift b/Sources/NSBatchInsertResult+Utils.swift index 9f6bdf36..2547c4a5 100644 --- a/Sources/NSBatchInsertResult+Utils.swift +++ b/Sources/NSBatchInsertResult+Utils.swift @@ -2,7 +2,6 @@ import CoreData -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) extension NSBatchInsertResult { /// Returns a dictionary containig all the inserted `NSManagedObjectID` instances ready to be passed to `NSManagedObjectContext.mergeChanges(fromRemoteContextSave:into:)`. public var changes: [String: [NSManagedObjectID]]? { diff --git a/Sources/NSCompositeAttributeDescription+Utils.swift b/Sources/NSCompositeAttributeDescription+Utils.swift new file mode 100644 index 00000000..e1e6ac12 --- /dev/null +++ b/Sources/NSCompositeAttributeDescription+Utils.swift @@ -0,0 +1,17 @@ +// CoreDataPlus + +import CoreData + +@available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, *) +extension NSCompositeAttributeDescription { + /// Creates a new `NSCompositeAttributeDescription` instance. + /// - Parameters: + /// - name: The name of the composite attribute. + /// - elements: The composed attribute descriptions. + /// - Warning: Composite attributes are available only to persistent stores that you configure with the **sqlite** store type. + public convenience init(name: String, elements: [NSAttributeDescription]) { + self.init() + self.name = name + self.elements = elements + } +} diff --git a/Sources/NSDerivedAttributeDescription+Utils.swift b/Sources/NSDerivedAttributeDescription+Utils.swift index a2dea247..e67847f1 100644 --- a/Sources/NSDerivedAttributeDescription+Utils.swift +++ b/Sources/NSDerivedAttributeDescription+Utils.swift @@ -3,17 +3,18 @@ import CoreData -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) extension NSDerivedAttributeDescription { /// Creates a new `NSDerivedAttributeDescription` instance. /// - Parameters: /// - name: The name of the derived attribute. /// - type: The type of the derived attribute. /// - derivationExpression: An expression for generating derived data. - public convenience init(name: String, type: NSAttributeType, derivationExpression: NSExpression) { + /// - Warning: Data recomputes derived attributes when you save a context. A managed object’s property does not reflect unsaved changes until you save the context and refresh the object. + public convenience init(name: String, type: NSAttributeDescription.AttributeType, derivationExpression: NSExpression) + { self.init() self.name = name - self.attributeType = type + self.type = type self.derivationExpression = derivationExpression } } diff --git a/Sources/NSFetchRequestResult+CoreData.swift b/Sources/NSFetchRequestResult+CoreData.swift index 32e5cf26..34975af1 100644 --- a/Sources/NSFetchRequestResult+CoreData.swift +++ b/Sources/NSFetchRequestResult+CoreData.swift @@ -2,41 +2,68 @@ import CoreData -extension NSFetchRequestResult where Self: NSManagedObject { +extension NSManagedObject { /// The entity name. - /// - Warning: The NSManagedObjectModel must be loaded or the execution will be stopped. - public static var entityName: String { + /// + /// Usually there is no need to override this property; once overridden be sure to ovverride it in all the subclasses (if any). + /// - Warning: If the `NSManagedObjectModel` is not loaded, the value will fallback to a string representation of `Self`. + @objc open class var entityName: String { if let name = entity().name { return name } + + // Fallback to string representation of Self + // https://stackoverflow.com/questions/37909392/exc-bad-access-when-calling-new-entity-method-in-ios-10-macos-sierra-core-da // https://stackoverflow.com/questions/43231873/nspersistentcontainer-unittests-with-ios10/43286175 // https://www.jessesquires.com/blog/swift-coredata-and-testing/ // https://github.com/jessesquires/rdar-19368054 - + // Returning a string representation of Self metatype doesn't work if you have a NSManagedObject subclass - // with a name different from the NSEntityDescription name. - // return String(describing: Self.self) - - // see testEntityName() - fatalError("Have you loaded your NSManagedObjectModel yet?") + // with a name different from the NSEntityDescription name (In that case is ovverride this property). + + return String(describing: Self.self) } +} - // MARK: - Fetch +//extension NSFetchRequestResult where Self: NSManagedObject { +// /// The entity name. +// /// - Warning: The `NSManagedObjectModel` must be loaded or the execution will be stopped. +// public static var _entityName: String { +// if let name = entity().name { +// return name +// } +// +// // https://stackoverflow.com/questions/37909392/exc-bad-access-when-calling-new-entity-method-in-ios-10-macos-sierra-core-da +// // https://stackoverflow.com/questions/43231873/nspersistentcontainer-unittests-with-ios10/43286175 +// // https://www.jessesquires.com/blog/swift-coredata-and-testing/ +// // https://github.com/jessesquires/rdar-19368054 +// +// // Returning a string representation of Self metatype doesn't work if you have a NSManagedObject subclass +// // with a name different from the NSEntityDescription name. +// // return String(describing: Self.self) +// +// fatalError("Have you loaded your NSManagedObjectModel yet?") +// } +//} +extension NSFetchRequestResult where Self: NSManagedObject { + + // MARK: - Fetch + /// Returns a new fetch request initialized with the entity represented by this subclass (`self`). /// - Warning: This fetch request is created with a string name (`entityName`), and cannot respond to -entity until used by an NSManagedObjectContex. public static func newFetchRequest() -> NSFetchRequest { NSFetchRequest(entityName: entityName) } - + /// - Returns: an object for a specified `id` even if the object needs to be fetched. /// If the object is not registered in the context, it may be fetched or returned as a fault. /// If use existingObject(with:) if you don't want a faulted object. public static func object(with id: NSManagedObjectID, in context: NSManagedObjectContext) -> Self? { context.object(with: id) as? Self } - + /// - Returns: the object for the specified ID or nil if the object does not exist. /// If there is a managed object with the given ID already registered in the context, that object is returned directly; otherwise the corresponding object is faulted into the context. /// This method might perform I/O if the data is uncached. @@ -44,7 +71,7 @@ extension NSFetchRequestResult where Self: NSManagedObject { public static func existingObject(with id: NSManagedObjectID, in context: NSManagedObjectContext) throws -> Self? { try context.existingObject(with: id) as? Self } - + /// Performs a configurable fetch request in a context. /// - Note: It always accesses the underlying persistent stores to retrieve the latest results. /// - Attention: Core Data makes heavy use of Futures, especially for relationship values. @@ -56,17 +83,20 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - configuration: Configuration closure applied **only** before fetching. /// - Throws: It throws an error in cases of failure. /// - Returns: An array of objects meeting the criteria specified by request fetched from *the receiver* and from *the persistent stores* associated with the receiver’s persistent store coordinator. - public static func fetchObjects(in context: NSManagedObjectContext, with configuration: (NSFetchRequest) -> Void = { _ in }) throws -> [Self] { + public static func fetchObjects(in context: NSManagedObjectContext, + with configuration: (NSFetchRequest) -> Void = { _ in }) throws -> [Self] { // Check the Discussion paragraph for the fetch(_:) documentation: // https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506672-fetch // When you execute an instance of NSFetchRequest, it always accesses the underlying persistent stores to retrieve the latest results. // https://developer.apple.com/documentation/coredata/nsfetchrequest let request = NSFetchRequest(entityName: entityName) configuration(request) - precondition(request.resultType == .managedObjectResultType, "This method requires a NSFetchRequest with resultType of .managedObjectResultType.") + precondition( + request.resultType == .managedObjectResultType, + "This method requires a NSFetchRequest with resultType of .managedObjectResultType.") return try context.fetch(request) } - + /// Performs a configurable fetch request in a context. /// - Note: When fetching data from Core Data, you don’t always know how many values you’ll be getting back. /// Core Data solves this problem by using a subclass of `NSArray` that will dynamically pull in data from the underlying store on demand. @@ -78,18 +108,20 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - configuration: Configuration closure applied **only** before fetching. /// - Throws: It throws an error in cases of failure. /// - Returns: Returns an array of objects that meet the criteria specified by a given fetch request. - public static func fetchNSArray(in context: NSManagedObjectContext, with configuration: (NSFetchRequest) -> Void = { _ in }) throws -> NSArray { + public static func fetchNSArray( + in context: NSManagedObjectContext, with configuration: (NSFetchRequest) -> Void = { _ in } + ) throws -> NSArray { // Check the Discussion paragraph for the fetch(_:) documentation: // https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506672-fetch - + // When you execute an instance of NSFetchRequest, it always accesses the underlying persistent stores to retrieve the latest results. // https://developer.apple.com/documentation/coredata/nsfetchrequest let request = NSFetchRequest(entityName: entityName) configuration(request) - + return try context.fetchNSArray(request) } - + /// Fetches all the `NSManagedObjectID` for a given predicate. /// - Note: it always accesses the underlying persistent stores to retrieve the latest results. /// @@ -120,12 +152,12 @@ extension NSFetchRequestResult where Self: NSManagedObject { request.includesSubentities = includingSubentities request.predicate = predicate request.affectedStores = affectedStores - + return try context.fetch(request) } - + // MARK: - First - + /// Fetches an object matching the given predicate. /// - Note: it always accesses the underlying persistent stores to retrieve the latest results. /// @@ -136,11 +168,13 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - affectedStores: An array of persistent stores specified for the fetch request. /// - Throws: It throws an error in cases of failure. /// - Returns: A **materialized** object matching the predicate. - public static func fetchOneObject(in context: NSManagedObjectContext, - where predicate: NSPredicate, - includesPendingChanges: Bool = true, - affectedStores: [NSPersistentStore]? = nil) throws -> Self? { - return try fetchObjects(in: context) { request in + public static func fetchOneObject( + in context: NSManagedObjectContext, + where predicate: NSPredicate, + includesPendingChanges: Bool = true, + affectedStores: [NSPersistentStore]? = nil + ) throws -> Self? { + try fetchObjects(in: context) { request in request.predicate = predicate request.returnsObjectsAsFaults = false request.includesPendingChanges = includesPendingChanges @@ -148,9 +182,9 @@ extension NSFetchRequestResult where Self: NSManagedObject { request.fetchLimit = 1 }.first } - + // MARK: - Unique - + /// Executes a fetch request where **at most** a single object is expected as result; if more than one object are fetched, a fatal error will occour. /// - Note: To guarantee uniqueness the fetch accesses the underlying persistent stores to retrieve the latest results and, also, matches against currently /// unsaved changes in the managed object context. @@ -160,26 +194,28 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - predicate: Matching predicate. /// - affectedStores: An array of persistent stores specified for the fetch request. /// - Returns: An unique object matching the given configuration (if any). - public static func fetchUniqueObject(in context: NSManagedObjectContext, where predicate: NSPredicate, affectedStores: [NSPersistentStore]? = nil) throws -> Self? { + public static func fetchUniqueObject(in context: NSManagedObjectContext, + where predicate: NSPredicate, + affectedStores: [NSPersistentStore]? = nil) throws -> Self? { let result = try fetchObjects(in: context) { request in request.predicate = predicate - request.includesPendingChanges = true // default, uniqueness should be guaranteed + request.includesPendingChanges = true // default, uniqueness should be guaranteed request.affectedStores = affectedStores request.fetchLimit = 2 } - + switch result.count { - case 0: - return nil - case 1: - return result[0] - default: - fatalError("Returned multiple objects, expected max 1.") + case 0: + return nil + case 1: + return result[0] + default: + fatalError("Returned multiple objects, expected only one.") } } - + // MARK: - Delete - + /// Specifies the objects (matching a given `predicate`) that should be removed from its persistent store when changes are committed. /// If objects have not yet been saved to a persistent store, they are simply removed from the context. /// If the dataset to delete is very large, use the `limit` value to decide the number of objects to be deleted otherwise the operation could last an unbounded amount time. @@ -191,7 +227,8 @@ extension NSFetchRequestResult where Self: NSManagedObject { includingSubentities: Bool = true, where predicate: NSPredicate = NSPredicate(value: true), limit: Int? = nil, - affectedStores: [NSPersistentStore]? = nil) throws { + affectedStores: [NSPersistentStore]? = nil + ) throws { try autoreleasepool { try fetchObjects(in: context) { request in request.includesPropertyValues = false @@ -205,7 +242,7 @@ extension NSFetchRequestResult where Self: NSManagedObject { }.lazy.forEach(context.delete(_:)) } } - + /// Removes all entities from within the specified `NSManagedObjectContext` excluding a given list of entities. /// /// - Parameters: @@ -214,28 +251,31 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - affectedStores: An array of persistent stores specified for the fetch request. /// - Throws: It throws an error in cases of failure. /// - Note: `NSBatchDeleteRequest` would be more efficient but requires a context with an `NSPersistentStoreCoordinator` directly connected (no child context). - public static func delete(in context: NSManagedObjectContext, except objects: [Self], affectedStores: [NSPersistentStore]? = nil) throws { + public static func delete(in context: NSManagedObjectContext, + except objects: [Self], + affectedStores: [NSPersistentStore]? = nil) throws { let predicate = NSPredicate(format: "NOT (self IN %@)", objects) try delete(in: context, includingSubentities: true, where: predicate, affectedStores: affectedStores) } - + // MARK: - Count - + /// Counts the results of a configurable fetch request in a context. /// - Throws: It throws an error in cases of failure. - public static func count(in context: NSManagedObjectContext, for configuration: (NSFetchRequest) -> Void = { _ in }) throws -> Int { + public static func count(in context: NSManagedObjectContext, + for configuration: (NSFetchRequest) -> Void = { _ in }) throws -> Int { let request = newFetchRequest() configuration(request) - + let result = try context.count(for: request) // result is equal to NSNotFound if an error occurs (an exception is expected to be thrown) guard result != NSNotFound else { return 0 } - + return result } - + // MARK: - Materialized Object - + /// Iterates over the context’s registeredObjects set (which contains all managed objects the context currently knows about) until it finds one that is not a fault matching for a given predicate. /// Faulted objects are not considered to prevent Core Data to make a round trip to the persistent store. /// @@ -246,12 +286,12 @@ extension NSFetchRequestResult where Self: NSManagedObject { public static func materializedObject(in context: NSManagedObjectContext, where predicate: NSPredicate) -> Self? { for object in context.registeredObjects where !object.isFault { guard let result = object as? Self, predicate.evaluate(with: result) else { continue } - + return result } return nil } - + /// Iterates over the context’s registeredObjects set (which contains all managed objects the context currently knows about) until it finds /// all the objects that aren't a fault matching for a given predicate. /// Faulted objects are not considered to prevent Core Data to make a round trip to the persistent store. @@ -279,13 +319,14 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Throws: It throws an error in cases of failure. /// - Returns: The result returned when executing a batch update request. /// - Note: A batch delete can **only** be done on a SQLite store. - public static func batchUpdate(using context: NSManagedObjectContext, configuration: (NSBatchUpdateRequest) -> Void) throws -> NSBatchUpdateResult { + public static func batchUpdate(using context: NSManagedObjectContext, + configuration: (NSBatchUpdateRequest) -> Void) throws -> NSBatchUpdateResult { let batchRequest = NSBatchUpdateRequest(entityName: entityName) configuration(batchRequest) // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchUpdateResult } - + /// Executes a batch delete on the context's persistent store coordinator. /// - Parameters: /// - context: The context whose the persistent store coordinator will be used to execute the batch delete. @@ -301,7 +342,8 @@ extension NSFetchRequestResult where Self: NSManagedObject { predicate: NSPredicate? = nil, includesSubentities: Bool = true, resultType: NSBatchDeleteRequestResultType = .resultTypeStatusOnly, - affectedStores: [NSPersistentStore]? = nil) throws -> NSBatchDeleteResult { + affectedStores: [NSPersistentStore]? = nil + ) throws -> NSBatchDeleteResult { // Only a subset of NSFetchRequest properties are used by a NSBatchDeleteRequest // // affectedStores should be set (if needed) in the NSBatchDeleteRequest object: @@ -309,16 +351,16 @@ extension NSFetchRequestResult where Self: NSManagedObject { let request = NSFetchRequest(entityName: entityName) request.predicate = predicate request.includesSubentities = includesSubentities - + // swiftlint:disable:next force_cast let batchRequest = NSBatchDeleteRequest(fetchRequest: request as! NSFetchRequest) batchRequest.resultType = resultType batchRequest.affectedStores = affectedStores - + // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchDeleteResult } - + /// Executes a batch insert on the context's persistent store coordinator. /// - Parameters: /// - context: The context whose the persistent store coordinator will be used to execute the batch insert. @@ -327,19 +369,19 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Throws: It throws an error in cases of failure /// - Returns: The result that Core Data returns when executing a batch-insertion request. /// - Note: A batch insert can **only** be done on a SQLite store. - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) public static func batchInsert(using context: NSManagedObjectContext, resultType: NSBatchInsertRequestResultType = .statusOnly, objects: [[String: Any]], - affectedStores: [NSPersistentStore]? = nil) throws -> NSBatchInsertResult { + affectedStores: [NSPersistentStore]? = nil + ) throws -> NSBatchInsertResult { let batchRequest = NSBatchInsertRequest(entityName: entityName, objects: objects) batchRequest.resultType = resultType batchRequest.affectedStores = affectedStores - + // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchInsertResult } - + /// Executes a batch insert on the context's persistent store coordinator. /// Doing a batch insert with this method is more memory efficient than the standard batch insert where all the items are passed alltogether. /// - Parameters: @@ -349,17 +391,18 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Throws: It throws an error in cases of failure. /// - Returns: The result that Core Data returns when executing a batch-insertion request. /// - Note: A batch insert can **only** be done on a SQLite store. - @available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) - public static func batchInsert(using context: NSManagedObjectContext, - resultType: NSBatchInsertRequestResultType = .statusOnly, - dictionaryHandler handler: @escaping (NSMutableDictionary) -> Bool) throws -> NSBatchInsertResult { + public static func batchInsert( + using context: NSManagedObjectContext, + resultType: NSBatchInsertRequestResultType = .statusOnly, + dictionaryHandler handler: @escaping (NSMutableDictionary) -> Bool + ) throws -> NSBatchInsertResult { let batchRequest = NSBatchInsertRequest(entityName: entityName, dictionaryHandler: handler) batchRequest.resultType = resultType - + // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchInsertResult } - + /// Executes a batch insert on the context's persistent store coordinator. /// Doing a batch insert with this method is more memory efficient than the standard batch insert where all the items are passed alltogether. /// - Parameters: @@ -369,16 +412,18 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Throws: It throws an error in cases of failure. /// - Returns: The result that Core Data returns when executing a batch-insertion request. /// - Note: A batch insert can **only** be done on a SQLite store. - @available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) public static func batchInsert(using context: NSManagedObjectContext, resultType: NSBatchInsertRequestResultType = .statusOnly, - managedObjectHandler handler: @escaping (Self) -> Bool) throws -> NSBatchInsertResult { - let batchRequest = NSBatchInsertRequest(entityName: entityName, managedObjectHandler: { object -> Bool in - // swiftlint:disable:next force_cast - return handler(object as! Self) - }) + managedObjectHandler handler: @escaping (Self) -> Bool + ) throws -> NSBatchInsertResult { + let batchRequest = NSBatchInsertRequest( + entityName: entityName, + managedObjectHandler: { object -> Bool in + // swiftlint:disable:next force_cast + handler(object as! Self) + }) batchRequest.resultType = resultType - + // swiftlint:disable:next force_cast return try context.execute(batchRequest) as! NSBatchInsertResult } @@ -411,10 +456,11 @@ extension NSFetchRequestResult where Self: NSManagedObject { public static func fetchObjects(in context: NSManagedObjectContext, estimatedResultCount: Int = 0, with configuration: (NSFetchRequest) -> Void = { _ in }, - completion: @escaping (Result<[Self], Error>) -> Void) throws -> NSAsynchronousFetchResult { + completion: @escaping (Result<[Self], Error>) -> Void + ) throws -> NSAsynchronousFetchResult { let request = Self.newFetchRequest() configuration(request) - + let asynchronousRequest = NSAsynchronousFetchRequest(fetchRequest: request) { result in if let error = result.operationError { completion(.failure(error)) @@ -425,13 +471,12 @@ extension NSFetchRequestResult where Self: NSManagedObject { } } asynchronousRequest.estimatedResultCount = estimatedResultCount - + // swiftlint:disable:next force_cast return try context.execute(asynchronousRequest) as! NSAsynchronousFetchResult } } -#if compiler(>=5.5.2) && canImport(_Concurrency) extension NSFetchRequestResult where Self: NSManagedObject { /// Performs a configurable asynchronous fetch request in a context. /// @@ -441,20 +486,19 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Returns: The results that were received from the fetch request. /// - Throws: It throws an error in cases of failure. /// - Warning: If the ConcurrencyDebug is enabled, the fetch request will cause a thread violation error, without it data races will be always detected by Xcode. - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) public static func fetchObjects(in context: NSManagedObjectContext, estimatedResultCount: Int = 0, with configuration: (NSFetchRequest) -> Void = { _ in }) async throws -> [Self] { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { continuation in do { // TODO: Swift concurrency and NSProgress: https://github.com/apple/swift-evolution/blob/main/proposals/0297-concurrency-objc.md#nsprogress // TODO: The associated test is disabled because of data races. try fetchObjects(in: context, estimatedResultCount: estimatedResultCount, with: configuration) { result in switch result { - case .success(let fetchResult): - continuation.resume(returning: fetchResult) - case .failure(let error): - continuation.resume(throwing: error) + case .success(let fetchResult): + continuation.resume(returning: fetchResult) + case .failure(let error): + continuation.resume(throwing: error) } } } catch let error { @@ -463,4 +507,3 @@ extension NSFetchRequestResult where Self: NSManagedObject { } } } -#endif diff --git a/Sources/NSFetchedPropertyDescription+Utils.swift b/Sources/NSFetchedPropertyDescription+Utils.swift index 5a021db0..4a802bc5 100644 --- a/Sources/NSFetchedPropertyDescription+Utils.swift +++ b/Sources/NSFetchedPropertyDescription+Utils.swift @@ -28,12 +28,16 @@ extension NSFetchedPropertyDescription { /// this causes the fetch request associated with this property to be executed again when the object fault is next fired. /// /// Unlike other relationships, which are all sets, fetched properties are represented by an ordered NSArray object just as if you executed the fetch request yourself. - public convenience init(name: String, destinationEntity: NSEntityDescription, configuration: (NSFetchRequest) -> Void ) { + public convenience init( + name: String, destinationEntity: NSEntityDescription, configuration: (NSFetchRequest) -> Void + ) { self.init() let request = NSFetchRequest(entity: destinationEntity) request.resultType = .managedObjectResultType configuration(request) - assert(request.resultType == .managedObjectResultType, "NSFetchedPropertyDescription supports only NSFetchRequest with resultType set to managedObjectResultType.") + assert( + request.resultType == .managedObjectResultType, + "NSFetchedPropertyDescription supports only NSFetchRequest with resultType set to managedObjectResultType.") self.name = name self.fetchRequest = request } diff --git a/Sources/NSManagedObjectContext+History.swift b/Sources/NSManagedObjectContext+History.swift index 68e5961c..851207da 100644 --- a/Sources/NSManagedObjectContext+History.swift +++ b/Sources/NSManagedObjectContext+History.swift @@ -22,11 +22,16 @@ extension NSManagedObjectContext { /// - Parameters: /// - historyFetchRequest: A request to fetch persistent history transactions. /// - withAssociatedChanges: if `true` (default) each transaction will contain all its changes. - public func historyTransactions(using historyFetchRequest: NSPersistentHistoryChangeRequest, andAssociatedChanges enableChanges: Bool = true) throws -> [NSPersistentHistoryTransaction] { + public func historyTransactions( + using historyFetchRequest: NSPersistentHistoryChangeRequest, andAssociatedChanges enableChanges: Bool = true + ) throws + -> [NSPersistentHistoryTransaction] + { historyFetchRequest.resultType = enableChanges ? .transactionsAndChanges : .transactionsOnly // swiftlint:disable force_cast let history = try execute(historyFetchRequest) as! NSPersistentHistoryResult - let transactions = history.result as! [NSPersistentHistoryTransaction] // ordered from the oldest to the most recent + // ordered from the oldest to the most recent + let transactions = history.result as! [NSPersistentHistoryTransaction] // swiftlint:enable force_cast return transactions } @@ -36,11 +41,13 @@ extension NSManagedObjectContext { /// Returns all the history changes for given a `NSPersistentHistoryChangeRequest` request. /// /// - Parameter historyFetchRequest: A request to fetch persistent history changes. - public func historyChanges(using historyFetchRequest: NSPersistentHistoryChangeRequest) throws -> [NSPersistentHistoryChange] { + public func historyChanges(using historyFetchRequest: NSPersistentHistoryChangeRequest) throws + -> [NSPersistentHistoryChange] + { historyFetchRequest.resultType = .changesOnly // swiftlint:disable force_cast let history = try execute(historyFetchRequest) as! NSPersistentHistoryResult - let changes = history.result as! [NSPersistentHistoryChange] // ordered from the oldest to the most recent + let changes = history.result as! [NSPersistentHistoryChange] // ordered from the oldest to the most recent // swiftlint:enable force_cast return changes } @@ -50,7 +57,9 @@ extension NSManagedObjectContext { /// - Important: The merging operation must be done inside a the context queue. /// /// Returns the last merged transaction's token and timestamp. - public func mergeTransactions(_ transactions: [NSPersistentHistoryTransaction]) throws -> (NSPersistentHistoryToken, Date)? { + public func mergeTransactions(_ transactions: [NSPersistentHistoryTransaction]) throws -> ( + NSPersistentHistoryToken, Date + )? { // Do your merging inside a context.performAndWait { … } (as shown in WWDC 2017). var result: (NSPersistentHistoryToken, Date)? for transaction in transactions { @@ -67,7 +76,7 @@ extension NSManagedObjectContext { /// Deletes all history. @discardableResult public func deleteHistory() throws -> Bool { - return try deleteHistory(before: .distantFuture) + try deleteHistory(before: .distantFuture) } /// Deletes all history before a given `date`. diff --git a/Sources/NSManagedObjectContext+Utils.swift b/Sources/NSManagedObjectContext+Utils.swift index 051cf716..043a96dc 100644 --- a/Sources/NSManagedObjectContext+Utils.swift +++ b/Sources/NSManagedObjectContext+Utils.swift @@ -16,6 +16,7 @@ extension NSManagedObjectContext { // https://developer.apple.com/forums/thread/651325. // swiftlint:disable force_cast let protocolRequest = request as! NSFetchRequest + // swiftlint:enable force_cast let results = try fetch(protocolRequest) as NSArray return results } @@ -40,7 +41,9 @@ extension NSManagedObjectContext { /// Returns a `new` child `NSManagedObjectContext`. /// - Parameters: /// - concurrencyType: Specifies the concurrency pattern used by this child context (defaults to the parent type). - public final func newChildContext(concurrencyType: NSManagedObjectContextConcurrencyType? = nil) -> NSManagedObjectContext { + public final func newChildContext(concurrencyType: NSManagedObjectContextConcurrencyType? = nil) + -> NSManagedObjectContext + { let type = concurrencyType ?? self.concurrencyType let context = NSManagedObjectContext(concurrencyType: type) context.parent = self @@ -61,7 +64,8 @@ extension NSManagedObjectContext { /// - Note: The `hasChanges` method would return `true` for transient changes as well which can lead to false positives. public var hasPersistentChanges: Bool { guard hasChanges else { return false } - return !insertedObjects.isEmpty || !deletedObjects.isEmpty || updatedObjects.first(where: { $0.hasPersistentChangedValues }) != nil + return !insertedObjects.isEmpty || !deletedObjects.isEmpty + || updatedObjects.first(where: { $0.hasPersistentChangedValues }) != nil } /// Returns the number of changes that will change the persistent store (transient changes are ignored). @@ -83,7 +87,7 @@ extension NSManagedObjectContext { do { try saveIfNeeded() } catch { - rollback() // rolls back the pending changes (and clears the undo stack if set up) + rollback() // rolls back the pending changes (and clears the undo stack if set up) throw error } } @@ -108,9 +112,11 @@ extension NSManagedObjectContext { /// /// Source: https://oleb.net/blog/2018/02/performandwait/ /// Source: https://github.com/apple/swift/blob/bb157a070ec6534e4b534456d208b03adc07704b/stdlib/public/SDK/Dispatch/Queue.swift#L228-L249 - private func _performAndWaitHelper(function: (() -> Void) -> Void, - execute work: (NSManagedObjectContext) throws -> T, - rescue: (Error) throws -> (T)) rethrows -> T { + private func _performAndWaitHelper( + function: (() -> Void) -> Void, + execute work: (NSManagedObjectContext) throws -> T, + rescue: (Error) throws -> (T) + ) rethrows -> T { var result: T? var error: Error? // swiftlint:disable:next identifier_name diff --git a/Sources/NSMappingModel+Utils.swift b/Sources/NSMappingModel+Utils.swift index d82873a3..8e080c52 100644 --- a/Sources/NSMappingModel+Utils.swift +++ b/Sources/NSMappingModel+Utils.swift @@ -2,16 +2,16 @@ import CoreData -public extension NSMappingModel { +extension NSMappingModel { /// Wheter or not the mapping model is inferred. - var isInferred: Bool { + public var isInferred: Bool { entityMappings.allSatisfy { $0.isInferred } } } -public extension NSEntityMapping { +extension NSEntityMapping { /// Wheter or not the mapping entity is inferred. - var isInferred: Bool { + public var isInferred: Bool { // An inferred entity mapping name starts with IEM (i.e. IEM_Add, IEM_Transform, IEM_Copy). // AFAIK it's the only way to detect at runtim an inferred mapping. name.starts(with: "IEM") diff --git a/Sources/NSPersistentStoreCoordinator+Utils.swift b/Sources/NSPersistentStoreCoordinator+Utils.swift index e7414aa7..ef4eb685 100644 --- a/Sources/NSPersistentStoreCoordinator+Utils.swift +++ b/Sources/NSPersistentStoreCoordinator+Utils.swift @@ -23,11 +23,8 @@ extension NSPersistentStoreCoordinator { public static func destroyStore(at url: URL, options: PersistentStoreOptions? = nil) throws { let persistentStoreCoordinator = self.init(managedObjectModel: NSManagedObjectModel()) /// destroyPersistentStore safely deletes everything in the database and leaves an empty database behind. - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - try persistentStoreCoordinator.destroyPersistentStore(at: url, type: .sqlite, options: options) - } else { - try persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: options) - } + try persistentStoreCoordinator.destroyPersistentStore(at: url, type: .sqlite, options: options) + let fileManager = FileManager.default let storePath = url.path try fileManager.removeItem(atPath: storePath) @@ -39,34 +36,31 @@ extension NSPersistentStoreCoordinator { /// Replaces the destination persistent store with the source store. /// - Attention: The store must be of SQLite type. - public static func replaceStore(at destinationURL: URL, - destinationOptions: PersistentStoreOptions? = nil, - withPersistentStoreFrom sourceURL: URL, - sourceOptions: PersistentStoreOptions? = nil) throws { + public static func replaceStore( + at destinationURL: URL, + destinationOptions: PersistentStoreOptions? = nil, + withPersistentStoreFrom sourceURL: URL, + sourceOptions: PersistentStoreOptions? = nil + ) throws { // https://mjtsai.com/blog/2021/03/31/replacing-vs-migrating-core-data-stores/ // https://atomicbird.com/blog/mostly-undocumented/ // https://github.com/atomicbird/CDMoveDemo // https://menuplan.app/coding/2021/10/27/core-data-store-path-migration.html let persistentStoreCoordinator = self.init(managedObjectModel: NSManagedObjectModel()) // replacing a store has a side effect of removing the current store from the psc - if #available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) { - try persistentStoreCoordinator.replacePersistentStore(at: destinationURL, - destinationOptions: destinationOptions, - withPersistentStoreFrom: sourceURL, - sourceOptions: sourceOptions, - type: .sqlite) - } else { - try persistentStoreCoordinator.replacePersistentStore(at: destinationURL, - destinationOptions: destinationOptions, - withPersistentStoreFrom: sourceURL, - sourceOptions: sourceOptions, - ofType: NSSQLiteStoreType) - } + try persistentStoreCoordinator.replacePersistentStore( + at: destinationURL, + destinationOptions: destinationOptions, + withPersistentStoreFrom: sourceURL, + sourceOptions: sourceOptions, + type: .sqlite) } /// Removes all the stores associated with the coordinator. public func removeAllStores() throws { - try persistentStores.forEach { try remove($0) } + for store in persistentStores { + try remove(store) + } } } diff --git a/Sources/NSPredicate+Utils.swift b/Sources/NSPredicate+Utils.swift index 06d4d6dd..1d15d0bd 100644 --- a/Sources/NSPredicate+Utils.swift +++ b/Sources/NSPredicate+Utils.swift @@ -11,9 +11,13 @@ extension NSPredicate { /// Returns a `new` compound NSPredicate formed by **AND**-ing `self` with `predicate`. /// - Parameter predicate: A `NSPredicate` object. - public final func and(_ predicate: NSPredicate) -> NSPredicate { NSCompoundPredicate(andPredicateWithSubpredicates: [self, predicate]) } + public final func and(_ predicate: NSPredicate) -> NSPredicate { + NSCompoundPredicate(andPredicateWithSubpredicates: [self, predicate]) + } /// Returns: a `new` compound NSPredicate formed by **OR**-ing `self` with `predicate`. /// - Parameter predicate: A `NSPredicate` object. - public final func or(_ predicate: NSPredicate) -> NSPredicate { NSCompoundPredicate(orPredicateWithSubpredicates: [self, predicate]) } + public final func or(_ predicate: NSPredicate) -> NSPredicate { + NSCompoundPredicate(orPredicateWithSubpredicates: [self, predicate]) + } } diff --git a/Sources/Notifications/Notification+Payloads.swift b/Sources/Notifications/Notification+Payloads.swift index 773f202b..9c32555a 100644 --- a/Sources/Notifications/Notification+Payloads.swift +++ b/Sources/Notifications/Notification+Payloads.swift @@ -19,10 +19,7 @@ import Foundation public struct ManagedObjectContextWillSaveObjects { /// Notification name. public static let notificationName: Notification.Name = { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return NSManagedObjectContext.willSaveObjectsNotification - } - return .NSManagedObjectContextWillSave + NSManagedObjectContext.willSaveObjectsNotification }() /// Underlying notification object. @@ -48,10 +45,7 @@ public struct ManagedObjectContextWillSaveObjects { public struct ManagedObjectContextDidSaveObjects { /// Notification name. public static let notificationName: Notification.Name = { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return NSManagedObjectContext.didSaveObjectsNotification - } - return .NSManagedObjectContextDidSave + NSManagedObjectContext.didSaveObjectsNotification }() /// Underlying notification object. @@ -67,43 +61,30 @@ public struct ManagedObjectContextDidSaveObjects { /// Returns a `Set` of objects that were inserted into the context. public var insertedObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objects(forKey: .insertedObjects) - } else { - return notification.objects(forKey: NSInsertedObjectsKey) - } + notification.objects(forKey: .insertedObjects) } /// Returns a `Set` of objects that were updated. public var updatedObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objects(forKey: .updatedObjects) - } else { - return notification.objects(forKey: NSUpdatedObjectsKey) - } + notification.objects(forKey: .updatedObjects) } /// Returns a `Set`of objects that were marked for deletion during the previous event. public var deletedObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objects(forKey: .deletedObjects) - } else { - return notification.objects(forKey: NSDeletedObjectsKey) - } + notification.objects(forKey: .deletedObjects) } /// The `NSPersistentHistoryToken` associated to the save operation. /// - Note: Optional: NSPersistentHistoryTrackingKey must be enabled. public var historyToken: NSPersistentHistoryToken? { // FB6840421 (missing documentation for "newChangeToken" key) - return notification.userInfo?["newChangeToken"] as? NSPersistentHistoryToken + notification.userInfo?["newChangeToken"] as? NSPersistentHistoryToken } /// The new `NSQueryGenerationToken` associated to the save operation. /// - Note: It's only available when you are using a SQLite persistent store. - @available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) public var queryGenerationToken: NSQueryGenerationToken? { - return notification.userInfo?[NSManagedObjectContext.NotificationKey.queryGeneration.rawValue] as? NSQueryGenerationToken + notification.userInfo?[NSManagedObjectContext.NotificationKey.queryGeneration.rawValue] as? NSQueryGenerationToken } public init(notification: Notification) { @@ -130,10 +111,7 @@ extension NSManagedObjectContext { public struct ManagedObjectContextObjectsDidChange { /// Notification name. public static let notificationName: Notification.Name = { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return NSManagedObjectContext.didChangeObjectsNotification - } - return .NSManagedObjectContextObjectsDidChange + NSManagedObjectContext.didChangeObjectsNotification }() /// Underlying notification object. @@ -149,59 +127,35 @@ public struct ManagedObjectContextObjectsDidChange { /// Returns a `Set` of objects that were inserted into the context. public var insertedObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objects(forKey: .insertedObjects) - } else { - return notification.objects(forKey: NSInsertedObjectsKey) - } + notification.objects(forKey: .insertedObjects) } /// Returns a `Set` of objects that were updated. public var updatedObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objects(forKey: .updatedObjects) - } else { - return notification.objects(forKey: NSUpdatedObjectsKey) - } + notification.objects(forKey: .updatedObjects) } /// Returns a `Set`of objects that were marked for deletion during the previous event. public var deletedObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objects(forKey: .deletedObjects) - } else { - return notification.objects(forKey: NSDeletedObjectsKey) - } + notification.objects(forKey: .deletedObjects) } /// A `Set` of objects that were refreshed but were not dirtied in the scope of this context. /// - Note: It can be populated only for `NSManagedObjectContextObjectsDidChange` notifications. public var refreshedObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objects(forKey: .refreshedObjects) - } else { - return notification.objects(forKey: NSRefreshedObjectsKey) - } + notification.objects(forKey: .refreshedObjects) } /// A `Set` of objects that were invalidated. /// - Note: It can be populated only for `NSManagedObjectContextObjectsDidChange` notifications. public var invalidatedObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objects(forKey: .invalidatedObjects) - } else { - return notification.objects(forKey: NSInvalidatedObjectsKey) - } + notification.objects(forKey: .invalidatedObjects) } /// When all the object in the context have been invalidated, returns a `Set` containing all the invalidated objects' NSManagedObjectID. /// - Note: It can be populated only for `NSManagedObjectContextObjectsDidChange` notifications. public var invalidatedAllObjects: Set { - if #available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11, *) { - return notification.objectIDs(forKey: .invalidatedAllObjects) - } else { - return notification.objectIDs(forKey: NSInvalidatedAllObjectsKey) - } + notification.objectIDs(forKey: .invalidatedAllObjects) } public init(notification: Notification) { @@ -213,7 +167,6 @@ public struct ManagedObjectContextObjectsDidChange { // MARK: Did Save IDs /// Typed payload for a Core Data *object IDs did save* notification. -@available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) public struct ManagedObjectContextDidSaveObjectIDs { /// Notification name. public static let notificationName: Notification.Name = NSManagedObjectContext.didSaveObjectIDsNotification @@ -253,7 +206,6 @@ public struct ManagedObjectContextDidSaveObjectIDs { // MARK: Did Merge IDs /// Typed payload for a Core Data *did merge changes IDs* notification. -@available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) public struct ManagedObjectContextDidMergeChangesObjectIDs { /// Notification name. public static let notificationName: Notification.Name = NSManagedObjectContext.didMergeChangesObjectIDsNotification @@ -264,7 +216,8 @@ public struct ManagedObjectContextDidMergeChangesObjectIDs { /// `NSManagedObjectContext` associated with the notification. public var managedObjectContext: NSManagedObjectContext { guard let context = notification.managedObjectContext else { - fatalError("A NSManagedObjectContext.didMergeChangesObjectIDsNotification must have a NSManagedObjectContext object.") + fatalError( + "A NSManagedObjectContext.didMergeChangesObjectIDsNotification must have a NSManagedObjectContext object.") } return context } @@ -313,7 +266,7 @@ public struct PersistentStoreRemoteChange { /// The `NSPersistentHistoryToken` associated to the change operation. /// -Note: It's optional because `NSPersistentHistoryTrackingKey` should be enabled. public var historyToken: NSPersistentHistoryToken? { - return notification.userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken + notification.userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken } /// The changed store URL. @@ -360,15 +313,15 @@ public struct PersistentStoreCoordinatorStoresWillChange { notification.userInfo?[NSRemovedPersistentStoresKey] as? [NSPersistentStore] ?? [] } -// public var ubiquitousTransitionType: NSPersistentStoreUbiquitousTransitionType { -// guard -// let typeValue = notification.userInfo?[NSPersistentStoreUbiquitousTransitionTypeKey] as? UInt, -// let type = NSPersistentStoreUbiquitousTransitionType.init(rawValue: typeValue) -// else { -// fatalError("A Notification.Name.NSPersistentStoreCoordinatorStoresWillChange must have a NSPersistentStoreUbiquitousTransitionType value.") -// } -// return type -// } + // public var ubiquitousTransitionType: NSPersistentStoreUbiquitousTransitionType { + // guard + // let typeValue = notification.userInfo?[NSPersistentStoreUbiquitousTransitionTypeKey] as? UInt, + // let type = NSPersistentStoreUbiquitousTransitionType.init(rawValue: typeValue) + // else { + // fatalError("A Notification.Name.NSPersistentStoreCoordinatorStoresWillChange must have a NSPersistentStoreUbiquitousTransitionType value.") + // } + // return type + // } public init(notification: Notification) { assert(notification.name == Self.notificationName) @@ -425,7 +378,9 @@ public struct PersistentStoreCoordinatorStoresDidChange { /// Store whose UUID changed. public var uuidChangedStore: UUIDChangedStore? { - guard let uuidChangedStore = notification.userInfo?[NSUUIDChangedPersistentStoresKey] as? NSArray else { return nil } + guard let uuidChangedStore = notification.userInfo?[NSUUIDChangedPersistentStoresKey] as? NSArray else { + return nil + } return UUIDChangedStore(changedStore: uuidChangedStore) } @@ -448,7 +403,8 @@ public struct PersistentStoreCoordinatorWillRemoveStore { /// The persistent store coordinator that will be removed. public var store: NSPersistentStore { guard let store = notification.object as? NSPersistentStore else { - fatalError("A Notification.Name.NSPersistentStoreCoordinatorWillRemoveStore must have a NSPersistentStore object.") + fatalError( + "A Notification.Name.NSPersistentStoreCoordinatorWillRemoveStore must have a NSPersistentStore object.") } return store } diff --git a/Sources/Notifications/Notification+Utils.swift b/Sources/Notifications/Notification+Utils.swift index 624d4bc6..f431ea07 100644 --- a/Sources/Notifications/Notification+Utils.swift +++ b/Sources/Notifications/Notification+Utils.swift @@ -8,7 +8,6 @@ extension Notification { userInfo?[key] as? Set ?? Set() } - @available(iOS 14.0, iOSApplicationExtension 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) func objects(forKey key: NSManagedObjectContext.NotificationKey) -> Set { userInfo?[key.rawValue] as? Set ?? Set() } @@ -17,17 +16,16 @@ extension Notification { if let objectIDs = userInfo?[key] as? Set { return objectIDs } else if let objectIDs = userInfo?[key] as? [NSManagedObjectID] { - return Set(objectIDs) // Did Change Notification + return Set(objectIDs) // Did Change Notification } return Set() } - @available(iOS 14.0, iOSApplicationExtension 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) func objectIDs(forKey key: NSManagedObjectContext.NotificationKey) -> Set { if let objectIDs = userInfo?[key.rawValue] as? Set { - return objectIDs // Did Save ObjectIDs Notification + return objectIDs // Did Save ObjectIDs Notification } else if let objectIDs = userInfo?[key.rawValue] as? [NSManagedObjectID] { - return Set(objectIDs) // Did Change Notification + return Set(objectIDs) // Did Change Notification } return Set() } diff --git a/Sources/Transformers/Transformer.swift b/Sources/Transformers/Transformer.swift index 40be09c5..1d2d6b93 100644 --- a/Sources/Transformers/Transformer.swift +++ b/Sources/Transformers/Transformer.swift @@ -41,7 +41,7 @@ public final class Transformer: NSSecureUnarchiveF // transformedValue(_:) and reverseTransformedValue(_:) methods for NSSecureUnarchiveFromDataTransformer subclasses are called // in the opposite way than on ValuteTransformer subclasses guard let data = value as? Data else { return nil } - return super.transformedValue(data) // super returns an instance of T + return super.transformedValue(data) // super returns an instance of T } public override func reverseTransformedValue(_ value: Any?) -> Any? { @@ -50,6 +50,6 @@ public final class Transformer: NSSecureUnarchiveF // context: this method may be called during a save guard let receivedValue = value as? T else { return nil } - return super.reverseTransformedValue(receivedValue) // super returns a Data object + return super.reverseTransformedValue(receivedValue) // super returns a Data object } } diff --git a/Support/CoreDataPlus.h b/Support/CoreDataPlus.h deleted file mode 100644 index 2a343e1f..00000000 --- a/Support/CoreDataPlus.h +++ /dev/null @@ -1,11 +0,0 @@ -// CoreDataPlus - -@import Foundation; - -//! Project version number for CoreDataPlus. -FOUNDATION_EXPORT double CoreDataPlusVersionNumber; - -//! Project version string for CoreDataPlus. -FOUNDATION_EXPORT const unsigned char CoreDataPlusVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Support/Info-Tests.plist b/Support/Info-Tests.plist deleted file mode 100644 index fe05d8c4..00000000 --- a/Support/Info-Tests.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 2.3.0 - CFBundleVersion - 1 - - diff --git a/Support/Info-tvOS.plist b/Support/Info-tvOS.plist deleted file mode 100644 index 070fd26b..00000000 --- a/Support/Info-tvOS.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - UIRequiredDeviceCapabilities - - arm64 - - - diff --git a/Support/Info.plist b/Support/Info.plist index fcd36391..82790df0 100644 --- a/Support/Info.plist +++ b/Support/Info.plist @@ -18,7 +18,5 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - diff --git a/Tests/FetchedResultsChangesTests.swift b/Tests/FetchedResultsChanges_Tests.swift similarity index 77% rename from Tests/FetchedResultsChangesTests.swift rename to Tests/FetchedResultsChanges_Tests.swift index 284b3144..c36df063 100644 --- a/Tests/FetchedResultsChangesTests.swift +++ b/Tests/FetchedResultsChanges_Tests.swift @@ -1,11 +1,12 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class FetchedResultsChangesTests: InMemoryTestCase { - func testNoChanges() throws { +final class FetchedResultsChanges_Tests: InMemoryTestCase { + func test_NoChanges() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -19,7 +20,10 @@ final class FetchedResultsChangesTests: InMemoryTestCase { delegate.didChangeExpectation.isInverted = true // When - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) + let controller = NSFetchedResultsController(fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil) controller.delegate = delegate try controller.performFetch() @@ -33,7 +37,7 @@ final class FetchedResultsChangesTests: InMemoryTestCase { XCTAssertFalse(controller.fetchedObjects!.isEmpty) } - func testRefreshAllObjects() throws { + func test_RefreshAllObjects() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -44,7 +48,8 @@ final class FetchedResultsChangesTests: InMemoryTestCase { let delegate = MockNSFetchedResultControllerDelegate() // When - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) + let controller = NSFetchedResultsController( + fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) controller.delegate = delegate try controller.performFetch() @@ -59,15 +64,15 @@ final class FetchedResultsChangesTests: InMemoryTestCase { XCTAssertEqual(controller.fetchRequest, request) XCTAssertFalse(controller.fetchedObjects!.isEmpty) - delegate.changes.forEach { change in - switch change { + for change in delegate.changes { + switch change { case .delete(object: _, indexPath: _), .insert(object: _, indexPath: _): XCTFail("Unexpected change.") - default: break // during a regresh we can have an update or a move + default: break // during a regresh we can have an update or a move } } } - func testObjectsChanges() throws { + func test_ObjectsChanges() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -79,7 +84,8 @@ final class FetchedResultsChangesTests: InMemoryTestCase { let delegate = MockNSFetchedResultControllerDelegate() // When - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) + let controller = NSFetchedResultsController( + fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) controller.delegate = delegate try controller.performFetch() @@ -128,10 +134,9 @@ final class FetchedResultsChangesTests: InMemoryTestCase { XCTAssertEqual(deletes.count, 1) XCTAssertEqual(moves.count, 1) XCTAssertEqual(updates.count, 2) - } - func testSectionsChanges() throws { + func test_SectionsChanges() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -143,10 +148,11 @@ final class FetchedResultsChangesTests: InMemoryTestCase { let delegate = MockNSFetchedResultControllerDelegate() // When - let controller = NSFetchedResultsController(fetchRequest: request, - managedObjectContext: context, - sectionNameKeyPath: #keyPath(Person.lastName), - cacheName: nil) + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: #keyPath(Person.lastName), + cacheName: nil) controller.delegate = delegate try controller.performFetch() @@ -176,7 +182,7 @@ final class FetchedResultsChangesTests: InMemoryTestCase { XCTAssertEqual(deletes.count, 2) } - func testDeleteDueToThePredicate() throws { + func test_DeleteDueToThePredicate() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -190,12 +196,16 @@ final class FetchedResultsChangesTests: InMemoryTestCase { let delegate = MockNSFetchedResultControllerDelegate() // When - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) + let controller = NSFetchedResultsController(fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil) controller.delegate = delegate try controller.performFetch() let car = controller.fetchedObjects!.first - car!.numberPlate = "304 no more" // car needs to be deleted because the numberPlate doesn't fullfil the predicate anymore + // car needs to be deleted because the numberPlate doesn't fullfil the predicate anymore + car!.numberPlate = "304 no more" try context.save() // Then @@ -215,20 +225,34 @@ final class FetchedResultsChangesTests: InMemoryTestCase { } } -fileprivate final class MockNSFetchedResultControllerDelegate: NSObject, NSFetchedResultsControllerDelegate { +private final class MockNSFetchedResultControllerDelegate: NSObject, + NSFetchedResultsControllerDelegate +{ var changes = [FetchedResultsObjectChange]() var sectionChanges = [FetchedResultsSectionChange]() var willChangeExpectation = XCTestExpectation(description: "\(#function)\(#line)") var didChangeExpectation = XCTestExpectation(description: "\(#function)\(#line)") - public func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - if let change = FetchedResultsObjectChange(object: anObject, indexPath: indexPath, changeType: type, newIndexPath: newIndexPath) { + public func controller(_ controller: NSFetchedResultsController, + didChange anObject: Any, + at indexPath: IndexPath?, + for type: NSFetchedResultsChangeType, newIndexPath: IndexPath? + ) { + if let change = FetchedResultsObjectChange(object: anObject, + indexPath: indexPath, + changeType: type, + newIndexPath: newIndexPath) + { changes.append(change) } } - public func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { - if let change = FetchedResultsSectionChange(section: sectionInfo, index: sectionIndex, changeType: type) { + public func controller(_ controller: NSFetchedResultsController, + didChange sectionInfo: NSFetchedResultsSectionInfo, + atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { + if let change = FetchedResultsSectionChange(section: sectionInfo, + index: sectionIndex, + changeType: type) { sectionChanges.append(change) } } diff --git a/Tests/FetchesWithAffectedStoresTests.swift b/Tests/FetchesWithAffectedStores_Tests.swift similarity index 62% rename from Tests/FetchesWithAffectedStoresTests.swift rename to Tests/FetchesWithAffectedStores_Tests.swift index 9e4b0fbc..dcccb77f 100644 --- a/Tests/FetchesWithAffectedStoresTests.swift +++ b/Tests/FetchesWithAffectedStores_Tests.swift @@ -1,20 +1,22 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus /// Tests with fetch requests targeting specific persistent stores -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -final class FetchesWithAffectedStoresTests: XCTestCase { - func testFetches() throws { +final class FetchesWithAffectedStores_Tests: XCTestCase { + func test_Fetches() throws { let uuid = UUID().uuidString let url1 = URL.newDatabaseURL(withName: "part1-\(uuid)") let url2 = URL.newDatabaseURL(withName: "part2-\(uuid)") let psc = NSPersistentStoreCoordinator(managedObjectModel: V2.makeManagedObjectModel()) - try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part1, at: url1, options: nil) - try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part2, at: url2, options: nil) + try psc.addPersistentStore( + ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part1, at: url1, options: nil) + try psc.addPersistentStore( + ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part2, at: url2, options: nil) let part1 = try XCTUnwrap(psc.persistentStores.first { $0.configurationName == V2.Configurations.part1 }) let part2 = try XCTUnwrap(psc.persistentStores.first { $0.configurationName == V2.Configurations.part2 }) @@ -63,7 +65,8 @@ final class FetchesWithAffectedStoresTests: XCTestCase { // fetchOne context.reset() let one = try FeedbackV2.fetchOneObject(in: context, where: predicate) - XCTAssertEqual(one?.objectID, feedbackPart1.objectID) // first inserted is the first one to be fetched during a query + // first inserted is the first one to be fetched during a query + XCTAssertEqual(one?.objectID, feedbackPart1.objectID) let onePart1 = try FeedbackV2.fetchOneObject(in: context, where: predicate, affectedStores: [part1]) XCTAssertEqual(onePart1?.objectID, feedbackPart1.objectID) let onePart2 = try FeedbackV2.fetchOneObject(in: context, where: predicate, affectedStores: [part2]) @@ -80,7 +83,9 @@ final class FetchesWithAffectedStoresTests: XCTestCase { // findUniqueOrCreate (deprecated) context.reset() let predicate2 = NSPredicate(format: "%K == %@", #keyPath(Feedback.authorAlias), "Andrea") - let objPart1 = try FeedbackV2.findUniqueOrCreate(in: context, where: predicate2, affectedStores: [part1], assignedStore: part1) { feedback in + let objPart1 = try FeedbackV2.findUniqueOrCreate( + in: context, where: predicate2, affectedStores: [part1], assignedStore: part1 + ) { feedback in feedback.authorAlias = "Andrea" feedback.bookID = sharedUUID feedback.comment = "ok" @@ -89,12 +94,15 @@ final class FetchesWithAffectedStoresTests: XCTestCase { XCTAssertTrue(objPart1.hasTemporaryID) try context.save() - let obj2Part1 = try FeedbackV2.findUniqueOrCreate(in: context, where: predicate2, affectedStores: [part1]) { feedback in + let obj2Part1 = try FeedbackV2.findUniqueOrCreate(in: context, where: predicate2, affectedStores: [part1]) { + feedback in XCTFail("There should be another object matching this predicate.") } XCTAssertFalse(obj2Part1.objectID.isTemporaryID) - let objPart2 = try FeedbackV2.findUniqueOrCreate(in: context, where: predicate2, affectedStores: [part2], assignedStore: part2) { feedback in + let objPart2 = try FeedbackV2.findUniqueOrCreate( + in: context, where: predicate2, affectedStores: [part2], assignedStore: part2 + ) { feedback in feedback.authorAlias = "Andrea" feedback.bookID = sharedUUID feedback.comment = "ok" @@ -102,21 +110,24 @@ final class FetchesWithAffectedStoresTests: XCTestCase { } XCTAssertTrue(objPart2.objectID.isTemporaryID) try context.save() - XCTAssertEqual(try FeedbackV2.count(in: context){ $0.predicate = predicate2 }, 2) + XCTAssertEqual(try FeedbackV2.count(in: context) { $0.predicate = predicate2 }, 2) // delete context.reset() - try FeedbackV2.delete(in: context, includingSubentities: true, where: predicate2, limit: nil, affectedStores: [part1]) - XCTAssertEqual(try FeedbackV2.count(in: context) { - $0.predicate = predicate2 - $0.affectedStores = [part1] - }, 0) + try FeedbackV2.delete( + in: context, includingSubentities: true, where: predicate2, limit: nil, affectedStores: [part1]) + XCTAssertEqual( + try FeedbackV2.count(in: context) { + $0.predicate = predicate2 + $0.affectedStores = [part1] + }, 0) try context.save() - XCTAssertEqual(try FeedbackV2.count(in: context){ $0.predicate = predicate2 }, 1) - try FeedbackV2.delete(in: context, includingSubentities: true, where: predicate2, limit: nil, affectedStores: nil) // no affectedStores -> part1 & part2 + XCTAssertEqual(try FeedbackV2.count(in: context) { $0.predicate = predicate2 }, 1) + // no affectedStores -> part1 & part2 + try FeedbackV2.delete(in: context, includingSubentities: true, where: predicate2, limit: nil, affectedStores: nil) try context.save() - XCTAssertEqual(try FeedbackV2.count(in: context){ $0.predicate = predicate2 }, 0) + XCTAssertEqual(try FeedbackV2.count(in: context) { $0.predicate = predicate2 }, 0) // delete excluding objects context.reset() @@ -129,20 +140,24 @@ final class FetchesWithAffectedStoresTests: XCTestCase { try context.save() try FeedbackV2.delete(in: context, except: [feedbackPart1, feedbackPart2], affectedStores: [part2]) - XCTAssertEqual(try FeedbackV2.count(in: context) { - $0.predicate = predicate2 - $0.affectedStores = [part1] - }, 1) // feedback2Part1 is still there because the delete request has affected only part2 - - XCTAssertEqual(try FeedbackV2.count(in: context) { - $0.predicate = predicate2 - $0.affectedStores = [part2] - }, 0) - try FeedbackV2.delete(in: context, except: [feedbackPart1, feedbackPart2], affectedStores: nil) // no affectedStores -> part1 & part2 - XCTAssertEqual(try FeedbackV2.count(in: context) { - $0.predicate = predicate2 - $0.affectedStores = [part1] - }, 0) + XCTAssertEqual( + try FeedbackV2.count(in: context) { + $0.predicate = predicate2 + $0.affectedStores = [part1] + }, 1) // feedback2Part1 is still there because the delete request has affected only part2 + + XCTAssertEqual( + try FeedbackV2.count(in: context) { + $0.predicate = predicate2 + $0.affectedStores = [part2] + }, 0) + // no affectedStores -> part1 & part2 + try FeedbackV2.delete(in: context, except: [feedbackPart1, feedbackPart2], affectedStores: nil) + XCTAssertEqual( + try FeedbackV2.count(in: context) { + $0.predicate = predicate2 + $0.affectedStores = [part1] + }, 0) try context.save() context._fix_sqlite_warning_when_destroying_a_store() @@ -150,15 +165,17 @@ final class FetchesWithAffectedStoresTests: XCTestCase { try NSPersistentStoreCoordinator.destroyStore(at: url2) } - func testBatchRequests() throws { + func test_BatchRequests() throws { let uuid = UUID().uuidString let url1 = URL.newDatabaseURL(withName: "part1-\(uuid)") let url2 = URL.newDatabaseURL(withName: "part2-\(uuid)") // Given let psc = NSPersistentStoreCoordinator(managedObjectModel: V2.makeManagedObjectModel()) - try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part1, at: url1, options: nil) - try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part2, at: url2, options: nil) + try psc.addPersistentStore( + ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part1, at: url1, options: nil) + try psc.addPersistentStore( + ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part2, at: url2, options: nil) let part1 = try XCTUnwrap(psc.persistentStores.first { $0.configurationName == V2.Configurations.part1 }) let part2 = try XCTUnwrap(psc.persistentStores.first { $0.configurationName == V2.Configurations.part2 }) @@ -166,36 +183,44 @@ final class FetchesWithAffectedStoresTests: XCTestCase { let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) context.persistentStoreCoordinator = psc - // When (insert) // part 1 - let feedback2: [String : Any] = [#keyPath(FeedbackV2.authorAlias): "Alessandro", - #keyPath(FeedbackV2.bookID): UUID(), - #keyPath(FeedbackV2.comment): "object2", - #keyPath(FeedbackV2.rating): 3.5] - let feedback4: [String : Any] = [#keyPath(FeedbackV2.authorAlias): "Alessandro", - #keyPath(FeedbackV2.bookID): UUID(), - #keyPath(FeedbackV2.comment): "object4", - #keyPath(FeedbackV2.rating): 3.5] + let feedback2: [String: Any] = [ + #keyPath(FeedbackV2.authorAlias): "Alessandro", + #keyPath(FeedbackV2.bookID): UUID(), + #keyPath(FeedbackV2.comment): "object2", + #keyPath(FeedbackV2.rating): 3.5, + ] + let feedback4: [String: Any] = [ + #keyPath(FeedbackV2.authorAlias): "Alessandro", + #keyPath(FeedbackV2.bookID): UUID(), + #keyPath(FeedbackV2.comment): "object4", + #keyPath(FeedbackV2.rating): 3.5, + ] // part 2 - let feedback1: [String : Any] = [#keyPath(FeedbackV2.authorAlias): "Alessandro", - #keyPath(FeedbackV2.bookID): UUID(), - #keyPath(FeedbackV2.comment): "object1", - #keyPath(FeedbackV2.rating): 3.5] - let feedback3: [String : Any] = [#keyPath(FeedbackV2.authorAlias): "Andrea", - #keyPath(FeedbackV2.bookID): UUID(), - #keyPath(FeedbackV2.comment): "object3", - #keyPath(FeedbackV2.rating): 2.5] - - let resultInsertPart1 = try FeedbackV2.batchInsert(using: context, resultType: .statusOnly, objects: [feedback2, feedback4], affectedStores: [part1]) - let resultInsertPart2 = try FeedbackV2.batchInsert(using: context, resultType: .statusOnly, objects: [feedback1, feedback3], affectedStores: [part2]) + let feedback1: [String: Any] = [ + #keyPath(FeedbackV2.authorAlias): "Alessandro", + #keyPath(FeedbackV2.bookID): UUID(), + #keyPath(FeedbackV2.comment): "object1", + #keyPath(FeedbackV2.rating): 3.5, + ] + let feedback3: [String: Any] = [ + #keyPath(FeedbackV2.authorAlias): "Andrea", + #keyPath(FeedbackV2.bookID): UUID(), + #keyPath(FeedbackV2.comment): "object3", + #keyPath(FeedbackV2.rating): 2.5, + ] + + let resultInsertPart1 = try FeedbackV2.batchInsert( + using: context, resultType: .statusOnly, objects: [feedback2, feedback4], affectedStores: [part1]) + let resultInsertPart2 = try FeedbackV2.batchInsert( + using: context, resultType: .statusOnly, objects: [feedback1, feedback3], affectedStores: [part2]) // Then XCTAssertTrue(resultInsertPart1.status!) XCTAssertTrue(resultInsertPart2.status!) XCTAssertEqual(try FeedbackV2.count(in: context) { $0.affectedStores = [part1] }, 2) XCTAssertEqual(try FeedbackV2.count(in: context) { $0.affectedStores = [part2] }, 2) - // When (update) let predicateAlessandro = NSPredicate(format: "%K == %@", #keyPath(FeedbackV2.authorAlias), "Alessandro") @@ -210,20 +235,37 @@ final class FetchesWithAffectedStoresTests: XCTestCase { // Then XCTAssertTrue(resultUpdatePart1.status!) XCTAssertEqual(try FeedbackV2.count(in: context) { $0.predicate = predicateAlessandro }, 2) - XCTAssertEqual(try FeedbackV2.count(in: context) { $0.predicate = predicateAlessandro; $0.affectedStores = [part1] }, 1) - XCTAssertEqual(try FeedbackV2.count(in: context) { $0.predicate = predicateAlessandro; $0.affectedStores = [part2] }, 1) + XCTAssertEqual( + try FeedbackV2.count(in: context) { + $0.predicate = predicateAlessandro + $0.affectedStores = [part1] + }, 1) + XCTAssertEqual( + try FeedbackV2.count(in: context) { + $0.predicate = predicateAlessandro + $0.affectedStores = [part2] + }, 1) // When (delete) // delete feedback in part2 where the authorAlias is Alessandro (1) - let resultDeletePart2 = try FeedbackV2.batchDelete(using: context, - predicate: predicateAlessandro, - resultType: .resultTypeCount, - affectedStores: [part2]) + let resultDeletePart2 = try FeedbackV2.batchDelete( + using: context, + predicate: predicateAlessandro, + resultType: .resultTypeCount, + affectedStores: [part2]) // Then XCTAssertEqual(resultDeletePart2.count!, 1) - XCTAssertEqual(try FeedbackV2.count(in: context) { $0.predicate = predicateAlessandro; $0.affectedStores = [part2] }, 0) - XCTAssertEqual(try FeedbackV2.count(in: context) { $0.predicate = predicateAlessandro; $0.affectedStores = [part1] }, 1) + XCTAssertEqual( + try FeedbackV2.count(in: context) { + $0.predicate = predicateAlessandro + $0.affectedStores = [part2] + }, 0) + XCTAssertEqual( + try FeedbackV2.count(in: context) { + $0.predicate = predicateAlessandro + $0.affectedStores = [part1] + }, 1) let resultDeleteAll = try FeedbackV2.batchDelete(using: context, affectedStores: [part1, part2]) XCTAssertTrue(resultDeleteAll.status!) diff --git a/Tests/MigrationsTests.swift b/Tests/Migrations_Tests.swift similarity index 67% rename from Tests/MigrationsTests.swift rename to Tests/Migrations_Tests.swift index 42d7c6d6..fcdc6b80 100644 --- a/Tests/MigrationsTests.swift +++ b/Tests/Migrations_Tests.swift @@ -1,26 +1,29 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest +import os.lock + @testable import CoreDataPlus -final class MigrationsTests: BaseTestCase { +final class Migrations_Tests: BaseTestCase { // MARK: - LightWeight Migration - func testMigrationFromNotExistingPersistentStore() { + func test_MigrationFromNotExistingPersistentStore() { let url = URL(fileURLWithPath: "/path/to/nothing.sqlite") let sourceDescription = NSPersistentStoreDescription(url: url) let destinationDescription = NSPersistentStoreDescription(url: url) - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: .version3) + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: .version3) XCTAssertThrowsError(try migrator.migrate(enableWALCheckpoint: true), "The store shouldn't exist.") } - func testMigrationEdgeCases() throws { + func test_MigrationEdgeCases() throws { let name = "SampleModel-\(UUID())" - let container = NSPersistentContainer(name: name, managedObjectModel: model) + let container = NSPersistentContainer(name: name, managedObjectModel: model1) let context = container.viewContext let expectation1 = expectation(description: "\(#function)\(#line)") @@ -51,13 +54,15 @@ final class MigrationsTests: BaseTestCase { let enableWALCheckpoint = false let sourceDescription = NSPersistentStoreDescription(url: sourceURL) let destinationDescription = NSPersistentStoreDescription(url: sourceURL) - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: targetVersion) + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: targetVersion) try migrator.migrate(enableWALCheckpoint: enableWALCheckpoint) // ⚠️ migration should be done before loading the NSPersistentContainer instance or you need to create a new one after the migration - let migratedContainer = NSPersistentContainer(name: name, managedObjectModel: targetVersion.managedObjectModel()) + let migratedContainer = NSPersistentContainer(name: name, + managedObjectModel: targetVersion.managedObjectModel()) let expectation2 = expectation(description: "\(#function)\(#line)") migratedContainer.loadPersistentStores { (store, error) in @@ -68,36 +73,45 @@ final class MigrationsTests: BaseTestCase { let context2 = migratedContainer.viewContext let luxuryCars = try context2.fetch(NSFetchRequest(entityName: "LuxuryCar")) - luxuryCars.forEach { XCTAssertNotNil($0.value(forKey: "isLimitedEdition")) } + for car in luxuryCars { + XCTAssertNotNil(car.value(forKey: "isLimitedEdition")) + } } - func testIfMigrationIsNeeded() throws { + func test_IfMigrationIsNeeded() throws { let bundle = Bundle.tests - let sourceURLV1 = try XCTUnwrap(bundle.url(forResource: "SampleModelV1", withExtension: "sqlite")) - let sourceURLV2 = try XCTUnwrap(bundle.url(forResource: "SampleModelV2", withExtension: "sqlite")) - let migrationNeededFromV1toV1 = try CoreDataPlus.isMigrationNecessary(for: sourceURLV1, to: SampleModelVersion.version1) + let sourceURLV1 = try XCTUnwrap(bundle.url(forResource: "SampleModel_V1", withExtension: "sqlite")) + let sourceURLV2 = try XCTUnwrap(bundle.url(forResource: "SampleModel_V2", withExtension: "sqlite")) + let migrationNeededFromV1toV1 = try CoreDataPlus.isMigrationNecessary( + for: sourceURLV1, to: SampleModelVersion.version1) XCTAssertFalse(migrationNeededFromV1toV1) - let migrationNeededFromV1toV2 = try CoreDataPlus.isMigrationNecessary(for: sourceURLV1, to: SampleModelVersion.version2) + let migrationNeededFromV1toV2 = try CoreDataPlus.isMigrationNecessary( + for: sourceURLV1, to: SampleModelVersion.version2) XCTAssertTrue(migrationNeededFromV1toV2) - let migrationNeededFromV1toV3 = try CoreDataPlus.isMigrationNecessary(for: sourceURLV1, to: SampleModelVersion.version3) + let migrationNeededFromV1toV3 = try CoreDataPlus.isMigrationNecessary( + for: sourceURLV1, to: SampleModelVersion.version3) XCTAssertTrue(migrationNeededFromV1toV3) - let migrationNeededFromV2toV1 = try CoreDataPlus.isMigrationNecessary(for: sourceURLV2, to: SampleModelVersion.version1) + let migrationNeededFromV2toV1 = try CoreDataPlus.isMigrationNecessary( + for: sourceURLV2, to: SampleModelVersion.version1) XCTAssertFalse(migrationNeededFromV2toV1) } - func testMigrationFromV1toV1() throws { - let sourceURL = try createSQLiteSampleForV1() + func test_MigrationFromV1toV1() throws { + let sourceURL = try Self.createSQLiteSample1ForV1() let sourceDescription = NSPersistentStoreDescription(url: sourceURL) let destinationDescription = NSPersistentStoreDescription(url: sourceURL) - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: .version1) + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: .version1) try migrator.migrate(enableWALCheckpoint: true) + + try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } - func testMigrationFromV1ToV2() throws { - let sourceURL = try createSQLiteSampleForV1() + func test_MigrationFromV1ToV2() throws { + let sourceURL = try Self.createSQLiteSample1ForV1() let targetVersion = SampleModelVersion.version2 let steps = SampleModelVersion.version1.migrationSteps(to: .version2) @@ -108,49 +122,48 @@ final class MigrationsTests: BaseTestCase { // When let targetDescription = NSPersistentStoreDescription(url: sourceURL) - let migrator = Migrator(targetStoreDescription: - targetDescription, + let migrator = Migrator(targetStoreDescription: targetDescription, targetVersion: targetVersion) migrator.enableLog = true migrator.enableLog = false - var completion = 0.0 + let completion = OSAllocatedUnfairLock(initialState: 0.0) let token = migrator.progress.observe(\.fractionCompleted, options: [.new]) { (progress, change) in - completion = progress.fractionCompleted + completion.withLock { $0 = progress.fractionCompleted } } try migrator.migrate(enableWALCheckpoint: true) - let migratedContext = NSManagedObjectContext(model: targetVersion.managedObjectModel(), storeURL: sourceURL) + let migratedContext = NSManagedObjectContext(model: targetVersion.managedObjectModel(), + storeURL: sourceURL) let luxuryCars = try LuxuryCar.fetchObjects(in: migratedContext) XCTAssertEqual(luxuryCars.count, 5) let cars = try migratedContext.fetch(NSFetchRequest(entityName: "Car")) XCTAssertTrue(cars.count >= 1) - if #available(iOS 11, tvOS 11, watchOS 4, macOS 10.13, *) { - cars.forEach { car in - if car is LuxuryCar || car is SportCar { - XCTAssertEqual(car.entity.indexes.count, 0) - } else if car is Car { - let index = car.entity.indexes.first - XCTAssertNotNil(index, "There should be a compound index") - XCTAssertEqual(index!.elements.count, 2) - } else { - XCTFail("Undefined") - } + for car in cars { + if car is LuxuryCar || car is SportCar { + XCTAssertEqual(car.entity.indexes.count, 0) + } else if car is Car { + let index = car.entity.indexes.first + XCTAssertNotNil(index, "There should be a compound index") + XCTAssertEqual(index!.elements.count, 2) + } else { + XCTFail("Undefined") } } - XCTAssertEqual(completion, 1.0) + XCTAssertEqual(completion.withLock { $0 }, 1.0) migratedContext._fix_sqlite_warning_when_destroying_a_store() token.invalidate() + try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } - func testMigrationFromV1ToV2UsingCustomMigratorProvider() throws { - let sourceURL = try createSQLiteSampleForV1() - + func test_MigrationFromV1ToV2UsingCustomMigratorProvider() throws { + let sourceURL = try Self.createSQLiteSample1ForV1() + let targetVersion = SampleModelVersion.version2 let steps = SampleModelVersion.version1.migrationSteps(to: .version2) XCTAssertEqual(steps.count, 1) @@ -161,23 +174,18 @@ final class MigrationsTests: BaseTestCase { let sourceDescription = NSPersistentStoreDescription(url: sourceURL) let destinationDescription = NSPersistentStoreDescription(url: sourceURL) - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: targetVersion) + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: targetVersion) // When - var completion = 0.0 + let completion = OSAllocatedUnfairLock(initialState: 0.0) let token = migrator.progress.observe(\.fractionCompleted, options: [.new]) { (progress, change) in - completion = progress.fractionCompleted + completion.withLock { $0 = progress.fractionCompleted } } - try migrator.migrate(enableWALCheckpoint: true) { metadata in - XCTAssertTrue(metadata.mappingModel.isInferred) - let manager = LightweightMigrationManager(sourceModel: metadata.sourceModel, destinationModel: metadata.destinationModel) - manager.updateProgressInterval = 0.001 // we need to set a very low refresh interval to get some fake progress updates - manager.estimatedTime = 0.1 - return manager - } + try migrator.migrate(enableWALCheckpoint: true) let migratedContext = NSManagedObjectContext(model: targetVersion.managedObjectModel(), storeURL: sourceURL) let luxuryCars = try LuxuryCar.fetchObjects(in: migratedContext) @@ -186,30 +194,29 @@ final class MigrationsTests: BaseTestCase { let cars = try migratedContext.fetch(NSFetchRequest(entityName: "Car")) XCTAssertTrue(cars.count >= 1) - if #available(iOS 11, tvOS 11, watchOS 4, macOS 10.13, *) { - cars.forEach { car in - if car is LuxuryCar || car is SportCar { - XCTAssertEqual(car.entity.indexes.count, 0) - } else if car is Car { - let index = car.entity.indexes.first - XCTAssertNotNil(index, "There should be a compound index") - XCTAssertEqual(index!.elements.count, 2) - } else { - XCTFail("Undefined") - } + for car in cars { + if car is LuxuryCar || car is SportCar { + XCTAssertEqual(car.entity.indexes.count, 0) + } else if car is Car { + let index = car.entity.indexes.first + XCTAssertNotNil(index, "There should be a compound index") + XCTAssertEqual(index!.elements.count, 2) + } else { + XCTFail("Undefined") } } - XCTAssertEqual(completion, 1.0) + XCTAssertEqual(completion.withLock { $0 }, 1.0) migratedContext._fix_sqlite_warning_when_destroying_a_store() token.invalidate() + try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } // MARK: - HeavyWeight Migration - func testMigrationFromV2ToV3() throws { - let sourceURL = try createSQLiteSampleForV2() + func test_MigrationFromV2ToV3() throws { + let sourceURL = try Self.createSQLiteSample1ForV2() let targetURL = sourceURL let version = try SampleModelVersion(persistentStoreURL: sourceURL as URL) @@ -223,13 +230,14 @@ final class MigrationsTests: BaseTestCase { targetVersion: .version3) try migrator.migrate(enableWALCheckpoint: true) - let migratedContext = NSManagedObjectContext(model: SampleModelVersion.version3.managedObjectModel(), storeURL: targetURL) + let migratedContext = NSManagedObjectContext(model: SampleModelVersion.version3.managedObjectModel(), + storeURL: targetURL) let cars = try migratedContext.fetch(NSFetchRequest(entityName: "Car")) let makers = try Maker.fetchObjects(in: migratedContext) XCTAssertEqual(makers.count, 10) XCTAssertEqual(cars.count, 125) - cars.forEach { object in + for object in cars { let owner = object.value(forKey: "owner") as? NSManagedObject let maker = object.value(forKey: "createdBy") as? NSManagedObject XCTAssertNotNil(maker) @@ -251,13 +259,12 @@ final class MigrationsTests: BaseTestCase { XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) migratedContext._fix_sqlite_warning_when_destroying_a_store() - try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } - func testCancelMigrationFromV2ToV3() throws { + func test_CancelMigrationFromV2ToV3() throws { // Given - let sourceURL = try createSQLiteSampleForV2() + let sourceURL = try Self.createSQLiteSample1ForV2() let sourceDescription = NSPersistentStoreDescription(url: sourceURL) let destinationDescription = NSPersistentStoreDescription(url: sourceURL) let migrator = Migrator(sourceStoreDescription: sourceDescription, @@ -267,64 +274,72 @@ final class MigrationsTests: BaseTestCase { // When migrator.progress.cancel() // Then - XCTAssertThrowsError(try migrator.migrate(enableWALCheckpoint: true), - "The migrator should throw an error because the progress has cancelled the migration steps") { error in + XCTAssertThrowsError( + try migrator.migrate(enableWALCheckpoint: true), + "The migrator should throw an error because the progress has cancelled the migration steps" + ) { error in let nserror = error as NSError XCTAssertEqual(nserror.domain, NSError.migrationCancelled.domain) XCTAssertEqual(nserror.code, NSError.migrationCancelled.code) } // When - let migrator2 = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: .version3) + let migrator2 = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: .version3) migrator2.enableLog = true - XCTAssertNoThrow(try migrator2.migrate(enableWALCheckpoint: true), "A new migrator should handle the migration phase without any errors.") + XCTAssertNoThrow( + try migrator2.migrate(enableWALCheckpoint: true), + "A new migrator should handle the migration phase without any errors.") try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } - func testMigrationFromV1ToV3() throws { - let sourceURL = try createSQLiteSampleForV1() - + func test_MigrationFromV1ToV3() throws { + let sourceURL = try Self.createSQLiteSample1ForV1() let version = try SampleModelVersion(persistentStoreURL: sourceURL as URL) - XCTAssertTrue(version == .version1) + + XCTAssertTrue(SampleModelVersion.version1.isLightWeightMigrationPossibleToNextModelVersion()) + XCTAssertTrue(SampleModelVersion.version2.isLightWeightMigrationPossibleToNextModelVersion()) + XCTAssertFalse(SampleModelVersion.version3.isLightWeightMigrationPossibleToNextModelVersion()) - let targetURL = URL.temporaryDirectoryURL.appendingPathComponent("SampleModel").appendingPathExtension("sqlite") + let targetURL = URL.temporaryDirectory.appendingPathComponent("SampleModel").appendingPathExtension("sqlite") let sourceDescription = NSPersistentStoreDescription(url: sourceURL) let destinationDescription = NSPersistentStoreDescription(url: targetURL) let migrator = Migrator(sourceStoreDescription: sourceDescription, destinationStoreDescription: destinationDescription, targetVersion: .version3) migrator.enableLog = true - var completion = 0.0 + let completion = OSAllocatedUnfairLock(initialState: 0.0) let token = migrator.progress.observe(\.fractionCompleted, options: [.new]) { (progress, change) in - print(progress.fractionCompleted) - completion = progress.fractionCompleted + //print(progress.fractionCompleted) + completion.withLock { $0 = progress.fractionCompleted } } try migrator.migrate(enableWALCheckpoint: true) - - let migratedContext = NSManagedObjectContext(model: SampleModelVersion.version3.managedObjectModel(), storeURL: targetURL) + let migratedContext = NSManagedObjectContext(model: SampleModelVersion.version3.managedObjectModel(), + storeURL: targetURL) let makers = try migratedContext.fetch(NSFetchRequest(entityName: "Maker")) XCTAssertEqual(makers.count, 10) - makers.forEach { (maker) in + for maker in makers { let name = maker.value(forKey: "name") as! String maker.setValue("--\(name)--", forKey: "name") } try migratedContext.save() - XCTAssertEqual(completion, 1.0) + XCTAssertEqual(completion.withLock { $0 }, 1.0) XCTAssertFalse(FileManager.default.fileExists(atPath: sourceURL.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: targetURL.path)) migratedContext._fix_sqlite_warning_when_destroying_a_store() token.invalidate() + try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) } - func testInvestigationProgress() { + func test_InvestigationProgress() { let expectationChildCancelled = expectation(description: "Child Progress cancelled") let expectationGrandChildCancelled = expectation(description: "Grandchild Progress cancelled") let progress = Progress(totalUnitCount: 1) @@ -340,34 +355,38 @@ final class MigrationsTests: BaseTestCase { } } -extension MigrationsTests { - func createSQLiteSampleForV1() throws -> URL { - let bundle = Bundle.tests - let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModelV1", withExtension: "sqlite")) // 125 cars, 5 sport cars +// MARK: - Sample creation +extension Migrations_Tests { + static func createSQLiteSample1ForV1() throws -> URL { + let bundle = Bundle.tests + // 125 cars, 5 sport cars + let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel_V1", withExtension: "sqlite")) + // Being the test run multiple times, we create an unique copy for every test let uuid = UUID().uuidString - let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModelV1_copy-\(uuid).sqlite") + let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel_V1_copy-\(uuid).sqlite") try FileManager.default.copyItem(at: _sourceURL, to: sourceURL) XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) return sourceURL } - - func createSQLiteSampleForV2() throws -> URL { + + static func createSQLiteSample1ForV2() throws -> URL { let bundle = Bundle.tests - let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModelV2", withExtension: "sqlite")) // 125 cars, 5 sport cars - + // 125 cars, 5 sport cars + let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel_V2", withExtension: "sqlite")) + // Being the test run multiple times, we create an unique copy for every test let uuid = UUID().uuidString - let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModelV2_copy-\(uuid).sqlite") + let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel_V2_copy-\(uuid).sqlite") try FileManager.default.copyItem(at: _sourceURL, to: sourceURL) XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) return sourceURL } - + /// Creates a .sqlite with some data for the initial model (version 1) - func createSampleVersion1(completion: @escaping (Result) -> Void) { - let containerSQLite = NSPersistentContainer(name: "SampleModel-\(UUID())", managedObjectModel: model) + static func createSampleVersion1(completion: @escaping (Result) -> Void) { + let containerSQLite = NSPersistentContainer(name: "SampleModel-\(UUID())", managedObjectModel: model1) containerSQLite.loadPersistentStores { (description, error) in if let error = error { completion(.failure(error)) diff --git a/Tests/ModelVersionTests.swift b/Tests/ModelVersion_Tests.swift similarity index 75% rename from Tests/ModelVersionTests.swift rename to Tests/ModelVersion_Tests.swift index 549d3f06..ed0dd1c2 100644 --- a/Tests/ModelVersionTests.swift +++ b/Tests/ModelVersion_Tests.swift @@ -1,21 +1,22 @@ // CoreDataPlus import XCTest + @testable import CoreDataPlus -final class ModelVersionTests: XCTestCase { - func testInvalidInitialization() { +final class ModelVersion_Tests: XCTestCase { + func test_InvalidInitialization() { XCTAssertThrowsError(try SampleModelVersion(persistentStoreURL: URL(string: "wrong-url")!)) } - func testVersionModelSetup() { + func test_VersionModelSetup() { XCTAssertTrue(SampleModelVersion.currentVersion == .version1) XCTAssertTrue(SampleModelVersion.allVersions == [.version1, .version2, .version3]) - XCTAssertTrue(SampleModelVersion.version1.successor == .version2) + XCTAssertTrue(SampleModelVersion.version1.next == .version2) XCTAssertNil(SampleModelVersion.version3.mappingModelToNextModelVersion()) } - func testMappingModelsByName() { + func test_MappingModelsByName() { do { let models = SampleModelVersion.version2.mappingModels(for: ["V2toV3"]) XCTAssertEqual(models.count, 1) diff --git a/Tests/NSAttributeDescriptionUtilsTests.swift b/Tests/NSAttributeDescriptionUtils_Tests.swift similarity index 81% rename from Tests/NSAttributeDescriptionUtilsTests.swift rename to Tests/NSAttributeDescriptionUtils_Tests.swift index 633a4427..0b930315 100644 --- a/Tests/NSAttributeDescriptionUtilsTests.swift +++ b/Tests/NSAttributeDescriptionUtils_Tests.swift @@ -1,102 +1,105 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSAttributeDescriptionUtilsTests: XCTestCase { - func testInt16() { +final class NSAttributeDescriptionUtils_Tests: XCTestCase { + func test_Int16() { let attribute = NSAttributeDescription.int16(name: #function, defaultValue: 1) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? Int16, 1) } - func testInt32() { + func test_Int32() { let attribute = NSAttributeDescription.int32(name: #function, defaultValue: 11) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? Int32, 11) } - func testInt64() { + func test_Int64() { let attribute = NSAttributeDescription.int64(name: #function, defaultValue: nil) XCTAssertEqual(attribute.name, #function) XCTAssertNil(attribute.defaultValue) } - func testDecimal() { + func test_Decimal() { let attribute = NSAttributeDescription.decimal(name: #function, defaultValue: 1.11111) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? NSDecimalNumber, NSDecimalNumber(decimal: 1.11111)) } - func testFloat() { + func test_Float() { let attribute = NSAttributeDescription.float(name: #function, defaultValue: 1.11111) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? Float, 1.11111) } - func testDouble() { + func test_Double() { let attribute = NSAttributeDescription.double(name: #function, defaultValue: 1.11111) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? Double, 1.11111) } - func testString() { + func test_String() { let string = "hello world" let attribute = NSAttributeDescription.string(name: #function, defaultValue: string) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? String, string) } - func testBool() { + func test_Bool() { let attribute = NSAttributeDescription.bool(name: #function, defaultValue: false) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? Bool, false) } - func testDate() { + func test_Date() { let date = Date() let attribute = NSAttributeDescription.date(name: #function, defaultValue: date) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? Date, date) } - func testUUID() { + func test_UUID() { let uuid = UUID() let attribute = NSAttributeDescription.uuid(name: #function, defaultValue: uuid) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? UUID, uuid) } - func testURI() { + func test_URI() { let url = URL(string: "http://www.alessandromarzoli.com/CoreDataPlus/")! let attribute = NSAttributeDescription.uri(name: #function, defaultValue: url) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? URL, url) } - func testBinaryData() { + func test_BinaryData() { let data = "Test".data(using: .utf8) - let attribute = NSAttributeDescription.binaryData(name: #function, - defaultValue: data, - allowsExternalBinaryDataStorage: true) + let attribute = NSAttributeDescription.binaryData( + name: #function, + defaultValue: data, + allowsExternalBinaryDataStorage: true) XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? Data, data) XCTAssertTrue(attribute.allowsExternalBinaryDataStorage) } - func testCustomTransformable() { + func test_CustomTransformable() { let color = Color(name: "color") - let attribute = NSAttributeDescription.customTransformable(for: Color.self, name: #function, defaultValue: color) { _ in - return nil + let attribute = NSAttributeDescription.customTransformable(for: Color.self, name: #function, defaultValue: color) { + _ in + nil } reverse: { _ in - return nil + nil } XCTAssertEqual(attribute.name, #function) XCTAssertEqual(attribute.defaultValue as? Color, color) } - func testTransformable() { + func test_Transformable() { let color = Color(name: "color") let attribute = NSAttributeDescription.transformable(for: Color.self, name: #function, defaultValue: color) XCTAssertEqual(attribute.name, #function) diff --git a/Tests/NSEntityDescriptionUtilsTests.swift b/Tests/NSEntityDescriptionUtils_Tests.swift similarity index 75% rename from Tests/NSEntityDescriptionUtilsTests.swift rename to Tests/NSEntityDescriptionUtils_Tests.swift index 90c720da..55882026 100644 --- a/Tests/NSEntityDescriptionUtilsTests.swift +++ b/Tests/NSEntityDescriptionUtils_Tests.swift @@ -1,29 +1,43 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -fileprivate extension NSManagedObject { - convenience init(usingContext context: NSManagedObjectContext) { +extension NSManagedObject { + fileprivate convenience init(usingContext context: NSManagedObjectContext) { let name = String(describing: type(of: self)) let entity = NSEntityDescription.entity(forEntityName: name, in: context)! self.init(entity: entity, insertInto: context) } } -final class NSEntityDescriptionUtilsTests: InMemoryTestCase { - func testEntity() { +// This entity is not mapped in any model and it will trigger an error: +// "No NSEntityDescriptions in any model claim the NSManagedObject subclass 'CoreDataPlus_Tests.FakeEntity' +// so +entity is confused. Have you loaded your NSManagedObjectModel yet ?" +private class FakeEntity: NSManagedObject { } + +final class NSEntityDescriptionUtils_Tests: InMemoryTestCase { + func test_EntityName() { + XCTAssertEqual(NSManagedObject.entityName, "NSManagedObject") + XCTAssertEqual(FakeEntity.entityName, "FakeEntity") + XCTAssertEqual(SportCar.entityName, "SportCar") + } + + func test_Entity() { let context = container.viewContext let expensiveCar = ExpensiveSportCar(context: context) - let entityNames = expensiveCar.entity.ancestorEntities().compactMap { $0.name} + XCTAssertEqual(SportCar.entityName, "SportCar") + let entityNames = expensiveCar.entity.ancestorEntities().compactMap { $0.name } XCTAssertTrue(entityNames.count == 2) XCTAssertTrue(entityNames.contains(Car.entityName)) XCTAssertTrue(entityNames.contains(SportCar.entityName)) - XCTAssertFalse(entityNames.contains(ExpensiveSportCar.entityName), "The hierarchy should contain only super entities") + XCTAssertFalse( + entityNames.contains(ExpensiveSportCar.entityName), "The hierarchy should contain only super entities") } - func testTopMostEntity() { + func test_TopMostEntity() { /// Making sure that all the necessary bits are available guard let model = container.viewContext.persistentStoreCoordinator?.managedObjectModel else { @@ -36,10 +50,12 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { XCTFail("Car Entity not found; available entities: \(entities)") return } - + // Car.entity().name can be nil while running tests // To avoid some random failed tests, the entity is created by looking in a context. - guard let carEntity = NSEntityDescription.entity(forEntityName: Car.entityName, in: container.viewContext) else { + guard + let carEntity = NSEntityDescription.entity(forEntityName: Car.entityName, in: container.viewContext) + else { XCTFail("Car Entity Not Found.") return } @@ -51,14 +67,17 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { // Using a custom init to avoid some problems during tests. let expensiveCar = ExpensiveSportCar(usingContext: container.viewContext) let topMostAncestorEntityForExpensiveCar = expensiveCar.entity.topMostAncestorEntity - XCTAssertTrue(topMostAncestorEntityForExpensiveCar == carEntity, "\(topMostAncestorEntityForExpensiveCar) should be a Car entity \(String(describing: topMostAncestorEntityForExpensiveCar.name)).") + XCTAssertTrue( + topMostAncestorEntityForExpensiveCar == carEntity, + "\(topMostAncestorEntityForExpensiveCar) should be a Car entity \(String(describing: topMostAncestorEntityForExpensiveCar.name))." + ) let car = Car(usingContext: container.viewContext) let topMostAncestorEntity = car.entity.topMostAncestorEntity XCTAssertTrue(topMostAncestorEntity == carEntity, "\(topMostAncestorEntity) should be a Car entity.") } - func testCommonEntityAncestor() { + func test_CommonEntityAncestor() { let context = container.viewContext do { @@ -132,12 +151,16 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { } - - func testEntitiesKeepingOnlyCommonEntityAncestors() { + func test_EntitiesKeepingOnlyCommonEntityAncestors() { let context = container.viewContext do { - let entities = [ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, SportCar(context: context).entity, SportCar(context: context).entity] + let entities = [ + ExpensiveSportCar(context: context).entity, + ExpensiveSportCar(context: context).entity, + SportCar(context: context).entity, + SportCar(context: context).entity, + ] let ancestors = Set(entities).entitiesKeepingOnlyCommonAncestorEntities() XCTAssertTrue(!ancestors.isEmpty) XCTAssertTrue(ancestors.count == 1) @@ -145,7 +168,11 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { } do { - let entities = [ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, SportCar(context: context).entity, Car(context: context).entity] + let entities = [ + ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, + SportCar(context: context).entity, + Car(context: context).entity, + ] let ancestors = Set(entities).entitiesKeepingOnlyCommonAncestorEntities() XCTAssertTrue(!ancestors.isEmpty) XCTAssertTrue(ancestors.count == 1) @@ -153,7 +180,11 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { } do { - let entities = [Car(context: context).entity, ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, SportCar(context: context).entity] + let entities = [ + Car(context: context).entity, ExpensiveSportCar(context: context).entity, + ExpensiveSportCar(context: context).entity, + SportCar(context: context).entity, + ] let ancestors = Set(entities).entitiesKeepingOnlyCommonAncestorEntities() XCTAssertTrue(!ancestors.isEmpty) XCTAssertTrue(ancestors.count == 1) @@ -161,7 +192,10 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { } do { - let entities = [SportCar(context: context).entity, Car(context: context).entity, ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, ] + let entities = [ + SportCar(context: context).entity, Car(context: context).entity, ExpensiveSportCar(context: context).entity, + ExpensiveSportCar(context: context).entity, + ] let ancestors = Set(entities).entitiesKeepingOnlyCommonAncestorEntities() XCTAssertTrue(!ancestors.isEmpty) XCTAssertTrue(ancestors.count == 1) @@ -219,7 +253,11 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { /// 2+ do { - let entities = [ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, SportCar(context: context).entity, SportCar(context: context).entity, Person(context: context).entity] + let entities = [ + ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, + SportCar(context: context).entity, + SportCar(context: context).entity, Person(context: context).entity, + ] let ancestors = Set(entities).entitiesKeepingOnlyCommonAncestorEntities() XCTAssertTrue(!ancestors.isEmpty) XCTAssertTrue(ancestors.count == 2) @@ -228,7 +266,11 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { } do { - let entities = [ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, SportCar(context: context).entity, SportCar(context: context).entity, Person(context: context).entity, Car(context: context).entity] + let entities = [ + ExpensiveSportCar(context: context).entity, ExpensiveSportCar(context: context).entity, + SportCar(context: context).entity, + SportCar(context: context).entity, Person(context: context).entity, Car(context: context).entity, + ] let ancestors = Set(entities).entitiesKeepingOnlyCommonAncestorEntities() XCTAssertTrue(!ancestors.isEmpty) XCTAssertTrue(ancestors.count == 2) @@ -237,7 +279,7 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { } } - func testIsSubEntity() { + func test_IsSubEntity() { let context = container.viewContext let car = Car(context: context).entity let sportCar = SportCar(context: context).entity @@ -249,7 +291,7 @@ final class NSEntityDescriptionUtilsTests: InMemoryTestCase { XCTAssertTrue(sportCar.isDescendantEntity(of: car)) XCTAssertTrue(sportCar.isDescendantEntity(of: car, recursive: true)) - XCTAssertFalse(expensiveCar.isDescendantEntity(of: car)) // ExpensiveSportCar is a sub entity of SportCar + XCTAssertFalse(expensiveCar.isDescendantEntity(of: car)) // ExpensiveSportCar is a sub entity of SportCar XCTAssertTrue(expensiveCar.isDescendantEntity(of: car, recursive: true)) XCTAssertTrue(expensiveCar.isDescendantEntity(of: sportCar)) diff --git a/Tests/NSFetchRequestResultCoreDataTests.swift b/Tests/NSFetchRequestResultCoreData_Tests.swift similarity index 73% rename from Tests/NSFetchRequestResultCoreDataTests.swift rename to Tests/NSFetchRequestResultCoreData_Tests.swift index 65ee32c2..7c3c487f 100644 --- a/Tests/NSFetchRequestResultCoreDataTests.swift +++ b/Tests/NSFetchRequestResultCoreData_Tests.swift @@ -1,12 +1,13 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSFetchRequestResultCoreDataTests: InMemoryTestCase { +final class NSFetchRequestResultCoreData_Tests: InMemoryTestCase { - func testFetchObjectsIDs() throws { + func test_FetchObjectsIDs() throws { let context = container.viewContext // Given @@ -21,8 +22,10 @@ final class NSFetchRequestResultCoreDataTests: InMemoryTestCase { let ids = try Car.fetchObjectIDs(in: context, where: predicate) let idCar = try Car.fetchObjectIDs(in: context, includingSubentities: false, where: predicate) let idSportCar = try SportCar.fetchObjectIDs(in: context, includingSubentities: false, where: predicate) - let idExpensiveSportCar = try ExpensiveSportCar.fetchObjectIDs(in: context, includingSubentities: false, where: predicate) - let noIds = try Car.fetchObjectIDs(in: context, where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "not-existing-number-plage")) + let idExpensiveSportCar = try ExpensiveSportCar.fetchObjectIDs( + in: context, includingSubentities: false, where: predicate) + let noIds = try Car.fetchObjectIDs( + in: context, where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "not-existing-number-plage")) // Then XCTAssertEqual(ids.count, 2) @@ -33,19 +36,23 @@ final class NSFetchRequestResultCoreDataTests: InMemoryTestCase { XCTAssertTrue(context.registeredObjects.isEmpty) } - func testdeleteIncludingSubentities() { + func test_deleteIncludingSubentities() { let context = container.viewContext // Given context.fillWithSampleData() // When - XCTAssertNoThrow(try SportCar.delete(in: context, where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "302"), limit: 1000)) + XCTAssertNoThrow( + try SportCar.delete( + in: context, where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "302"), limit: 1000)) // Then XCTAssertTrue(try SportCar.fetchObjects(in: context).filter { $0.numberPlate == "302" }.isEmpty) XCTAssertTrue(try ExpensiveSportCar.fetchObjects(in: context).filter { $0.numberPlate == "302" }.isEmpty) // When - XCTAssertNoThrow(try ExpensiveSportCar.delete(in: context, where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "301"))) + XCTAssertNoThrow( + try ExpensiveSportCar.delete( + in: context, where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "301"))) // Then XCTAssertTrue(try SportCar.fetchObjects(in: context).filter { $0.numberPlate == "301" }.isEmpty) XCTAssertTrue(try ExpensiveSportCar.fetchObjects(in: context).filter { $0.numberPlate == "301" }.isEmpty) @@ -64,19 +71,25 @@ final class NSFetchRequestResultCoreDataTests: InMemoryTestCase { } - func testdeleteExcludingSubentities() { + func test_deleteExcludingSubentities() { let context = container.viewContext // Given context.fillWithSampleData() // When - XCTAssertNoThrow(try SportCar.delete(in: context, includingSubentities: false, where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "302"))) //it's an ExpensiveSportCar + XCTAssertNoThrow( + try SportCar.delete( + in: context, includingSubentities: false, + where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "302"))) //it's an ExpensiveSportCar // Then XCTAssertFalse(try SportCar.fetchObjects(in: context).filter { $0.numberPlate == "302" }.isEmpty) XCTAssertFalse(try ExpensiveSportCar.fetchObjects(in: context).filter { $0.numberPlate == "302" }.isEmpty) // When - XCTAssertNoThrow(try ExpensiveSportCar.delete(in: context, includingSubentities: false, where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "301"))) + XCTAssertNoThrow( + try ExpensiveSportCar.delete( + in: context, includingSubentities: false, + where: NSPredicate(format: "%K == %@", #keyPath(SportCar.numberPlate), "301"))) // Then XCTAssertTrue(try SportCar.fetchObjects(in: context).filter { $0.numberPlate == "301" }.isEmpty) XCTAssertTrue(try ExpensiveSportCar.fetchObjects(in: context).filter { $0.numberPlate == "301" }.isEmpty) @@ -94,13 +107,15 @@ final class NSFetchRequestResultCoreDataTests: InMemoryTestCase { } - func testdeleteExcludingExceptions() throws { + func test_deleteExcludingExceptions() throws { let context = container.viewContext context.fillWithSampleData() // Given let optonalCar = try Car.fetchObjects(in: context).filter { $0.numberPlate == "5" }.first - let optionalPerson = try Person.fetchObjects(in: context).filter { $0.firstName == "Theodora" && $0.lastName == "Stone" }.first + let optionalPerson = try Person.fetchObjects(in: context).filter { + $0.firstName == "Theodora" && $0.lastName == "Stone" + }.first let persons = try Person.fetchObjects(in: context).filter { $0.lastName == "Moreton" } // When @@ -123,7 +138,10 @@ final class NSFetchRequestResultCoreDataTests: InMemoryTestCase { var exceptions = persons exceptions.append(person) XCTAssertNoThrow(try Person.delete(in: context, except: exceptions)) - XCTAssertFalse(try Person.fetchObjects(in: context).filter { ($0.firstName == "Theodora" && $0.lastName == "Stone") || ($0.lastName == "Moreton") }.isEmpty) + XCTAssertFalse( + try Person.fetchObjects(in: context).filter { + ($0.firstName == "Theodora" && $0.lastName == "Stone") || ($0.lastName == "Moreton") + }.isEmpty) /// no exceptions XCTAssertNoThrow(try Person.delete(in: context, except: [])) @@ -131,13 +149,14 @@ final class NSFetchRequestResultCoreDataTests: InMemoryTestCase { } - func testdeleteWithSubentitiesExcludingExceptions() throws { + func test_deleteWithSubentitiesExcludingExceptions() throws { // Given let context = container.viewContext context.fillWithSampleData() // When, Then - let predicate = NSPredicate(format: "%K >= %@ && %K <= %@", #keyPath(Car.numberPlate), "202", #keyPath(Car.numberPlate), "204") + let predicate = NSPredicate( + format: "%K >= %@ && %K <= %@", #keyPath(Car.numberPlate), "202", #keyPath(Car.numberPlate), "204") let sportCars = try SportCar.fetchObjects(in: context, with: { $0.predicate = predicate }) XCTAssertNotNil(sportCars) XCTAssertNoThrow(try Car.delete(in: context, except: sportCars)) diff --git a/Tests/NSFetchRequestResultUtilsTests.swift b/Tests/NSFetchRequestResultUtils_Tests.swift similarity index 66% rename from Tests/NSFetchRequestResultUtilsTests.swift rename to Tests/NSFetchRequestResultUtils_Tests.swift index 11a26def..7ca32c90 100644 --- a/Tests/NSFetchRequestResultUtilsTests.swift +++ b/Tests/NSFetchRequestResultUtils_Tests.swift @@ -1,13 +1,14 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSFetchRequestResultUtilsTests: OnDiskTestCase { +final class NSFetchRequestResultUtils_Tests: OnDiskTestCase { // MARK: - Batch Faulting - func testFaultAndMaterializeObjectWithoutNSManagedObjectContext() throws { + func test_FaultAndMaterializeObjectWithoutNSManagedObjectContext() throws { let context = container.viewContext // Given @@ -27,7 +28,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertFalse(sportCar1.isFault) } - func testFaultAndMaterializeTemporaryObject() throws { + func test_FaultAndMaterializeTemporaryObject() throws { let context = container.viewContext // Given @@ -48,7 +49,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertFalse(sportCar1.isFault) } - func testBatchFaulting() throws { + func test_BatchFaulting() throws { // Given let context = container.viewContext @@ -63,7 +64,6 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { let request = Car.newFetchRequest() request.predicate = NSPredicate(value: true) - // When let cars = try context.fetch(request) @@ -83,7 +83,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { } - func testBatchFaultingEdgeCases() throws { + func test_BatchFaultingEdgeCases() throws { // Given let context = container.viewContext context.performAndWait { @@ -117,7 +117,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { } - func testBatchFaultingWithDifferentContexts() { + func test_BatchFaultingWithDifferentContexts() { // Given let context1 = container.viewContext let context2 = context1.newBackgroundContext(asChildContext: false) @@ -162,7 +162,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertTrue(objects.filter { !$0.isFault }.count == 4) } - func testBatchFaultingToManyRelationship() throws { + func test_BatchFaultingToManyRelationship() throws { let context = container.viewContext context.performAndWait { @@ -170,10 +170,11 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { try! context.save() } - context.refreshAllObjects() //re-fault objects that don't have pending changes + context.refreshAllObjects() //re-fault objects that don't have pending changes let request = Person.newFetchRequest() - request.predicate = NSPredicate(format: "\(#keyPath(Person.firstName)) == %@ AND \(#keyPath(Person.lastName)) == %@", "Theodora", "Stone") + request.predicate = NSPredicate( + format: "\(#keyPath(Person.firstName)) == %@ AND \(#keyPath(Person.lastName)) == %@", "Theodora", "Stone") let persons = try context.fetch(request) @@ -193,7 +194,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // MARK: - Fetch - func testFetch() throws { + func test_Fetch() throws { let context = container.viewContext context.performAndWait { @@ -219,7 +220,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // MARK: - Count - func testCount() throws { + func test_Count() throws { let context = container.viewContext context.performAndWait { @@ -245,7 +246,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // MARK: - Unique - func testfetchUniqueObject() throws { + func test_fetchUniqueObject() throws { let context = container.viewContext context.performAndWait { @@ -259,18 +260,22 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // } do { - let person = try Person.fetchUniqueObject(in: context, where: NSPredicate(format: "\(#keyPath(Person.lastName)) == %@", "MoretonXYZ")) + let person = try Person.fetchUniqueObject( + in: context, where: NSPredicate(format: "\(#keyPath(Person.lastName)) == %@", "MoretonXYZ")) XCTAssertNil(person) } do { - let person = try Person.fetchUniqueObject(in: context, where: NSPredicate(format: "\(#keyPath(Person.firstName)) == %@ AND \(#keyPath(Person.lastName)) == %@", "Theodora", "Stone")) + let person = try Person.fetchUniqueObject( + in: context, + where: NSPredicate( + format: "\(#keyPath(Person.firstName)) == %@ AND \(#keyPath(Person.lastName)) == %@", "Theodora", "Stone")) XCTAssertNotNil(person) } } - func testFindUniqueAfterPendingDelete() throws { + func test_FindUniqueAfterPendingDelete() throws { let context = container.viewContext // 1. a Lamborghini with plate 304 is saved @@ -293,7 +298,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertNotNil(car2) } - func testFindUniqueOrCreate() throws { + func test_FindUniqueOrCreate() throws { let context = container.viewContext context.performAndWait { context.fillWithSampleData() @@ -302,7 +307,9 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // Case 1: existing object do { - let car = try Car.findUniqueOrCreate(in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304")) { _ in + let car = try Car.findUniqueOrCreate( + in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304") + ) { _ in XCTFail("A new car shouldn't be created.") } @@ -312,11 +319,13 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // Case 2: new object do { - let car = try Car.findUniqueOrCreate(in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304-new"), with: { car in - XCTAssertNil(car.numberPlate) - car.numberPlate = "304-new" - car.model = "test" - }) + let car = try Car.findUniqueOrCreate( + in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304-new"), + with: { car in + XCTAssertNil(car.numberPlate) + car.numberPlate = "304-new" + car.model = "test" + }) XCTAssertNotNil(car) XCTAssertNil(car.maker) XCTAssertEqual(car.numberPlate, "304-new") @@ -349,7 +358,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // MARK: - First - func testfetchOneObject() throws { + func test_fetchOneObject() throws { let context = container.viewContext context.performAndWait { @@ -357,32 +366,38 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { try! context.save() } - let car1 = try Car.fetchOneObject(in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304")) + let car1 = try Car.fetchOneObject( + in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304")) XCTAssertNotNil(car1) car1?.numberPlate = "304-edited" - let car2 = try Car.fetchOneObject(in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304")) + let car2 = try Car.fetchOneObject( + in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304")) XCTAssertNil(car2) // the fetch will be run only against saved values (number plate "304-edited" is a pending change) - let car3 = try Car.fetchOneObject(in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304"), includesPendingChanges: false) + let car3 = try Car.fetchOneObject( + in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304"), + includesPendingChanges: false) XCTAssertNotNil(car3) - let newCar = Car(context: context) newCar.numberPlate = "304-fake" newCar.maker = "fake-maker" newCar.model = "fake-model" - let car4 = try Car.fetchOneObject(in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304-fake")) + let car4 = try Car.fetchOneObject( + in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304-fake")) XCTAssertNotNil(car4) - let car5 = try Car.fetchOneObject(in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304-fake"), includesPendingChanges: false) + let car5 = try Car.fetchOneObject( + in: context, where: NSPredicate(format: "\(#keyPath(Car.numberPlate)) == %@", "304-fake"), + includesPendingChanges: false) XCTAssertNil(car5) } - func testFindFirstMaterializedObject() throws { + func test_FindFirstMaterializedObject() throws { let context = container.viewContext context.performAndWait { @@ -410,7 +425,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // MARK: Materialized Object - func testfindMaterializedObjects() throws { + func test_findMaterializedObjects() throws { let context = container.viewContext context.performAndWait { @@ -426,7 +441,10 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertTrue(Car.materializedObjects(in: context, where: NSPredicate(value: true)).count > 1) // with the previous fetch we have materialized only one Lamborghini (the expensive one) - XCTAssertTrue(SportCar.materializedObjects(in: context, where: NSPredicate(format: "\(#keyPath(Car.maker)) == %@", "Lamborghini")).count == 1) + XCTAssertTrue( + SportCar.materializedObjects( + in: context, where: NSPredicate(format: "\(#keyPath(Car.maker)) == %@", "Lamborghini") + ).count == 1) // de-materialize all objects context.refreshAllObjects() @@ -436,7 +454,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // MARK: Batch Delete - func testbatchDeleteWithResultTypeStatusOnly() throws { + func test_batchDeleteWithResultTypeStatusOnly() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -448,10 +466,10 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertNotNil(result.status) XCTAssertTrue(result.status! == true) XCTAssertNil(result.deletes) - XCTAssertEqual(result.changes?[NSDeletedObjectsKey]?.count, nil) // wrong result type + XCTAssertEqual(result.changes?[NSDeletedObjectsKey]?.count, nil) // wrong result type } - func testbatchDeleteWithResultTypeCount() throws { + func test_batchDeleteWithResultTypeCount() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -462,16 +480,15 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertNotNil(result.count) XCTAssertTrue(result.count! > 1) - XCTAssertEqual(result.changes?[NSDeletedObjectsKey]?.count, nil) // wrong result type + XCTAssertEqual(result.changes?[NSDeletedObjectsKey]?.count, nil) // wrong result type } - func testbatchDeleteWithResultTypeObjectIDs() throws { + func test_batchDeleteWithResultTypeObjectIDs() throws { // Given let context = container.viewContext context.fillWithSampleData() try context.save() - let fiatPredicate = NSPredicate(format: "%K == %@", #keyPath(Car.maker), "FIAT") let fiatCount = try Car.count(in: context) { request in request.predicate = fiatPredicate } XCTAssertTrue(fiatCount > 0) @@ -486,14 +503,14 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertTrue(deletedValues > 1) XCTAssertTrue(result.changes?[NSDeletedObjectsKey]?.count ?? 0 > 1) - // the var `changes` is usefull when used while merging changes + // the var `changes` is usefull when used while merging changes NSManagedObjectContext.mergeChanges(fromRemoteContextSave: result.changes!, into: [context]) let fiatCountAfterMerge = try Car.count(in: context) { request in request.predicate = fiatPredicate } XCTAssertEqual(fiatCountAfterMerge, 0) } - func testBatchDeleteEntities() throws { + func test_BatchDeleteEntities() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -510,13 +527,13 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertEqual(expensiveSportCarCount, 0) } - func testBatchDeleteEntitiesExcludingSubentities() throws { + func test_BatchDeleteEntitiesExcludingSubentities() throws { // Given let context = container.viewContext context.fillWithSampleData() try context.save() - let preDeleteSportCarCount = try SportCar.count(in: context) // This count include all the subentities + let preDeleteSportCarCount = try SportCar.count(in: context) // This count include all the subentities let preDeleteExpensiveSportCarCount = try ExpensiveSportCar.count(in: context) let expectedRemovedCarCount = preDeleteSportCarCount - preDeleteExpensiveSportCarCount let expectedRemainingSportCartCount = preDeleteSportCarCount - expectedRemovedCarCount @@ -532,67 +549,72 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertEqual(expensiveSportCarCount, preDeleteExpensiveSportCarCount) } - func testbatchDeleteMarkedForDeletion() throws { + func test_batchDeleteMarkedForDeletion() throws { // Given let context = container.viewContext context.fillWithSampleData() try context.save() let kias = [9, 10, 11] - let kiasCars = try Car.fetchObjects(in: context, with: { $0.predicate = NSPredicate(format: "%K IN %@", #keyPath(Car.numberPlate), kias) }) + let kiasCars = try Car.fetchObjects( + in: context, with: { $0.predicate = NSPredicate(format: "%K IN %@", #keyPath(Car.numberPlate), kias) }) XCTAssertEqual(kiasCars.count, 3) - kiasCars.forEach { car in + for car in kiasCars { car.markForDelayedDeletion() } try context.save() - let result = try Car.batchDeleteMarkedForDeletion(with: context, olderThan: Date(), resultType: .resultTypeStatusOnly) + let result = try Car.batchDeleteMarkedForDeletion( + with: context, olderThan: Date(), resultType: .resultTypeStatusOnly) XCTAssertTrue(result.status!) try context.save() - let kiasCars2 = try Car.fetchObjects(in: context, with: { $0.predicate = NSPredicate(format: "%K IN %@", #keyPath(Car.numberPlate), kias) }) + let kiasCars2 = try Car.fetchObjects( + in: context, with: { $0.predicate = NSPredicate(format: "%K IN %@", #keyPath(Car.numberPlate), kias) }) XCTAssertEqual(kiasCars2.count, 0) } // MARK: Batch Insert - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - func testbatchInsertWithResultTypeStatusOnly() throws { + func test_batchInsertWithResultTypeStatusOnly() throws { // Given let context = container.viewContext - let object = [#keyPath(Car.maker): "FIAT", - #keyPath(Car.numberPlate): "123", - #keyPath(Car.model): "Panda"] + let object = [ + #keyPath(Car.maker): "FIAT", + #keyPath(Car.numberPlate): "123", + #keyPath(Car.model): "Panda", + ] - let result = try! Car.batchInsert(using: context, - resultType: .statusOnly, - objects: [object]) + let result = try! Car.batchInsert( + using: context, + resultType: .statusOnly, + objects: [object]) XCTAssertTrue(result.status!) - XCTAssertEqual(result.changes?[NSInsertedObjectsKey]?.count, nil) // wrong result type + XCTAssertEqual(result.changes?[NSInsertedObjectsKey]?.count, nil) // wrong result type } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - func testbatchInsertWithResultTypeCount() throws { + func test_batchInsertWithResultTypeCount() throws { // Given let context = container.viewContext - let object = [#keyPath(Car.maker): "FIAT", - #keyPath(Car.numberPlate): "123", - #keyPath(Car.model): "Panda"] + let object = [ + #keyPath(Car.maker): "FIAT", + #keyPath(Car.numberPlate): "123", + #keyPath(Car.model): "Panda", + ] - let result = try! Car.batchInsert(using: context, - resultType: .count, - objects: [object]) + let result = try! Car.batchInsert( + using: context, + resultType: .count, + objects: [object]) XCTAssertEqual(result.count!, 1) - XCTAssertEqual(result.changes?[NSInsertedObjectsKey]?.count, nil) // wrong result type + XCTAssertEqual(result.changes?[NSInsertedObjectsKey]?.count, nil) // wrong result type } - func testbatchInsertWithResultObjectIDs() throws { - guard #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) else { return } - + func test_batchInsertWithResultObjectIDs() throws { // Given let context = container.viewContext let numberPlate = UUID().uuidString @@ -600,22 +622,28 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { let objects = [ // This object will be inserted without a maker - ["WRONG_KEY": "FIAT", - #keyPath(Car.numberPlate): numberPlate, - #keyPath(Car.model): "Panda"], + [ + "WRONG_KEY": "FIAT", + #keyPath(Car.numberPlate): numberPlate, + #keyPath(Car.model): "Panda", + ], // This object will not be inserted because the number plate has already been inserted - [#keyPath(Car.maker): "FIAT", - #keyPath(Car.numberPlate): numberPlate, - #keyPath(Car.model): "Panda*"], - - [#keyPath(Car.maker): "FIAT", - #keyPath(Car.numberPlate): numberPlate2, - #keyPath(Car.model): "Panda"] + [ + #keyPath(Car.maker): "FIAT", + #keyPath(Car.numberPlate): numberPlate, + #keyPath(Car.model): "Panda*", + ], + + [ + #keyPath(Car.maker): "FIAT", + #keyPath(Car.numberPlate): numberPlate2, + #keyPath(Car.model): "Panda", + ], ] if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) { - // on iOS 14, throws an error (NSValidationErrorKey) + // on iOS 14 or greater, throws an error (NSValidationErrorKey) XCTAssertThrowsError(try Car.batchInsert(using: context, resultType: .objectIDs, objects: objects)) } else { // on iOS 13, it doesn't throw @@ -633,23 +661,22 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { } } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - func testFailedbatchInsertWithResultObjectIDs() throws { + func test_FailedbatchInsertWithResultObjectIDs() throws { // Given let context = container.viewContext let objects = [ - [#keyPath(Car.maker): "FIAT", - "WRONG_REQUIRED_KEY": "1234", - #keyPath(Car.model): "Panda"], + [ + #keyPath(Car.maker): "FIAT", + "WRONG_REQUIRED_KEY": "1234", + #keyPath(Car.model): "Panda", + ] ] XCTAssertThrowsError(try Car.batchInsert(using: context, resultType: .objectIDs, objects: objects)) } - func testbatchInsertWithDictionaryHandler() throws { - guard #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) else { return } - + func test_batchInsertWithDictionaryHandler() throws { // Given let context = container.viewContext @@ -664,25 +691,25 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // Provide one dictionary at a time when the block is called. var index = 0 let total = dictionaries.count - let result = try Car.batchInsert(using: context, dictionaryHandler: { dictionary -> Bool in - guard index < total else { return true } - dictionary.addEntries(from: dictionaries[index]) - index += 1 - return false - }) + let result = try Car.batchInsert( + using: context, + dictionaryHandler: { dictionary -> Bool in + guard index < total else { return true } + dictionary.addEntries(from: dictionaries[index]) + index += 1 + return false + }) let insertResult = try XCTUnwrap(result) XCTAssertEqual(index, total) XCTAssertTrue(insertResult.status!) - XCTAssertEqual(insertResult.changes?[NSInsertedObjectsKey]?.count, nil) // wrong result type + XCTAssertEqual(insertResult.changes?[NSInsertedObjectsKey]?.count, nil) // wrong result type let count = try Car.count(in: context) XCTAssertEqual(count, total) } - func testBatchInserWithObjectHandler() throws { - guard #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) else { return } - + func test_BatchInserWithObjectHandler() throws { // Given let context = container.viewContext @@ -697,29 +724,85 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // Provide one object at a time when the block is called. var index = 0 let total = dictionaries.count - let result = try ExpensiveSportCar.batchInsert(using: context, managedObjectHandler: { car -> Bool in - guard index < total else { return true } - let dictionary = dictionaries[index] - car.maker = dictionary[#keyPath(Car.maker)] as? String - car.numberPlate = dictionary[#keyPath(Car.numberPlate)] as? String - car.model = dictionary[#keyPath(Car.model)] as? String - car.isLimitedEdition = Bool.random() - index += 1 - return false - }) + let result = try ExpensiveSportCar.batchInsert( + using: context, + managedObjectHandler: { car -> Bool in + guard index < total else { return true } + let dictionary = dictionaries[index] + car.maker = dictionary[#keyPath(Car.maker)] as? String + car.numberPlate = dictionary[#keyPath(Car.numberPlate)] as? String + car.model = dictionary[#keyPath(Car.model)] as? String + car.isLimitedEdition = Bool.random() + index += 1 + return false + }) let insertResult = try XCTUnwrap(result) XCTAssertEqual(index, total) XCTAssertTrue(insertResult.status!) - XCTAssertEqual(insertResult.changes?[NSInsertedObjectsKey]?.count, nil) // wrong result type + XCTAssertEqual(insertResult.changes?[NSInsertedObjectsKey]?.count, nil) // wrong result type let count = try Car.count(in: context) XCTAssertEqual(count, total) } + + @MainActor + func test_BatchInserWithNSFetchedResultController() throws { + // How to make FRC aware of batch insertions + + let context = container.viewContext + context.automaticallyMergesChangesFromParent = true + let backgroundContext = container.newBackgroundContext() + + let request = Car.fetchRequest() + request.addSortDescriptors([]) + let delegate = FetchedResultsControllerMockDelegate() + let frc = NSFetchedResultsController(fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil) + frc.delegate = delegate + try frc.performFetch() + + XCTAssertTrue((frc.fetchedObjects ?? []).isEmpty) + + let objects = [ + [ + #keyPath(Car.maker): "FIAT", + #keyPath(Car.numberPlate): "1", + #keyPath(Car.model): "500", + ], + [ + #keyPath(Car.maker): "FIAT", + #keyPath(Car.numberPlate): "2", + #keyPath(Car.model): "600", + ], + [ + #keyPath(Car.maker): "FIAT", + #keyPath(Car.numberPlate): "3", + #keyPath(Car.model): "Panda", + ], + ] + + try backgroundContext.performAndWait { + let result: NSBatchInsertResult = try Car.batchInsert(using: $0, + resultType: .objectIDs, + objects: objects) + try $0.save() // not needed because batch insertions are done directly in the store + XCTAssertEqual(delegate.insertedObjects.count, 0) // FRC is not aware yet + + let changes = try XCTUnwrap(result.changes) + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) // make FRC aware of inserts + } + + XCTAssertEqual(delegate.insertedObjects.count, 3) + XCTAssertEqual(delegate.deletedObjects.count, 0) + XCTAssertEqual(delegate.updatedObjects.count, 0) + } // MARK: Batch Update - func testbatchUpdateWithResultTypeStatusOnly() throws { + func test_batchUpdateWithResultTypeStatusOnly() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -743,10 +826,10 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertEqual(newFiatCount, 0) let newFCACount = try Car.count(in: context) { $0.predicate = fcaPredicate } XCTAssertEqual(newFCACount, fiatCount) - XCTAssertEqual(result.changes?[NSUpdatedObjectsKey]?.count, nil) // wrong result type + XCTAssertEqual(result.changes?[NSUpdatedObjectsKey]?.count, nil) // wrong result type } - func testbatchUpdateWithResultCountType() throws { + func test_batchUpdateWithResultCountType() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -772,10 +855,10 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { XCTAssertEqual(newFiatCount, 0) let newFCACount = try Car.count(in: context) { $0.predicate = fcaPredicate } XCTAssertEqual(newFCACount, fiatCount) - XCTAssertEqual(result.changes?[NSUpdatedObjectsKey]?.count, nil) // wrong result type + XCTAssertEqual(result.changes?[NSUpdatedObjectsKey]?.count, nil) // wrong result type } - func testbatchUpdateWithResultObjectIDsType() throws { + func test_batchUpdateWithResultObjectIDsType() throws { // Given let context = container.viewContext context.fillWithSampleData() @@ -790,7 +873,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { let result = try Car.batchUpdate(using: context) { $0.resultType = .updatedObjectIDsResultType - $0.propertiesToUpdate = [#keyPath(Car.maker): "FCA"] + $0.propertiesToUpdate = [#keyPath(Car.maker): "FCA"] $0.includesSubentities = true $0.predicate = fiatPredicate } @@ -807,66 +890,65 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { } // TODO: investigate this behaviour (and open a FB) -// func testFailedBatchUpdateWithResultObjectIDs() throws { -// guard #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) else { return } -// -// // Given -// let context = container.viewContext -// context.fillWithSampleData() -// try context.save() -// -// // numberPlate is not optional constraint for the Car Entity -// // but in the backing .sqlite file it's actually just an optional string -// -// XCTAssertThrowsError(try Car.batchUpdate(using: context) { -// $0.resultType = .statusOnlyResultType -// $0.propertiesToUpdate = [#keyPath(Car.numberPlate): NSExpression(forConstantValue: "SAME_VALE")] -// $0.includesSubentities = true -// }) -// -// let cars = try Car.fetchObjects(in: context) -// XCTAssertEqual(cars.count, 125) // Nothing changed here -// -// // While setting the same value for a constraint property throws an exception, setting nil doesn't throw anything but then -// // all the updated objects are "broken" -// let result2 = try Car.batchUpdate(using: context) { -// $0.resultType = .statusOnlyResultType -// $0.propertiesToUpdate = [#keyPath(Car.numberPlate): NSExpression(forConstantValue: nil)] -// $0.includesSubentities = true -// } -// XCTAssertNotNil(result2.status, "There should be a status for a batch update with statusOnlyResultType option.") -// XCTAssertTrue(result2.status!) -// -// // Row (pk = 1) for entity 'Car' is missing mandatory text data for property 'numberPlate' -// do { -// let car = try Car.fetchOneObject(in: context, where: NSPredicate(value: true)) -// XCTAssertNotNil(car) // since the object is broken the fetch returns nil -// } -// -// try context.save() -// try Car.delete(in: context) -// try context.save() -// let cars2 = try! Car.fetchObjects(in: context) -// XCTAssertTrue(cars2.isEmpty) // All the cars aren't valid (for the CoreData model) anymore -// -// do { -// let car = try Car.fetchOneObject(in: context, where: NSPredicate(value: true)) -// XCTAssertNotNil(car) // since the object is broken the fetch returns nil -// } -// -// let count = try Car.count(in: context) -// XCTAssertEqual(count, 125) -// -// try Car.batchDelete(using: context, predicate: NSPredicate(format: "%K == NULL", #keyPath(Car.numberPlate)), resultType: .resultTypeCount) -// let count2 = try Car.count(in: context) -// -// XCTAssertEqual(count2, 0) -// // https://developer.apple.com/library/archive/featuredarticles/CoreData_Batch_Guide/BatchUpdates/BatchUpdates.html -// } + // func test_FailedBatchUpdateWithResultObjectIDs() throws { + // // Given + // let context = container.viewContext + // context.fillWithSampleData() + // try context.save() + // + // // numberPlate is not optional constraint for the Car Entity + // // but in the backing .sqlite file it's actually just an optional string + // + // XCTAssertThrowsError(try Car.batchUpdate(using: context) { + // $0.resultType = .statusOnlyResultType + // $0.propertiesToUpdate = [#keyPath(Car.numberPlate): NSExpression(forConstantValue: "SAME_VALE")] + // $0.includesSubentities = true + // }) + // + // let cars = try Car.fetchObjects(in: context) + // XCTAssertEqual(cars.count, 125) // Nothing changed here + // + // // While setting the same value for a constraint property throws an exception, setting nil doesn't throw anything but then + // // all the updated objects are "broken" + // let result2 = try Car.batchUpdate(using: context) { + // $0.resultType = .statusOnlyResultType + // $0.propertiesToUpdate = [#keyPath(Car.numberPlate): NSExpression(forConstantValue: nil)] + // $0.includesSubentities = true + // } + // XCTAssertNotNil(result2.status, "There should be a status for a batch update with statusOnlyResultType option.") + // XCTAssertTrue(result2.status!) + // + // // Row (pk = 1) for entity 'Car' is missing mandatory text data for property 'numberPlate' + // do { + // let car = try Car.fetchOneObject(in: context, where: NSPredicate(value: true)) + // XCTAssertNotNil(car) // since the object is broken the fetch returns nil + // } + // + // try context.save() + // try Car.delete(in: context) + // try context.save() + // let cars2 = try! Car.fetchObjects(in: context) + // XCTAssertTrue(cars2.isEmpty) // All the cars aren't valid (for the CoreData model) anymore + // + // do { + // let car = try Car.fetchOneObject(in: context, where: NSPredicate(value: true)) + // XCTAssertNotNil(car) // since the object is broken the fetch returns nil + // } + // + // let count = try Car.count(in: context) + // XCTAssertEqual(count, 125) + // + // try Car.batchDelete(using: context, predicate: NSPredicate(format: "%K == NULL", #keyPath(Car.numberPlate)), resultType: .resultTypeCount) + // let count2 = try Car.count(in: context) + // + // XCTAssertEqual(count2, 0) + // // https://developer.apple.com/library/archive/featuredarticles/CoreData_Batch_Guide/BatchUpdates/BatchUpdates.html + // } // MARK: - Async Fetch - func testAsyncFetch() throws { + @MainActor + func test_AsyncFetch() throws { // BUG: Async fetches can't be tested with the ConcurrencyDebug enabled, // https://stackoverflow.com/questions/31728425/coredata-asynchronous-fetch-causes-concurrency-debugger-error try XCTSkipIf(UserDefaults.standard.integer(forKey: "com.apple.CoreData.ConcurrencyDebug") == 1) @@ -875,7 +957,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { let expectation2 = self.expectation(description: "\(#function)\(#line)") let mainContext = container.viewContext - (1...10_000).forEach { (i) in + for i in 1...10_000 { let car = Car(context: mainContext) car.numberPlate = "test\(i)" } @@ -884,7 +966,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { let currentProgress = Progress(totalUnitCount: 1) currentProgress.becomeCurrent(withPendingUnitCount: 1) - let token = try Car.fetchObjects(in: mainContext, with: { $0.predicate = NSPredicate(value: true) } ) { result in + let token = try Car.fetchObjects(in: mainContext, with: { $0.predicate = NSPredicate(value: true) }) { result in switch result { case .success(let cars): XCTAssertEqual(cars.count, 10_000) @@ -908,39 +990,39 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { currentToken?.invalidate() } -// @available(swift 5.5) -// @available(iOS 15.0, iOSApplicationExtension 15.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, macOS 12, *) -// func testAsyncFetchUsingSwiftConcurrency() async throws { -// // In testAsyncFetch() the standard implementation doesn't pass the test only if we enable ConcurrencyDebug. -// // The async/await version (that is, btw, used in WWDC 2021 videos on how to use continuations) always fails due to data races. -// // https://stackoverflow.com/questions/31728425/coredata-asynchronous-fetch-causes-concurrency-debugger-error -// try XCTSkipIf(UserDefaults.standard.integer(forKey: "com.apple.CoreData.ConcurrencyDebug") == 1) -// let mainContext = container.viewContext -// -// try await mainContext.perform { -// (1...10_000).forEach { -// let car = Car(context: mainContext) -// car.numberPlate = "test\($0)" -// } -// try mainContext.save() -// } -// let results = try await Car.fetchObjects(in: mainContext) { $0.predicate = .true } -// XCTAssertEqual(results.count, 10_000) -// } + // func test_AsyncFetchUsingSwiftConcurrency() async throws { + // // In test_AsyncFetch() the standard implementation doesn't pass the test only if we enable ConcurrencyDebug. + // // The async/await version (that is, btw, used in WWDC 2021 videos on how to use continuations) always fails due to data races. + // // https://stackoverflow.com/questions/31728425/coredata-asynchronous-fetch-causes-concurrency-debugger-error + // try XCTSkipIf(UserDefaults.standard.integer(forKey: "com.apple.CoreData.ConcurrencyDebug") == 1) + // let mainContext = container.viewContext + // + // // https://forums.developer.apple.com/forums/thread/741461 + // + // (1...10_000).forEach { + // let car = Car(context: mainContext) + // car.numberPlate = "test\($0)" + // } + // try mainContext.save() + // + // let results = try await Car.fetchObjects(in: mainContext) { $0.predicate = .true } + // XCTAssertEqual(results.count, 10_000) + // } // MARK: - Subquery - func testInvestigationSubQuery() throws { + func test_InvestigationSubQuery() throws { // https://medium.com/@Czajnikowski/subquery-is-not-that-scary-3f95cb9e2d98 let context = container.viewContext context.fillWithSampleData() let people = try Person.fetchObjects(in: context) let results = people.filter { person in - let count = person.cars?.filter { object in - let car = object as! Car - return car.maker == "FIAT" - }.count ?? 0 + let count = + person.cars?.filter { object in + let car = object as! Car + return car.maker == "FIAT" + }.count ?? 0 //print("\t\(person.firstName) \(person.lastName) has \(count) FIAT cars") return count > 2 } @@ -953,7 +1035,7 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // MARK: - Group By - func testInvestigationGroupBy() throws { + func test_InvestigationGroupBy() throws { // https://developer.apple.com/documentation/coredata/nsfetchrequest/1506191-propertiestogroupby // https://gist.github.com/pronebird/cca9777af004e9c91f9cd36c23cc821c // http://www.cimgf.com/2015/06/25/core-data-and-aggregate-fetches-in-swift/ @@ -972,9 +1054,10 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { // describes a column to be returned from a fetch that may not appear directly as an attribute or relationship on an entity let countExpressionDescription = NSExpressionDescription() - countExpressionDescription.name = "count" // alias + countExpressionDescription.name = "count" // alias countExpressionDescription.expression = expression - countExpressionDescription.expressionResultType = .integer64AttributeType // in iOS 15 use resultType: NSAttributeDescription.AttributeType + // in iOS 15 use resultType: NSAttributeDescription.AttributeType + countExpressionDescription.expressionResultType = .integer64AttributeType let request = Car.fetchRequest() request.returnsObjectsAsFaults = false @@ -982,9 +1065,8 @@ final class NSFetchRequestResultUtilsTests: OnDiskTestCase { request.propertiesToFetch = [#keyPath(Car.maker), countExpressionDescription] request.resultType = .dictionaryResultType request.havingPredicate = NSPredicate(format: "%@ > 100", NSExpression(forVariable: "count")) - let results = try context.fetch(request) as! [Dictionary] + let results = try context.fetch(request) as! [[String: Any]] XCTAssertEqual(results.count, 1) } } - diff --git a/Tests/NSFetchRequestUtilsTests.swift b/Tests/NSFetchRequestUtils_Tests.swift similarity index 95% rename from Tests/NSFetchRequestUtilsTests.swift rename to Tests/NSFetchRequestUtils_Tests.swift index 90624c0b..acab5596 100644 --- a/Tests/NSFetchRequestUtilsTests.swift +++ b/Tests/NSFetchRequestUtils_Tests.swift @@ -1,11 +1,12 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSFetchRequestUtilsTests: XCTestCase { - func testInit() { +final class NSFetchRequestUtils_Tests: XCTestCase { + func test_Init() { let fakeEntity = NSEntityDescription() do { // Given, When diff --git a/Tests/NSManagedObjectContextHistoryTests.swift b/Tests/NSManagedObjectContextHistory_Tests.swift similarity index 82% rename from Tests/NSManagedObjectContextHistoryTests.swift rename to Tests/NSManagedObjectContextHistory_Tests.swift index 70b9812d..97bf4b73 100644 --- a/Tests/NSManagedObjectContextHistoryTests.swift +++ b/Tests/NSManagedObjectContextHistory_Tests.swift @@ -1,13 +1,13 @@ // CoreDataPlus -import XCTest import CoreData -@testable import CoreDataPlus +import XCTest -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -final class NSManagedObjectContextHistoryTests: BaseTestCase { +@testable import CoreDataPlus - func testMergeHistoryAfterDate() throws { +final class NSManagedObjectContextHistory_Tests: BaseTestCase { + @MainActor + func test_MergeHistoryAfterDate() throws { // Given let id = UUID() let container1 = OnDiskPersistentContainer.makeNew(id: id) @@ -27,12 +27,15 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { XCTAssertTrue(viewContext2.registeredObjects.isEmpty) // When, Then - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: viewContext2) - .sink { _ in - expectation1.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: viewContext2 + ) + .sink { _ in + expectation1.fulfill() + } - let transactionsFromDistantPast = try viewContext2.historyTransactions(using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) + let transactionsFromDistantPast = try viewContext2.historyTransactions( + using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) XCTAssertEqual(transactionsFromDistantPast.count, 1) let result = try viewContext2.mergeTransactions(transactionsFromDistantPast) @@ -46,19 +49,20 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { // cleaning avoiding SQLITE warnings let psc1 = viewContext1.persistentStoreCoordinator! - try psc1.persistentStores.forEach { store in + for store in psc1.persistentStores { try psc1.remove(store) } let psc2 = viewContext2.persistentStoreCoordinator! - try psc2.persistentStores.forEach { store in + for store in psc2.persistentStores { try psc2.remove(store) } try container1.destroy() } - func testMergeHistoryAfterDateWithMultipleTransactions() throws { + @MainActor + func test_MergeHistoryAfterDateWithMultipleTransactions() throws { // Given let id = UUID() let container1 = OnDiskPersistentContainer.makeNew(id: id) @@ -105,38 +109,42 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { person5.firstName = "Juliana" person5.lastName = "Pyke" - try viewContext1.save() // 3 inserts + try viewContext1.save() // 3 inserts person1.firstName = person1.firstName + "*" - try viewContext1.save() // 1 update + try viewContext1.save() // 1 update person2.delete() - try viewContext1.save() // 1 delete + try viewContext1.save() // 1 delete - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: viewContext2) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertTrue(payload.managedObjectContext === viewContext2) - if !payload.insertedObjects.isEmpty { - expectation1.fulfill() - } else if !payload.updatedObjects.isEmpty || !payload.refreshedObjects.isEmpty { - expectation2.fulfill() - } else if !payload.deletedObjects.isEmpty { - expectation3.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: viewContext2 + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertTrue(payload.managedObjectContext === viewContext2) + if !payload.insertedObjects.isEmpty { + expectation1.fulfill() + } else if !payload.updatedObjects.isEmpty || !payload.refreshedObjects.isEmpty { + expectation2.fulfill() + } else if !payload.deletedObjects.isEmpty { + expectation3.fulfill() } + } - let transactionsFromDistantPast = try viewContext2.historyTransactions(using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) + let transactionsFromDistantPast = try viewContext2.historyTransactions( + using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) XCTAssertEqual(transactionsFromDistantPast.count, 3) let result = try viewContext2.mergeTransactions(transactionsFromDistantPast) XCTAssertNotNil(result) + waitForExpectations(timeout: 5, handler: nil) try viewContext2.save() - print(viewContext2.insertedObjects) + //print(viewContext2.insertedObjects) cancellable.cancel() let status = try viewContext2.deleteHistory(before: result!.0) @@ -144,19 +152,19 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { // cleaning avoiding SQLITE warnings let psc1 = viewContext1.persistentStoreCoordinator! - try psc1.persistentStores.forEach { store in + for store in psc1.persistentStores { try psc1.remove(store) } let psc2 = viewContext2.persistentStoreCoordinator! - try psc2.persistentStores.forEach { store in + for store in psc2.persistentStores { try psc2.remove(store) } try container1.destroy() } - func testMergeHistoryAfterNilTokenWithoutAnyHistoryChanges() throws { + func test_MergeHistoryAfterNilTokenWithoutAnyHistoryChanges() throws { let container1 = OnDiskPersistentContainer.makeNew() let stores = container1.persistentStoreCoordinator.persistentStores XCTAssertEqual(stores.count, 1) @@ -165,11 +173,13 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { // it's a new store, there shouldn't be any transactions let requestToken: NSPersistentHistoryToken? = nil - let transactions = try container1.viewContext.historyTransactions(using: NSPersistentHistoryChangeRequest.fetchHistory(after: requestToken)) + let transactions = try container1.viewContext.historyTransactions( + using: NSPersistentHistoryChangeRequest.fetchHistory(after: requestToken)) XCTAssertTrue(transactions.isEmpty) XCTAssertNotNil(currentToken) - let transactionsAfterCurrentToken = try container1.viewContext.historyTransactions(using: NSPersistentHistoryChangeRequest.fetchHistory(after: currentToken)) + let transactionsAfterCurrentToken = try container1.viewContext.historyTransactions( + using: NSPersistentHistoryChangeRequest.fetchHistory(after: currentToken)) let result = try container1.viewContext.mergeTransactions(transactionsAfterCurrentToken) XCTAssertNil(result) @@ -179,11 +189,13 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { try container1.destroy() } - func testPersistentStoreWithHistoryTrackingEnabledGeneratesHistoryTokens() throws { + @MainActor + func test_PersistentStoreWithHistoryTrackingEnabledGeneratesHistoryTokens() throws { // Given - let psc = NSPersistentStoreCoordinator(managedObjectModel: model) + let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) let storeURL = URL.newDatabaseURL(withID: UUID()) - let options: PersistentStoreOptions = [NSPersistentHistoryTrackingKey: true as NSNumber] // enable History Tracking + // enable History Tracking + let options: PersistentStoreOptions = [NSPersistentHistoryTrackingKey: true as NSNumber] try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) let expectation1 = expectation(description: "\(#function)\(#line)") let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) @@ -209,15 +221,16 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { XCTAssertTrue(result) // cleaning avoiding SQLITE warnings - try psc.persistentStores.forEach { - try psc.remove($0) + for store in psc.persistentStores { + try psc.remove(store) } try NSPersistentStoreCoordinator.destroyStore(at: storeURL) } - func testPersistentStoreWithHistoryTrackingDisabledDoesntGenerateHistoryTokens() throws { + @MainActor + func test_PersistentStoreWithHistoryTrackingDisabledDoesntGenerateHistoryTokens() throws { // Given - let psc = NSPersistentStoreCoordinator(managedObjectModel: model) + let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) let storeURL = URL.newDatabaseURL(withID: UUID()) try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil) let expectation1 = expectation(description: "\(#function)\(#line)") @@ -228,7 +241,10 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: context) .map { ManagedObjectContextDidSaveObjects(notification: $0) } .sink { payload in - XCTAssertNil(payload.historyToken, "The Persistent Store Coordinator doesn't have the NSPersistentStoreRemoteChangeNotificationPostOptionKey option enabled.") + XCTAssertNil( + payload.historyToken, + "The Persistent Store Coordinator doesn't have the NSPersistentStoreRemoteChangeNotificationPostOptionKey option enabled." + ) expectation1.fulfill() } @@ -241,13 +257,13 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { cancellable.cancel() // cleaning avoiding SQLITE warnings - try psc.persistentStores.forEach { - try psc.remove($0) + for store in psc.persistentStores { + try psc.remove(store) } try NSPersistentStoreCoordinator.destroyStore(at: storeURL) } - func testDeleteHistoryAfterTransaction() throws { + func test_DeleteHistoryAfterTransaction() throws { let container = OnDiskPersistentContainer.makeNew() let viewContext = container.viewContext @@ -267,25 +283,28 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { try viewContext.save() - let transactions1 = try viewContext.historyTransactions(using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) + let transactions1 = try viewContext.historyTransactions( + using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) XCTAssertEqual(transactions1.count, 2) let firstTransaction1 = try XCTUnwrap(transactions1.first) // Removes all the transactions before the first one: no transactions are actually deleted let result1 = try XCTUnwrap(try viewContext.deleteHistory(before: firstTransaction1)) XCTAssertTrue(result1) - let transactions2 = try viewContext.historyTransactions(using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) + let transactions2 = try viewContext.historyTransactions( + using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) XCTAssertEqual(transactions2.count, 2) let lastTransaction2 = try XCTUnwrap(transactions2.last) // Removes all the transactions before the last one: 1 transaction gets deleted let result2 = try XCTUnwrap(try viewContext.deleteHistory(before: lastTransaction2)) XCTAssertTrue(result2) - let transactions3 = try viewContext.historyTransactions(using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) + let transactions3 = try viewContext.historyTransactions( + using: NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)) XCTAssertEqual(transactions3.count, 1) } - func testFetchHistoryChangesUsingFetchRequest() throws { + func test_FetchHistoryChangesUsingFetchRequest() throws { // Given let id = UUID() let container1 = OnDiskPersistentContainer.makeNew(id: id) @@ -321,7 +340,8 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { } do { - let predicate = NSCompoundPredicate(type: .and, subpredicates: [tokenGreaterThanLastHistoryTokenPredicate, notAuthor1Predicate]) + let predicate = NSCompoundPredicate( + type: .and, subpredicates: [tokenGreaterThanLastHistoryTokenPredicate, notAuthor1Predicate]) let request = try makeTransactionFetchRequest(predicate: predicate) let allTransactions = try viewContext2.historyTransactions(using: request) XCTAssertTrue(allTransactions.isEmpty) @@ -342,7 +362,8 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { } do { - let predicate = NSCompoundPredicate(type: .and, subpredicates: [tokenGreaterThanLastHistoryTokenPredicate, notAuthor2Predicate]) + let predicate = NSCompoundPredicate( + type: .and, subpredicates: [tokenGreaterThanLastHistoryTokenPredicate, notAuthor2Predicate]) let request = try makeTransactionFetchRequest(predicate: predicate) let allTransactions = try viewContext2.historyTransactions(using: request) XCTAssertFalse(allTransactions.isEmpty) @@ -353,18 +374,18 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { // cleaning avoiding SQLITE warnings let psc1 = viewContext1.persistentStoreCoordinator! - try psc1.persistentStores.forEach { store in + for store in psc1.persistentStores { try psc1.remove(store) } - try psc2.persistentStores.forEach { store in + for store in psc2.persistentStores { try psc2.remove(store) } try container1.destroy() } - func testInvestigationHistoryFetches() throws { + func test_InvestigationHistoryFetches() throws { // Given let id = UUID() let container1 = OnDiskPersistentContainer.makeNew(id: id) @@ -447,10 +468,11 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { let transactionRequest = NSPersistentHistoryChangeRequest.fetchHistory(withFetch: request) transactionRequest.resultType = .transactionsOnly - let transactions = try viewContext2.performAndWait { _ ->[NSPersistentHistoryTransaction] in + let transactions = try viewContext2.performAndWait { _ -> [NSPersistentHistoryTransaction] in // swiftlint:disable force_cast let history = try viewContext2.execute(transactionRequest) as! NSPersistentHistoryResult - let transactions = history.result as! [NSPersistentHistoryTransaction] // ordered from the oldest to the most recent + // ordered from the oldest to the most recent + let transactions = history.result as! [NSPersistentHistoryTransaction] // swiftlint:enable force_cast return transactions } @@ -458,7 +480,7 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { XCTAssertEqual(transactions.count, 1) let first = try XCTUnwrap(transactions.first) XCTAssertEqual(first.token, tokens.last) - XCTAssertNil(first.changes) // result type is transactionsOnly + XCTAssertNil(first.changes) // result type is transactionsOnly } catch { XCTFail("Querying the Transaction entity failed: \(error.localizedDescription)") } @@ -466,7 +488,7 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { do { // ⏺ Query the "Change" entity by "changeType" let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ - NSPredicate(format: "changeType == %d", NSPersistentHistoryChangeType.update.rawValue), // Change condition + NSPredicate(format: "changeType == %d", NSPersistentHistoryChangeType.update.rawValue) // Change condition // ⚠️ Even if it seems that some Transaction fields like "token" can be used here, the behavior is not predicatble // it's best if we stick with Change field names // NSPredicate(format: "token > %@", secondLastToken) // Transaction condition (working) @@ -481,7 +503,8 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { let transactions = try viewContext2.performAndWait { _ -> [NSPersistentHistoryTransaction] in let history = try viewContext2.execute(changeRequest) as! NSPersistentHistoryResult - let transactions = history.result as! [NSPersistentHistoryTransaction] // ordered from the oldest to the most recent + // ordered from the oldest to the most recent + let transactions = history.result as! [NSPersistentHistoryTransaction] return transactions } @@ -489,16 +512,17 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { let first = try XCTUnwrap(transactions.first) XCTAssertEqual(first.token, tokens.last) let changes = try XCTUnwrap(first.changes) - XCTAssertFalse(changes.isEmpty) // result type is transactionsAndChanges + XCTAssertFalse(changes.isEmpty) // result type is transactionsAndChanges } catch { XCTFail("Querying the Change entity failed: \(error.localizedDescription)") } do { // ⏺ Query the "Change" entity by "changeType" and "changedEntity" + // (sub entities are ignored by the second predicate) let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "changeType == %d", NSPersistentHistoryChangeType.insert.rawValue), - NSPredicate(format: "changedEntity == %@ || changedEntity == %@", Car.entity(), Person.entity()) // ignores sub entities + NSPredicate(format: "changedEntity == %@ || changedEntity == %@", Car.entity(), Person.entity()), ]) let request = try XCTUnwrap(NSPersistentHistoryChangeRequest.makeChangeFetchRequest(with: viewContext2)) @@ -508,7 +532,7 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { let changes = try viewContext2.historyChanges(using: changeRequest) - XCTAssertEqual(changes.count, 3) // 2 Cars + 1 Person + XCTAssertEqual(changes.count, 3) // 2 Cars + 1 Person } catch { XCTFail("Querying the Change entity failed: \(error.localizedDescription)") } @@ -526,7 +550,7 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { let historyFetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: token2) historyFetchRequest.fetchRequest = request - historyFetchRequest.resultType = .changesOnly // ⚠️ impact the return type + historyFetchRequest.resultType = .changesOnly // ⚠️ impact the return type // After token2 we expect "Transaction #3" containing 2 changes: // Updated: 1 Person @@ -534,7 +558,7 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { // Since the fetch used a predicate on the personObjectID, we expect only the Updated change on the Person object. let changes = try viewContext1.historyChanges(using: historyFetchRequest) - XCTAssertEqual(changes.count, 1) // Person + XCTAssertEqual(changes.count, 1) // Person // ⚠️ fetching history changes with a changedObjectID in the predicate doesn't work on a context associated with a different container // (even if the underlying store is the same) @@ -542,7 +566,10 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { // // At the moment querying for changes using changedObjectID seems useful only in bulk updates (many contexts for the same container) let changes2 = try viewContext2.historyChanges(using: historyFetchRequest) - XCTAssertTrue(changes2.isEmpty, "It seems that applying a predicate with changedObjectID doesn't work on a context associated with a different container.") + XCTAssertTrue( + changes2.isEmpty, + "It seems that applying a predicate with changedObjectID doesn't work on a context associated with a different container." + ) let moID = psc2.managedObjectID(forURIRepresentation: personObjectID.uriRepresentation())! let mo = try viewContext2.existingObject(with: moID) @@ -554,7 +581,7 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { let backgroundViewContext = container1.newBackgroundContext() try backgroundViewContext.performAndWait { let changes3 = try backgroundViewContext.historyChanges(using: historyFetchRequest) - XCTAssertEqual(changes3.count, 1) // Person + XCTAssertEqual(changes3.count, 1) // Person } let first = try XCTUnwrap(changes.first) @@ -562,7 +589,9 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { XCTAssertEqual(first.changeType, NSPersistentHistoryChangeType.update) } else { // FB8353599 - XCTAssertNil(changeEntityDescription.attributesByName["changedObjectID"], "Shown on WWDC 2020 but nil prior iOS 15 (FB8353599).") + XCTAssertNil( + changeEntityDescription.attributesByName["changedObjectID"], + "Shown on WWDC 2020 but nil prior iOS 15 (FB8353599).") } } @@ -573,11 +602,12 @@ final class NSManagedObjectContextHistoryTests: BaseTestCase { request.predicate = NSPredicate(format: "%K = %@", column, Car.entity()) let historyFetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: secondLastToken) - historyFetchRequest.fetchRequest = request // ⚠️ WWDC 2020: history requests can be tailored using the fetchRequest property - historyFetchRequest.resultType = .changesOnly // ⚠️ impact the return type + // ⚠️ WWDC 2020: history requests can be tailored using the fetchRequest property + historyFetchRequest.fetchRequest = request + historyFetchRequest.resultType = .changesOnly // ⚠️ impact the return type let changes = try viewContext2.historyChanges(using: historyFetchRequest) - XCTAssertEqual(changes.count, 1) // Car2 has been eliminated in the last token + XCTAssertEqual(changes.count, 1) // Car2 has been eliminated in the last token let first = try XCTUnwrap(changes.first) XCTAssertEqual(first.changedObjectID.uriRepresentation(), car2.objectID.uriRepresentation()) XCTAssertEqual(first.changeType, NSPersistentHistoryChangeType.delete) @@ -600,8 +630,9 @@ extension NSPersistentHistoryChangeRequest { /// - `timestamp` (`NSDate`) /// - `token` (`NSNumber` - `NSInteger64`) /// - `transactionNumber` (`NSNumber` - `NSInteger64`) - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - fileprivate final class func makeTransactionFetchRequest(with context: NSManagedObjectContext) -> NSFetchRequest? { + fileprivate final class func makeTransactionFetchRequest(with context: NSManagedObjectContext) -> NSFetchRequest< + NSFetchRequestResult + >? { // https://developer.apple.com/videos/play/wwdc2019/230 let transactionFetchRequest: NSFetchRequest if let request = NSPersistentHistoryTransaction.fetchRequest { @@ -626,8 +657,9 @@ extension NSPersistentHistoryChangeRequest { /// - `changedID` (`NSNumber` - `NSInteger64`) /// - `changedEntity` (`NSNumber` - `NSInteger64`) /// - `changeType` (`NSNumber` - `NSInteger64`) - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - public final class func makeChangeFetchRequest(with context: NSManagedObjectContext) -> NSFetchRequest? { + public final class func makeChangeFetchRequest(with context: NSManagedObjectContext) -> NSFetchRequest< + NSFetchRequestResult + >? { let changeFetchRequest: NSFetchRequest if let request = NSPersistentHistoryChange.fetchRequest { changeFetchRequest = request diff --git a/Tests/NSManagedObjectContextInvestigationTests.swift b/Tests/NSManagedObjectContextInvestigation_Tests.swift similarity index 87% rename from Tests/NSManagedObjectContextInvestigationTests.swift rename to Tests/NSManagedObjectContextInvestigation_Tests.swift index c1c68dc4..eb6409ca 100644 --- a/Tests/NSManagedObjectContextInvestigationTests.swift +++ b/Tests/NSManagedObjectContextInvestigation_Tests.swift @@ -2,10 +2,12 @@ import CoreData import XCTest +import os.lock -final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { +final class NSManagedObjectContextInvestigation_Tests: InMemoryTestCase { /// Investigation test: calling refreshAllObjects calls refreshObject:mergeChanges on all objects in the context. - func testInvestigationRefreshAllObjects() throws { + @MainActor + func test_InvestigationRefreshAllObjects() throws { let viewContext = container.viewContext let car1 = Car(context: viewContext) car1.numberPlate = "car1" @@ -23,14 +25,18 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { } /// Investigation test: KVO is fired whenever a property changes (even if the object is not saved in the context). - func testInvestigationKVO() throws { + @MainActor + func test_InvestigationKVO() throws { let context = container.viewContext let expectation = self.expectation(description: "\(#function)\(#line)") let sportCar1 = SportCar(context: context) - var count = 0 + let count = OSAllocatedUnfairLock(initialState: 0) let token = sportCar1.observe(\.maker, options: .new) { (car, changes) in - count += 1 - if count == 2 { + let shouldFulfill = count.withLock { + $0 += 1 + return $0 == 2 + } + if shouldFulfill { expectation.fulfill() } } @@ -45,12 +51,15 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { } /// Investigation test: automaticallyMergesChangesFromParent behaviour - func testInvestigationAutomaticallyMergesChangesFromParent() throws { + func test_InvestigationAutomaticallyMergesChangesFromParent() throws { // automaticallyMergesChangesFromParent = true do { - let psc = NSPersistentStoreCoordinator(managedObjectModel: model) + let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) let storeURL = URL.newDatabaseURL(withID: UUID()) - try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil) + try psc.addPersistentStore(ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: nil) let parentContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) parentContext.persistentStoreCoordinator = psc @@ -87,7 +96,7 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { // automaticallyMergesChangesFromParent = false do { - let psc = NSPersistentStoreCoordinator(managedObjectModel: model) + let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) let storeURL = URL.newDatabaseURL(withID: UUID()) try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil) @@ -119,7 +128,7 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { } childContext.performAndWait { - XCTAssertEqual(childCar.maker, "FIAT") // no changes + XCTAssertEqual(childCar.maker, "FIAT") // no changes } } @@ -186,12 +195,12 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { } childContext.performAndWait { - XCTAssertEqual(childCar.maker, "FIAT") // no changes + XCTAssertEqual(childCar.maker, "FIAT") // no changes } } } - func testInvestigationStalenessInterval() throws { + func test_InvestigationStalenessInterval() throws { // Given let context = container.viewContext let car = Car(context: context) @@ -218,14 +227,14 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { XCTAssertEqual(car.maker, "FIAT") // When, Then - context.stalenessInterval = 0 // issue a new fetch request instead of using the row cache + context.stalenessInterval = 0 // issue a new fetch request instead of using the row cache car.refresh() XCTAssertEqual(car.maker, "FCA") - context.stalenessInterval = -1 // default + context.stalenessInterval = -1 // default // The default is a negative value, which represents infinite staleness allowed. 0.0 represents β€œno staleness acceptable”. } - func testInvestigationShouldRefreshRefetchedObjectsIsStillBroken() throws { + func test_InvestigationShouldRefreshRefetchedObjectsIsStillBroken() throws { // https://mjtsai.com/blog/2019/10/17/ // I've opened a feedback myself too: FB7419788 @@ -272,7 +281,7 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { } } - func testInvestigationTransientProperties() throws { + func test_InvestigationTransientProperties() throws { let container = InMemoryPersistentContainer.makeNew() let viewContext = container.viewContext @@ -292,7 +301,7 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { XCTAssertEqual(car.currentDrivingSpeed, 0) } - func testInvestigationTransientPropertiesBehaviorInParentChildContextRelationship() throws { + func test_InvestigationTransientPropertiesBehaviorInParentChildContextRelationship() throws { let container = InMemoryPersistentContainer.makeNew() let viewContext = container.viewContext let childContext = viewContext.newBackgroundContext(asChildContext: true) @@ -310,12 +319,12 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { try! $0.save() carID = car.objectID XCTAssertEqual(car.currentDrivingSpeed, 50) - print($0.registeredObjects) - car.currentDrivingSpeed = 20 // ⚠️ dirting the context again + //print($0.registeredObjects) + car.currentDrivingSpeed = 20 // ⚠️ dirting the context again } childContext.performAndWait { - print(childContext.registeredObjects) + //print(childContext.registeredObjects) } let id = try XCTUnwrap(carID) @@ -323,19 +332,25 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { XCTAssertEqual(car.maker, "FIAT") XCTAssertEqual(car.model, "Panda") XCTAssertEqual(car.numberPlate, plateNumber) - XCTAssertEqual(car.currentDrivingSpeed, 50, "The transient property value should be equal to the one saved by the child context.") + XCTAssertEqual( + car.currentDrivingSpeed, 50, "The transient property value should be equal to the one saved by the child context." + ) try childContext.performAndWait { - XCTAssertFalse(childContext.registeredObjects.isEmpty) // ⚠️ this condition is verified only because we have dirted the context after a save + // ⚠️ this condition is verified only because we have dirted the context after a save + XCTAssertFalse(childContext.registeredObjects.isEmpty) let car = try XCTUnwrap($0.object(with: id) as? Car) XCTAssertEqual(car.currentDrivingSpeed, 20) try $0.save() } - XCTAssertEqual(car.currentDrivingSpeed, 20, "The transient property value should be equal to the one saved by the child context.") + XCTAssertEqual( + car.currentDrivingSpeed, 20, "The transient property value should be equal to the one saved by the child context." + ) try childContext.performAndWait { - XCTAssertTrue(childContext.registeredObjects.isEmpty) // ⚠️ it seems that after a save, the objects are freed unless the context gets dirted again + // ⚠️ it seems that after a save, the objects are freed unless the context gets dirted again + XCTAssertTrue(childContext.registeredObjects.isEmpty) let car = try XCTUnwrap(try Car.fetchUniqueObject(in: $0, where: predicate)) XCTAssertEqual(car.currentDrivingSpeed, 0) } @@ -343,7 +358,7 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { // see testInvestigationContextRegisteredObjectBehaviorAfterSaving } - func testInvestigationContextRegisteredObjectBehaviorAfterSaving() throws { + func test_InvestigationContextRegisteredObjectBehaviorAfterSaving() throws { let context = container.newBackgroundContext() // A context keeps registered objects until it's dirted @@ -387,7 +402,7 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { // MARK: - Batch Size investigations - func testFetchAsNSArrayUsingBatchSize() throws { + func test_FetchAsNSArrayUsingBatchSize() throws { // For this investigation you have to enable SQL logs in the test plan (-com.apple.CoreData.SQLDebug 3) let context = container.viewContext context.fillWithSampleData() @@ -399,10 +414,11 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { let request = Car.fetchRequest() request.addSortDescriptors([NSSortDescriptor(key: #keyPath(Car.maker), ascending: true)]) request.fetchBatchSize = 10 - let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) + let frc = NSFetchedResultsController( + fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) try frc.performFetch() // A SELECT with LIMIT 10 is executed every 10 looped cars βœ… - frc.fetchedObjects?.forEach { car in + for car in frc.fetchedObjects ?? [] { let _ = car as! Car } } @@ -421,7 +437,7 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { let cars = try Car.fetchNSArray(in: context) { $0.fetchBatchSize = 10 } // This for loop will trigger a SELECT with LIMIT 10 every 10 looped cars. βœ… - cars.forEach { car in + for car in cars { let _ = car as! Car } @@ -434,7 +450,7 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { let _ = cars.firstObject as! Car } - func testFetchAsDictionaryUsingBatchSize() throws { + func test_FetchAsDictionaryUsingBatchSize() throws { // For this investigation you have to enable SQL logs in the test plan (-com.apple.CoreData.SQLDebug 3) let context = container.viewContext context.fillWithSampleData() @@ -452,11 +468,11 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { // let results__batchSize_not_working = try context.fetch(request) as! [Dictionary] // triggers 13 SELECT with LIMIT 10 ❌ // // triggers only SELECT t0.Z_ENT, t0.Z_PK FROM ZCAR t0 - let results_batchSize_working = try context.fetch(request) as! [NSDictionary] + let resultsBatchSizeWorking = try context.fetch(request) as! [NSDictionary] // // triggers only a single SELECT with LIMIT 10 βœ… // // SELECT t0.ZMAKER, t0.Z_ENT, t0.Z_PK FROM ZCAR t0 WHERE t0.Z_PK IN (SELECT * FROM _Z_intarray0) LIMIT 10 - XCTAssertNotNil(results_batchSize_working.first) + XCTAssertNotNil(resultsBatchSizeWorking.first) } // MARK: - UndoManager @@ -492,10 +508,10 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { [[moc undoManager] enableUndoRegistration]; **/ - func testInvestigationUndoManager() throws { + func test_InvestigationUndoManager() throws { do { let context = container.newBackgroundContext() - context.undoManager = UndoManager() // undoManager is needed to use the undo/redo methods + context.undoManager = UndoManager() // undoManager is needed to use the undo/redo methods context.performAndWait { _ in context.fillWithSampleData() context.undo() @@ -526,14 +542,14 @@ final class NSManagedObjectContextInvestigationTests: InMemoryTestCase { context.performAndWait { _ in context.undoManager = UndoManager() // stuff... - context.processPendingChanges() // flush operations for which you want undos + context.processPendingChanges() // flush operations for which you want undos context.undoManager!.disableUndoRegistration() // make changes for which undo operations are not to be recorded let car = Car(context: context) car.numberPlate = "1" car.maker = "fake-maker" car.model = "fake-model" - context.processPendingChanges() // flush operations for which you do not want undos + context.processPendingChanges() // flush operations for which you do not want undos context.undoManager!.enableUndoRegistration() context.undo() XCTAssertFalse(context.insertedObjects.isEmpty) diff --git a/Tests/NSManagedObjectContextUtilsTests.swift b/Tests/NSManagedObjectContextUtils_Tests.swift similarity index 78% rename from Tests/NSManagedObjectContextUtilsTests.swift rename to Tests/NSManagedObjectContextUtils_Tests.swift index f34d309e..35a0719d 100644 --- a/Tests/NSManagedObjectContextUtilsTests.swift +++ b/Tests/NSManagedObjectContextUtils_Tests.swift @@ -1,11 +1,12 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSManagedObjectContextUtilsTests: InMemoryTestCase { - func testHasPersistentChanges() throws { +final class NSManagedObjectContextUtils_Tests: InMemoryTestCase { + func test_HasPersistentChanges() throws { let viewContext = container.viewContext XCTAssertFalse(viewContext.hasPersistentChanges) let car = Car(context: viewContext) @@ -24,10 +25,12 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { XCTAssertFalse(car.hasPersistentChangedValues) XCTAssertEqual(viewContext.persistentChangesCount, 0) XCTAssertEqual(viewContext.changesCount, 1) - XCTAssertFalse(viewContext.hasPersistentChanges, "viewContext shouldn't have committable changes because only transients properties are changed.") + XCTAssertFalse( + viewContext.hasPersistentChanges, + "viewContext shouldn't have committable changes because only transients properties are changed.") } - func testHasPersistentChangesInParentChildContextRelationship() throws { + func test_HasPersistentChangesInParentChildContextRelationship() throws { let viewContext = container.viewContext let backgroundContext = viewContext.newBackgroundContext(asChildContext: true) @@ -57,13 +60,14 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { XCTAssertEqual(backgroundContext.persistentChangesCount, 0) XCTAssertEqual(backgroundContext.changesCount, 1) - try! backgroundContext.save() // pushing the transient value up to the parent context + try! backgroundContext.save() // pushing the transient value up to the parent context } let car = try XCTUnwrap(try Car.fetchOneObject(in: viewContext, where: NSPredicate(value: true))) XCTAssertEqual(car.currentDrivingSpeed, 50) XCTAssertTrue(viewContext.hasChanges, "The viewContext should have uncommitted changes after the child save.") - XCTAssertTrue(viewContext.hasPersistentChanges, "The viewContext should have uncommitted changes after the child save.") + XCTAssertTrue( + viewContext.hasPersistentChanges, "The viewContext should have uncommitted changes after the child save.") XCTAssertEqual(viewContext.persistentChangesCount, 1) XCTAssertEqual(viewContext.changesCount, 1) try viewContext.save() @@ -74,7 +78,9 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { XCTAssertEqual(car.currentDrivingSpeed, 0) car.currentDrivingSpeed = 30 XCTAssertTrue(backgroundContext.hasChanges) - XCTAssertFalse(backgroundContext.hasPersistentChanges, "backgroundContext shouldn't have committable changes because only transients properties are changed.") + XCTAssertFalse( + backgroundContext.hasPersistentChanges, + "backgroundContext shouldn't have committable changes because only transients properties are changed.") XCTAssertEqual(backgroundContext.persistentChangesCount, 0) XCTAssertEqual(backgroundContext.changesCount, 1) try backgroundContext.save() @@ -85,22 +91,24 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { XCTAssertTrue(viewContext.hasChanges, "The transient property has changed") XCTAssertEqual(car.currentDrivingSpeed, 30) - XCTAssertFalse(viewContext.hasPersistentChanges, "viewContext shouldn't have committable changes because only transients properties are changed.") + XCTAssertFalse( + viewContext.hasPersistentChanges, + "viewContext shouldn't have committable changes because only transients properties are changed.") XCTAssertEqual(viewContext.persistentChangesCount, 0) XCTAssertEqual(viewContext.changesCount, 1) } - func testNewBackgroundContext() { + func test_NewBackgroundContext() { let backgroundContext = container.viewContext.newBackgroundContext(asChildContext: true) - XCTAssertEqual(backgroundContext.concurrencyType,.privateQueueConcurrencyType) - XCTAssertEqual(backgroundContext.parent,container.viewContext) + XCTAssertEqual(backgroundContext.concurrencyType, .privateQueueConcurrencyType) + XCTAssertEqual(backgroundContext.parent, container.viewContext) let backgroundContext2 = container.viewContext.newBackgroundContext() - XCTAssertEqual(backgroundContext2.concurrencyType,.privateQueueConcurrencyType) - XCTAssertNotEqual(backgroundContext2.parent,container.viewContext) + XCTAssertEqual(backgroundContext2.concurrencyType, .privateQueueConcurrencyType) + XCTAssertNotEqual(backgroundContext2.parent, container.viewContext) } - func testNewChildContext() { + func test_NewChildContext() { let childContext = container.viewContext.newChildContext() XCTAssertEqual(childContext.concurrencyType, container.viewContext.concurrencyType) XCTAssertTrue(childContext.parent === container.viewContext) @@ -109,20 +117,21 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { XCTAssertTrue(childContext2.parent === container.viewContext) } - func testPerformAndWait() throws { + func test_PerformAndWait() throws { let context = container.viewContext context.fillWithSampleData() let cars = try context.performAndWait { (_context) -> [Car] in - XCTAssertTrue(_context === context ) + XCTAssertTrue(_context === context) return try Car.fetchObjects(in: _context) } XCTAssertFalse(cars.isEmpty) } - func testPerformAndWaitWithThrow() { + @MainActor + func test_PerformAndWaitWithThrow() { let expectation1 = expectation(description: "\(#function)\(#line)") let context = container.viewContext @@ -130,7 +139,7 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { do { _ = try context.performAndWait { (_context) -> [Car] in - XCTAssertTrue(_context === context ) + XCTAssertTrue(_context === context) throw NSError(domain: "test", code: 1, userInfo: nil) } } catch let catchedError { @@ -148,7 +157,7 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { waitForExpectations(timeout: 2) } - func testSaveIfNeededOrRollback() { + func test_SaveIfNeededOrRollback() { let context = container.viewContext let car1 = Car(context: context) @@ -164,7 +173,7 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { XCTAssertNoThrow(try context.saveIfNeededOrRollBack()) - XCTAssertEqual(context.registeredObjects.count, 2) // person1 and car1 with a circular reference cycle + XCTAssertEqual(context.registeredObjects.count, 2) // person1 and car1 with a circular reference cycle let person2 = Person(context: context) person2.firstName = "Tin" @@ -175,10 +184,10 @@ final class NSManagedObjectContextUtilsTests: InMemoryTestCase { XCTAssertThrowsError(try context.saveIfNeededOrRollBack()) - XCTAssertEqual(context.registeredObjects.count, 2) // person2 is discarded because it cannot be saved + XCTAssertEqual(context.registeredObjects.count, 2) // person2 is discarded because it cannot be saved } - func testCollectionDelete() throws { + func test_CollectionDelete() throws { let context = container.viewContext let newContext = context.newBackgroundContext() diff --git a/Tests/NSManagedObjectDelayedDeletableTests.swift b/Tests/NSManagedObjectDelayedDeletable_Tests.swift similarity index 79% rename from Tests/NSManagedObjectDelayedDeletableTests.swift rename to Tests/NSManagedObjectDelayedDeletable_Tests.swift index 72c6ba25..d2eb6f85 100644 --- a/Tests/NSManagedObjectDelayedDeletableTests.swift +++ b/Tests/NSManagedObjectDelayedDeletable_Tests.swift @@ -1,11 +1,12 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSManagedObjectDelayedDeletableTests: InMemoryTestCase { - func testMarkAsDelayedDeletable() throws { +final class NSManagedObjectDelayedDeletable_Tests: InMemoryTestCase { + func test_MarkAsDelayedDeletable() throws { let context = container.viewContext context.fillWithSampleData() @@ -27,11 +28,15 @@ final class NSManagedObjectDelayedDeletableTests: InMemoryTestCase { // When, Then try context.save() - let fiatNotDeletablePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fiatPredicate, Car.notMarkedForLocalDeletionPredicate]) + let fiatNotDeletablePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + fiatPredicate, Car.notMarkedForLocalDeletionPredicate, + ]) let notDeletableCars = try! Car.fetchObjects(in: context) { $0.predicate = fiatNotDeletablePredicate } XCTAssertTrue(notDeletableCars.isEmpty) - let fiatDeletablePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fiatPredicate, Car.markedForLocalDeletionPredicate]) + let fiatDeletablePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + fiatPredicate, Car.markedForLocalDeletionPredicate, + ]) let deletableCars = try! Car.fetchObjects(in: context) { $0.predicate = fiatDeletablePredicate } XCTAssertTrue(deletableCars.count > 0) } diff --git a/Tests/NSManagedObjectUpdateTimestampableTests.swift b/Tests/NSManagedObjectUpdateTimestampable_Tests.swift similarity index 88% rename from Tests/NSManagedObjectUpdateTimestampableTests.swift rename to Tests/NSManagedObjectUpdateTimestampable_Tests.swift index d30087ee..c29b7f2e 100644 --- a/Tests/NSManagedObjectUpdateTimestampableTests.swift +++ b/Tests/NSManagedObjectUpdateTimestampable_Tests.swift @@ -1,12 +1,13 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSManagedObjectUpdateTimestampableTests: InMemoryTestCase { +final class NSManagedObjectUpdateTimestampable_Tests: InMemoryTestCase { - func testRefreshUpdateDate() throws { + func test_RefreshUpdateDate() throws { let context = container.viewContext context.fillWithSampleData() diff --git a/Tests/NSManagedObjectUtilsTests.swift b/Tests/NSManagedObjectUtils_Tests.swift similarity index 88% rename from Tests/NSManagedObjectUtilsTests.swift rename to Tests/NSManagedObjectUtils_Tests.swift index 6199e3e5..ac27124d 100644 --- a/Tests/NSManagedObjectUtilsTests.swift +++ b/Tests/NSManagedObjectUtils_Tests.swift @@ -1,12 +1,13 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSManagedObjectUtilsTests: InMemoryTestCase { +final class NSManagedObjectUtils_Tests: InMemoryTestCase { - func testRefresh() { + func test_Refresh() { // Given let context = container.viewContext @@ -41,8 +42,7 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { } - - func testChangedAndCommittedValue() throws { + func test_ChangedAndCommittedValue() throws { let context = container.viewContext let carNumberPlate = #keyPath(Car.numberPlate) @@ -55,8 +55,8 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { // When let car = Car(context: context) let person = Person(context: context) - XCTAssertNil(car.changedValue(forKey: carNumberPlate)) - XCTAssertNil(car.changedValue(forKey: carModel)) + XCTAssertNil(car.changedValue(forKey: carNumberPlate)) + XCTAssertNil(car.changedValue(forKey: carModel)) car.model = "MyModel" car.numberPlate = "123456" person.firstName = "Alessandro" @@ -100,7 +100,8 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { XCTAssertNotNil(car.committedValue(forKey: carNumberPlate)) let request = NSFetchRequest(entityName: "Car") - request.predicate = NSPredicate(format: "\(#keyPath(Car.model)) == %@ AND \(#keyPath(Car.numberPlate)) == %@", "MyModel", "202") + request.predicate = NSPredicate( + format: "\(#keyPath(Car.model)) == %@ AND \(#keyPath(Car.numberPlate)) == %@", "MyModel", "202") request.fetchBatchSize = 1 if let fetchedCar = try! context.fetch(request).first { XCTAssertNotNil(car.committedValue(forKey: carNumberPlate)) @@ -117,7 +118,7 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { } } - func testFaultAndMaterialize() throws { + func test_FaultAndMaterialize() throws { let context = container.viewContext // Given @@ -143,7 +144,7 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { XCTAssertTrue(sportCar1.isFault) } - func testDelete() { + func test_Delete() { let context = container.viewContext // Given @@ -159,7 +160,7 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { XCTAssertTrue(sportCar1.isDeleted) } - func testCreatePermanentID() throws { + func test_CreatePermanentID() throws { let context = container.viewContext let car = Car(context: context) car.maker = "McLaren" @@ -175,7 +176,7 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { XCTAssertFalse(car.objectID.isTemporaryID) } - func testEvaluatePredicate() { + func test_EvaluatePredicate() { let context = container.viewContext do { let predicate = NSPredicate.true @@ -202,14 +203,16 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { XCTAssertTrue(car.evaluate(with: predicate)) } do { - let predicate = NSPredicate(format: "%K == %@ AND %K == %@", #keyPath(Car.maker), "FIAT", #keyPath(Car.numberPlate), "123") + let predicate = NSPredicate( + format: "%K == %@ AND %K == %@", #keyPath(Car.maker), "FIAT", #keyPath(Car.numberPlate), "123") let car = Car(context: context) car.maker = "FIAT" car.numberPlate = "000" XCTAssertFalse(car.evaluate(with: predicate)) } do { - let predicate = NSPredicate(format: "%K == %@ AND %K == %@", #keyPath(Car.maker), "FIAT", #keyPath(Car.numberPlate), "123") + let predicate = NSPredicate( + format: "%K == %@ AND %K == %@", #keyPath(Car.maker), "FIAT", #keyPath(Car.numberPlate), "123") let car = Car(context: context) car.maker = "FIAT" car.numberPlate = "123" @@ -217,5 +220,3 @@ final class NSManagedObjectUtilsTests: InMemoryTestCase { } } } - - diff --git a/Tests/NSPersistentStoreCoordinatorUtilsTests.swift b/Tests/NSPersistentStoreCoordinatorUtils_Tests.swift similarity index 83% rename from Tests/NSPersistentStoreCoordinatorUtilsTests.swift rename to Tests/NSPersistentStoreCoordinatorUtils_Tests.swift index ceeaf774..4ca45b1d 100644 --- a/Tests/NSPersistentStoreCoordinatorUtilsTests.swift +++ b/Tests/NSPersistentStoreCoordinatorUtils_Tests.swift @@ -1,16 +1,17 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class NSPersistentStoreCoordinatorUtilsTests: BaseTestCase { - func testMetadata() throws { +final class NSPersistentStoreCoordinatorUtils_Tests: BaseTestCase { + func test_Metadata() throws { // Given let id = UUID() let container1 = OnDiskPersistentContainer.makeNew(id: id) let store1 = try XCTUnwrap(container1.persistentStoreCoordinator.persistentStores.first) - let psc1 = container1.persistentStoreCoordinator // ot context.persistentStoreCoordinator! + let psc1 = container1.persistentStoreCoordinator // ot context.persistentStoreCoordinator! // When let metaData = psc1.metadata(for: store1) @@ -37,16 +38,17 @@ final class NSPersistentStoreCoordinatorUtilsTests: BaseTestCase { XCTAssertNotNil(updatedMetaData2["testKey"]) XCTAssertEqual(updatedMetaData2["testKey"] as? String, "Test") - try psc2.removeAllStores() // container2 must unload the store otherwise container1 can't be destroyed (SQLITE error) because they point to the same db + // container2 must unload the store otherwise container1 can't be destroyed (SQLITE error) because they point to the same db + try psc2.removeAllStores() try container1.destroy() try container2.destroy() } - func testInvestigationSettingMetadataFromPersistentStore() throws { + func test_InvestigationSettingMetadataFromPersistentStore() throws { let id = UUID() let container = OnDiskPersistentContainer.makeNew(id: id) let store = try XCTUnwrap(container.persistentStoreCoordinator.persistentStores.first) - var metadata = store.metadata ?? [String : Any]() + var metadata = store.metadata ?? [String: Any]() metadata["testKey"] = "Test" store.metadata = metadata @@ -57,13 +59,13 @@ final class NSPersistentStoreCoordinatorUtilsTests: BaseTestCase { let container2 = OnDiskPersistentContainer.makeNew(id: id) let store2 = try XCTUnwrap(container2.persistentStoreCoordinator.persistentStores.first) - let metadata2 = try XCTUnwrap(store2.metadata) - print(metadata2) + let metadata2 = try XCTUnwrap(store2.metadata) + XCTAssertNotNil(metadata2["testKey"]) XCTAssertEqual(metadata2["testKey"] as? String, "Test") } - func testInvestigationSettingMetadataFromPersistentStoreCoordinator() throws { + func test_InvestigationSettingMetadataFromPersistentStoreCoordinator() throws { // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/PersistentStoreFeatures.html // There are two ways you can set the metadata for a store: // @@ -78,16 +80,20 @@ final class NSPersistentStoreCoordinatorUtilsTests: BaseTestCase { let container = OnDiskPersistentContainer.makeNew(id: id) let url = container.persistentStoreCoordinator.persistentStores.first!.url! - var metaData = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: url, options: nil) + var metaData = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, at: url, options: nil) metaData["testKey"] = "Test" - try NSPersistentStoreCoordinator.setMetadata(metaData, forPersistentStoreOfType: NSSQLiteStoreType, at: url, options: nil) - let metaData2 = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: url, options: nil) + try NSPersistentStoreCoordinator.setMetadata( + metaData, forPersistentStoreOfType: NSSQLiteStoreType, at: url, options: nil) + let metaData2 = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, at: url, options: nil) XCTAssertNotNil(metaData2["testKey"]) XCTAssertEqual(metaData2["testKey"] as? String, "Test") // Already loaded container must remove and reload the store to see the changes try container.persistentStoreCoordinator.removeAllStores() - try container.persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) + try container.persistentStoreCoordinator.addPersistentStore( + ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) let psc = container.persistentStoreCoordinator let store = try XCTUnwrap(container.persistentStoreCoordinator.persistentStores.first) let updatedMetaData = psc.metadata(for: store) @@ -103,7 +109,7 @@ final class NSPersistentStoreCoordinatorUtilsTests: BaseTestCase { XCTAssertEqual(updatedMetaData2["testKey"] as? String, "Test") } - func testDestroyMissingStore() throws { + func test_DestroyMissingStore() throws { let wrongURL = URL(fileURLWithPath: "/dev/null") XCTAssertThrowsError(try NSPersistentStoreCoordinator.destroyStore(at: wrongURL)) diff --git a/Tests/NSPredicateUtilsTests.swift b/Tests/NSPredicateUtils_Tests.swift similarity index 79% rename from Tests/NSPredicateUtilsTests.swift rename to Tests/NSPredicateUtils_Tests.swift index 4721b98e..2ae69f03 100644 --- a/Tests/NSPredicateUtilsTests.swift +++ b/Tests/NSPredicateUtils_Tests.swift @@ -2,13 +2,13 @@ import XCTest -final class NSPredicateUtilsTests: XCTestCase { - func testAlwaysTrueAndFalsePredicates() { +final class NSPredicateUtils_Tests: XCTestCase { + func test_AlwaysTrueAndFalsePredicates() { XCTAssertEqual(NSPredicate.true.predicateFormat, "TRUEPREDICATE") XCTAssertEqual(NSPredicate.false.predicateFormat, "FALSEPREDICATE") } - func testPredicateComposition() { + func test_PredicateComposition() { do { let predicate = NSPredicate(format: "X = 10").and(NSPredicate(format: "Y = 30")) XCTAssertTrue(predicate == NSPredicate(format: "X = 10 AND Y = 30")) @@ -18,13 +18,13 @@ final class NSPredicateUtilsTests: XCTestCase { XCTAssertTrue(predicate == NSPredicate(format: "Z = 20 OR K = 40")) } do { - let predicate1 = NSPredicate(format: "X = 10").and(NSPredicate(format: "Y = 30")) // X = 10 AND Y = 30 + let predicate1 = NSPredicate(format: "X = 10").and(NSPredicate(format: "Y = 30")) // X = 10 AND Y = 30 let predicate2 = NSPredicate(format: "Z = 20").or(NSPredicate(format: "K = 40")) // Z = 20 OR K = 40 let predicate3 = predicate1.and(predicate2) XCTAssertTrue(predicate3.description == "(X == 10 AND Y == 30) AND (Z == 20 OR K == 40)") } do { - let predicate1 = NSPredicate(format: "X = 10").and(NSPredicate(format: "Y = 30")) // X = 10 AND Y = 30 + let predicate1 = NSPredicate(format: "X = 10").and(NSPredicate(format: "Y = 30")) // X = 10 AND Y = 30 let predicate2 = NSPredicate(format: "Z = 20").or(NSPredicate(format: "K = 40")) // Z = 20 OR K = 40 let predicate3 = predicate1.or(predicate2) XCTAssertTrue(predicate3.description == "(X == 10 AND Y == 30) OR (Z == 20 OR K == 40)") @@ -33,7 +33,9 @@ final class NSPredicateUtilsTests: XCTestCase { let predicate1 = NSPredicate(format: "X = 10 AND V = 11").and(NSPredicate(format: "Y = 30 OR W = 5")) let predicate2 = NSPredicate(format: "Z = 20").or(NSPredicate(format: "K = 40 AND C = 11")) let predicate3 = predicate1.or(predicate2) - XCTAssertTrue(predicate3.description == "((X == 10 AND V == 11) AND (Y == 30 OR W == 5)) OR (Z == 20 OR (K == 40 AND C == 11))") + XCTAssertTrue( + predicate3.description + == "((X == 10 AND V == 11) AND (Y == 30 OR W == 5)) OR (Z == 20 OR (K == 40 AND C == 11))") } } } diff --git a/Tests/NSSetCoreDataTests.swift b/Tests/NSSetCoreData_Tests.swift similarity index 78% rename from Tests/NSSetCoreDataTests.swift rename to Tests/NSSetCoreData_Tests.swift index 7a144322..9760c043 100644 --- a/Tests/NSSetCoreDataTests.swift +++ b/Tests/NSSetCoreData_Tests.swift @@ -2,11 +2,12 @@ import CoreData import XCTest + @testable import CoreDataPlus -final class NSSetCoreDataTests: InMemoryTestCase { +final class NSSetCoreData_Tests: InMemoryTestCase { - func testMaterializeFaultedManagedObjects() throws { + func test_MaterializeFaultedManagedObjects() throws { let context = container.viewContext context.performAndWait { context.fillWithSampleData() @@ -15,7 +16,8 @@ final class NSSetCoreDataTests: InMemoryTestCase { let request = Person.newFetchRequest() request.returnsObjectsAsFaults = true - let predicate = NSPredicate(format: "\(#keyPath(Person.firstName)) == %@ AND \(#keyPath(Person.lastName)) == %@", "Theodora", "Stone") + let predicate = NSPredicate( + format: "\(#keyPath(Person.firstName)) == %@ AND \(#keyPath(Person.lastName)) == %@", "Theodora", "Stone") let foundPerson = try Person.fetchUniqueObject(in: context, where: predicate, affectedStores: nil) let person = try XCTUnwrap(foundPerson) context.refreshAllObjects() @@ -42,7 +44,7 @@ final class NSSetCoreDataTests: InMemoryTestCase { XCTAssertEqual(finalFaultsCount, 0) } - func testDeleteManagedObjects() throws { + func test_DeleteManagedObjects() throws { let context = container.viewContext context.performAndWait { context.fillWithSampleData() @@ -52,7 +54,8 @@ final class NSSetCoreDataTests: InMemoryTestCase { let request = Person.newFetchRequest() request.returnsObjectsAsFaults = true - let predicate = NSPredicate(format: "\(#keyPath(Person.firstName)) == %@ AND \(#keyPath(Person.lastName)) == %@", "Theodora", "Stone") + let predicate = NSPredicate( + format: "\(#keyPath(Person.firstName)) == %@ AND \(#keyPath(Person.lastName)) == %@", "Theodora", "Stone") let foundPerson = try Person.fetchUniqueObject(in: context, where: predicate, affectedStores: nil) let person = try XCTUnwrap(foundPerson) diff --git a/Tests/Notifications/NotificationMergeTests.swift b/Tests/Notifications/NotificationMerge_Tests.swift similarity index 65% rename from Tests/Notifications/NotificationMergeTests.swift rename to Tests/Notifications/NotificationMerge_Tests.swift index 8766c012..426964b2 100644 --- a/Tests/Notifications/NotificationMergeTests.swift +++ b/Tests/Notifications/NotificationMerge_Tests.swift @@ -4,16 +4,19 @@ // http://mikeabdullah.net/merging-saved-changes-betwe.html // http://www.mlsite.net/blog/?p=518 -import XCTest -import CoreData import Combine +import CoreData +import XCTest + @testable import CoreDataPlus -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -final class NotificationMergeTests: InMemoryTestCase { - func testInvestigationRegisteredObjects() throws { - try XCTSkipIf(!ProcessInfo.processInfo.environment.keys.contains("XCODE_TESTS"), "This test should be run via Xcode and not using Swift test.") - try XCTSkipIf(ProcessInfo.processInfo.arguments.contains("zombieObjectsEnabled"), "Testing with Zombie Objects enabled") +final class NotificationMerge_Tests: InMemoryTestCase { + func test_InvestigationRegisteredObjects() throws { + try XCTSkipIf( + !ProcessInfo.processInfo.environment.keys.contains("XCODE_TESTS"), + "This test should be run via Xcode and not using Swift test.") + try XCTSkipIf( + ProcessInfo.processInfo.arguments.contains("zombieObjectsEnabled"), "Testing with Zombie Objects enabled") // By default, a managed object context only keeps a strong reference to managed objects that have pending changes. // This means that objects your code doesn’t have a strong reference to, will be removed from the context’s registeredObjects set and be deallocated @@ -55,7 +58,8 @@ final class NotificationMergeTests: InMemoryTestCase { XCTAssertEqual(viewContext.registeredObjects.count, 2) } - func testInvestigationMergeChanges() throws { + @MainActor + func test_InvestigationMergeChanges() throws { // see: testInvesigationRegisteredObjects let expectation1 = expectation(description: "\(#function)\(#line)") let viewContext = container.viewContext @@ -74,42 +78,47 @@ final class NotificationMergeTests: InMemoryTestCase { func findRegisteredPersonByFirstName(_ name: String, in context: NSManagedObjectContext) -> Person? { var person: Person? context.performAndWait { - person = context.registeredObjects.first { object in - if let person = object as? Person { - return person.firstName == name - } - return false - } as? Person + person = + context.registeredObjects.first { object in + if let person = object as? Person { + return person.firstName == name + } + return false + } as? Person } return person } let backgroundContext = container.viewContext.newBackgroundContext(asChildContext: false) - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: backgroundContext) - .map { ManagedObjectContextDidSaveObjects(notification: $0) } - .sink { payload in - XCTAssertEqual(payload.insertedObjects.count, 1) - XCTAssertEqual(payload.updatedObjects.count, 1) - XCTAssertEqual(payload.deletedObjects.count, 1) + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextDidSave, object: backgroundContext + ) + .map { ManagedObjectContextDidSaveObjects(notification: $0) } + .sink { payload in + XCTAssertEqual(payload.insertedObjects.count, 1) + XCTAssertEqual(payload.updatedObjects.count, 1) + XCTAssertEqual(payload.deletedObjects.count, 1) - XCTAssertEqual(viewContext.registeredObjects.count, 2) - XCTAssertEqual(viewContext.deletedObjects.count, 0) + XCTAssertEqual(viewContext.registeredObjects.count, 2) + XCTAssertEqual(viewContext.deletedObjects.count, 0) - let updatedPersonBefore = findRegisteredPersonByFirstName("Andrea", in: viewContext) - XCTAssertNotNil(updatedPersonBefore) + let updatedPersonBefore = findRegisteredPersonByFirstName("Andrea", in: viewContext) + XCTAssertNotNil(updatedPersonBefore) - viewContext.mergeChanges(fromContextDidSavePayload: payload) + viewContext.mergeChanges(fromContextDidSavePayload: payload) - let updatedPersonAfter = findRegisteredPersonByFirstName("Andrea", in: viewContext) - XCTAssertNil(updatedPersonAfter) - let updatedPersonAfterCorrect = findRegisteredPersonByFirstName("Andrea**", in: viewContext) - XCTAssertNotNil(updatedPersonAfterCorrect) + let updatedPersonAfter = findRegisteredPersonByFirstName("Andrea", in: viewContext) + XCTAssertNil(updatedPersonAfter) + let updatedPersonAfterCorrect = findRegisteredPersonByFirstName("Andrea**", in: viewContext) + XCTAssertNotNil(updatedPersonAfterCorrect) - XCTAssertEqual(viewContext.registeredObjects.count, 2) - XCTAssertEqual(viewContext.insertedObjects.count, 0) // no objects have been inserted (but not yet saved) in this context - XCTAssertEqual(viewContext.deletedObjects.count, 1) // a previously registered object has been deleted from this context - expectation1.fulfill() - } + XCTAssertEqual(viewContext.registeredObjects.count, 2) + // no objects have been inserted (but not yet saved) in this context + XCTAssertEqual(viewContext.insertedObjects.count, 0) + // a previously registered object has been deleted from this context + XCTAssertEqual(viewContext.deletedObjects.count, 1) + expectation1.fulfill() + } try backgroundContext.performAndWait { try Person.delete(in: $0, where: NSPredicate(format: "%K == %@", #keyPath(Person.firstName), "Alessandro")) @@ -127,17 +136,18 @@ final class NotificationMergeTests: InMemoryTestCase { } - func testMerge() throws { + @MainActor + func test_Merge() throws { let viewContext = container.viewContext let backgroundContext = container.viewContext.newBackgroundContext(asChildContext: false) - let person1_inserted = Person(context: viewContext) - person1_inserted.firstName = "Edythe" - person1_inserted.lastName = "Moreton" + let person1Inserted = Person(context: viewContext) + person1Inserted.firstName = "Edythe" + person1Inserted.lastName = "Moreton" - let person2_inserted = Person(context: viewContext) - person2_inserted.firstName = "Ellis" - person2_inserted.lastName = "Khoury" + let person2Inserted = Person(context: viewContext) + person2Inserted.firstName = "Ellis" + person2Inserted.lastName = "Khoury" try viewContext.save() @@ -149,21 +159,19 @@ final class NotificationMergeTests: InMemoryTestCase { var cancellables = [AnyCancellable]() - if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) { - // [6] observer - let expectation6 = self.expectation(description: "\(#function)\(#line)") - NotificationCenter.default.publisher(for: .NSManagedObjectContextDidMergeChangesObjectIDs, object: viewContext) - .map { ManagedObjectContextDidMergeChangesObjectIDs(notification: $0) } - .sink { payload in - XCTAssertTrue(payload.managedObjectContext === viewContext) - XCTAssertEqual(payload.insertedObjectIDs.count, 1) - XCTAssertEqual(payload.updatedObjectIDs.count, 2) - XCTAssertTrue(payload.deletedObjectIDs.isEmpty) - XCTAssertEqual(payload.refreshedObjectIDs.count, 2) - expectation6.fulfill() - } - .store(in: &cancellables) - } + // [6] observer + let expectation6 = self.expectation(description: "\(#function)\(#line)") + NotificationCenter.default.publisher(for: .NSManagedObjectContextDidMergeChangesObjectIDs, object: viewContext) + .map { ManagedObjectContextDidMergeChangesObjectIDs(notification: $0) } + .sink { payload in + XCTAssertTrue(payload.managedObjectContext === viewContext) + XCTAssertEqual(payload.insertedObjectIDs.count, 1) + XCTAssertEqual(payload.updatedObjectIDs.count, 2) + XCTAssertTrue(payload.deletedObjectIDs.isEmpty) + XCTAssertEqual(payload.refreshedObjectIDs.count, 2) + expectation6.fulfill() + } + .store(in: &cancellables) // [4] observer NotificationCenter.default.publisher(for: .NSManagedObjectContextWillSave, object: backgroundContext) @@ -189,12 +197,12 @@ final class NotificationMergeTests: InMemoryTestCase { // merging "backgroundContext" changes into the "viewContext" will: // show "backgroundContext" updatedObjects as NSRefreshedObjectsKey changes in the "viewContext" objects-did-change notification // show "backgroundContext" insertedObjects as NSInsertedObjectsKey changes in the "viewContext" objects-did-change notification - viewContext.mergeChanges(fromContextDidSavePayload: payload) // fires [2] [6] + viewContext.mergeChanges(fromContextDidSavePayload: payload) // fires [2] [6] viewContext.performAndWait { // Before saving, we didn't change anything: we don't expect any changes in the objects-did-save notification listened by [3] observer. // see: testInvesigationMergeChanges() - try! viewContext.save() // fires [3] + try! viewContext.save() // fires [3] } expectation2.fulfill() } @@ -242,11 +250,11 @@ final class NotificationMergeTests: InMemoryTestCase { person.firstName += " Updated" } - let person3_inserted = Person(context: backgroundContext) - person3_inserted.firstName = "Alessandro" - person3_inserted.lastName = "Marzoli" + let person3Inserted = Person(context: backgroundContext) + person3Inserted.firstName = "Alessandro" + person3Inserted.lastName = "Marzoli" - try! backgroundContext.save() // fires [0], [4] and then [1] + try! backgroundContext.save() // fires [0], [4] and then [1] } waitForExpectations(timeout: 20) @@ -258,7 +266,8 @@ final class NotificationMergeTests: InMemoryTestCase { } } - func testAsyncMerge() throws { + @MainActor + func test_AsyncMerge() throws { let context = container.viewContext let anotherContext = container.viewContext.newBackgroundContext(asChildContext: false) @@ -266,10 +275,12 @@ final class NotificationMergeTests: InMemoryTestCase { let expectation2 = expectation(description: "\(#function)\(#line)") let expectation3 = expectation(description: "\(#function)\(#line)") - let cancellable1 = NotificationCenter.default.publisher(for: .NSManagedObjectContextWillSave, object: anotherContext) - .sink { _ in - expectation1.fulfill() - } + let cancellable1 = NotificationCenter.default.publisher( + for: .NSManagedObjectContextWillSave, object: anotherContext + ) + .sink { _ in + expectation1.fulfill() + } let cancellable2 = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: anotherContext) .map { ManagedObjectContextDidSaveObjects(notification: $0) } @@ -280,36 +291,37 @@ final class NotificationMergeTests: InMemoryTestCase { } } - let cancellable3 = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: anotherContext) - .sink { _ in - expectation3.fulfill() - } + let cancellable3 = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: anotherContext + ) + .sink { _ in + expectation3.fulfill() + } - let person1_inserted = Person(context: context) - person1_inserted.firstName = "Edythe" - person1_inserted.lastName = "Moreton" + let person1Inserted = Person(context: context) + person1Inserted.firstName = "Edythe" + person1Inserted.lastName = "Moreton" - let person2_inserted = Person(context: context) - person2_inserted.firstName = "Ellis" - person2_inserted.lastName = "Khoury" + let person2Inserted = Person(context: context) + person2Inserted.firstName = "Ellis" + person2Inserted.lastName = "Khoury" try context.save() - try anotherContext.performAndWait {_ in + try anotherContext.performAndWait { _ in let persons = try Person.fetchObjects(in: anotherContext) for person in persons { person.firstName += " Updated" } - let person3_inserted = Person(context: anotherContext) - person3_inserted.firstName = "Alessandro" - person3_inserted.lastName = "Marzoli" + let person3Inserted = Person(context: anotherContext) + person3Inserted.firstName = "Alessandro" + person3Inserted.lastName = "Marzoli" try anotherContext.save() } - waitForExpectations(timeout: 2) XCTAssertFalse(context.hasChanges) try anotherContext.performAndWait { _ in @@ -322,7 +334,8 @@ final class NotificationMergeTests: InMemoryTestCase { cancellable3.cancel() } - func testNSFetchedResultController() throws { + @MainActor + func test_NSFetchedResultController() throws { let context = container.viewContext let person1 = Person(context: context) @@ -345,7 +358,8 @@ final class NotificationMergeTests: InMemoryTestCase { request.addSortDescriptors([]) let delegate = FetchedResultsControllerMockDelegate() - let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) + let frc = NSFetchedResultsController( + fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) frc.delegate = delegate try frc.performFetch() @@ -355,8 +369,8 @@ final class NotificationMergeTests: InMemoryTestCase { let cancellable1 = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: anotherContext) .map { ManagedObjectContextDidSaveObjects(notification: $0) } .sink { payload in - XCTAssertEqual(payload.insertedObjects.count, 2) // 1 person and 1 car - XCTAssertEqual(payload.updatedObjects.count, 1) // 1 person + XCTAssertEqual(payload.insertedObjects.count, 2) // 1 person and 1 car + XCTAssertEqual(payload.updatedObjects.count, 1) // 1 person context.perform { context.mergeChanges(fromContextDidSavePayload: payload) expectation1.fulfill() @@ -390,7 +404,7 @@ final class NotificationMergeTests: InMemoryTestCase { waitForExpectations(timeout: 5) XCTAssertEqual(delegate.updatedObjects.count + delegate.movedObjects.count, 1) - XCTAssertEqual(delegate.insertedObjects.count, 1) // the FRC monitors only for Person objects + XCTAssertEqual(delegate.insertedObjects.count, 1) // the FRC monitors only for Person objects XCTAssertEqual(context.registeredObjects.count, 4) @@ -401,7 +415,8 @@ final class NotificationMergeTests: InMemoryTestCase { cancellable1.cancel() } - func testNSFetchedResultControllerWithContextReset() throws { + @MainActor + func test_NSFetchedResultControllerWithContextReset() throws { let context = container.viewContext let person1 = Person(context: context) @@ -422,7 +437,8 @@ final class NotificationMergeTests: InMemoryTestCase { request.addSortDescriptors([]) let delegate = FetchedResultsControllerMockDelegate() - let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) + let frc = NSFetchedResultsController( + fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) frc.delegate = delegate try frc.performFetch() @@ -441,12 +457,14 @@ final class NotificationMergeTests: InMemoryTestCase { } } - let cancellable2 = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertFalse(payload.invalidatedAllObjects.isEmpty) - expectation2.fulfill() - } + let cancellable2 = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: context + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertFalse(payload.invalidatedAllObjects.isEmpty) + expectation2.fulfill() + } let persons = try Person.fetchObjects(in: context) @@ -466,7 +484,7 @@ final class NotificationMergeTests: InMemoryTestCase { firstPerson._cars = Set([car2]) context.reset() - try context.save() // the command will do nothing, the FRC delegate is exepcted to have 0 changed objects + try context.save() // the command will do nothing, the FRC delegate is exepcted to have 0 changed objects waitForExpectations(timeout: 5) @@ -478,38 +496,39 @@ final class NotificationMergeTests: InMemoryTestCase { cancellable1.cancel() cancellable2.cancel() } +} - /** - NSFetchedResultsController: Handling Object Invalidation - - https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller - - When a managed object context notifies the fetched results controller that individual objects are invalidated, the controller treats these as deleted objects and sends the proper delegate calls. - - It’s possible for all the objects in a managed object context to be invalidated simultaneously. - (For example, as a result of calling reset(), or if a store is removed from the the persistent store coordinator.). - When this happens, NSFetchedResultsController does not invalidate all objects, nor does it send individual notifications for object deletions. - Instead, you must call performFetch() to reset the state of the controller then reload the data in the table view (reloadData()). - **/ - class FetchedResultsControllerMockDelegate: NSObject, NSFetchedResultsControllerDelegate { - var updatedObjects = [Any]() - var insertedObjects = [Any]() - var movedObjects = [Any]() - var deletedObjects = [Any]() - - public func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - switch (type) { - case .delete: - deletedObjects.append(anObject) - case .insert: - insertedObjects.append(anObject) - case .move: - movedObjects.append(anObject) - case .update: - updatedObjects.append(anObject) - @unknown default: - fatalError("not implemented") - } +/// NSFetchedResultsController: Handling Object Invalidation +/// +/// https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller +/// +/// When a managed object context notifies the fetched results controller that individual objects are invalidated, the controller treats these as deleted objects and sends the proper delegate calls. +/// +/// It’s possible for all the objects in a managed object context to be invalidated simultaneously. +/// (For example, as a result of calling reset(), or if a store is removed from the the persistent store coordinator.). +/// When this happens, NSFetchedResultsController does not invalidate all objects, nor does it send individual notifications for object deletions. +/// Instead, you must call performFetch() to reset the state of the controller then reload the data in the table view (reloadData()). +class FetchedResultsControllerMockDelegate: NSObject, NSFetchedResultsControllerDelegate { + var updatedObjects = [Any]() + var insertedObjects = [Any]() + var movedObjects = [Any]() + var deletedObjects = [Any]() + + public func controller( + _ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, + for type: NSFetchedResultsChangeType, newIndexPath: IndexPath? + ) { + switch type { + case .delete: + deletedObjects.append(anObject) + case .insert: + insertedObjects.append(anObject) + case .move: + movedObjects.append(anObject) + case .update: + updatedObjects.append(anObject) + @unknown default: + fatalError("not implemented") } } } diff --git a/Tests/Notifications/NotificationPayloadTests.swift b/Tests/Notifications/NotificationPayload_Tests.swift similarity index 58% rename from Tests/Notifications/NotificationPayloadTests.swift rename to Tests/Notifications/NotificationPayload_Tests.swift index 6eae363c..ae28dae7 100644 --- a/Tests/Notifications/NotificationPayloadTests.swift +++ b/Tests/Notifications/NotificationPayload_Tests.swift @@ -1,12 +1,12 @@ // CoreDataPlus -import XCTest -import CoreData import Combine +import CoreData +import XCTest + @testable import CoreDataPlus -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -final class NotificationPayloadTests: InMemoryTestCase { +final class NotificationPayload_Tests: InMemoryTestCase { /// To issue a NSManagedObjectContextObjectsDidChangeNotification from a background thread, call the NSManagedObjectContext’s processPendingChanges method. /// http://openradar.appspot.com/14310964 /// NSManagedObjectContext’s `perform` method encapsulates an autorelease pool and a call to processPendingChanges, `performAndWait` does not. @@ -44,7 +44,8 @@ final class NotificationPayloadTests: InMemoryTestCase { // MARK: - NSManagedObjectContextObjectsDidChange - func testObserveInsertionsAndInvalidationsOnDidChangeNotification() { + @MainActor + func test_ObserveInsertionsAndInvalidationsOnDidChangeNotification() { // Invalidation causes: // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/TroubleshootingCoreData.html // Either you have removed the store for the fault you are attempting to fire, or the managed object's context has been sent a reset. @@ -57,30 +58,30 @@ final class NotificationPayloadTests: InMemoryTestCase { .map { ManagedObjectContextObjectsDidChange(notification: $0) } .sink { payload in switch count { - case 0: - count += 1 - XCTAssertTrue(Thread.isMainThread) - XCTAssertTrue(payload.managedObjectContext === context) - XCTAssertEqual(payload.insertedObjects.count, 1) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation.fulfill() - case 1: - count += 1 - XCTAssertTrue(Thread.isMainThread) - XCTAssertTrue(payload.managedObjectContext === context) - XCTAssertTrue(payload.insertedObjects.isEmpty) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertEqual(payload.invalidatedAllObjects.count, 1) - expectation2.fulfill() - default: - XCTFail("Too many notifications.") + case 0: + count += 1 + XCTAssertTrue(Thread.isMainThread) + XCTAssertTrue(payload.managedObjectContext === context) + XCTAssertEqual(payload.insertedObjects.count, 1) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation.fulfill() + case 1: + count += 1 + XCTAssertTrue(Thread.isMainThread) + XCTAssertTrue(payload.managedObjectContext === context) + XCTAssertTrue(payload.insertedObjects.isEmpty) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertEqual(payload.invalidatedAllObjects.count, 1) + expectation2.fulfill() + default: + XCTFail("Too many notifications.") } } @@ -100,53 +101,60 @@ final class NotificationPayloadTests: InMemoryTestCase { cancellable.cancel() } - func testObserveInsertionsOnDidChangeNotificationOnBackgroundContext() { + @MainActor + func test_ObserveInsertionsOnDidChangeNotificationOnBackgroundContext() { let expectation = self.expectation(description: "\(#function)\(#line)") let backgroundContext = container.newBackgroundContext() - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: backgroundContext) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertTrue(Thread.isMainThread) - XCTAssertTrue(payload.managedObjectContext === backgroundContext) - XCTAssertTrue(Thread.isMainThread) - XCTAssertEqual(payload.insertedObjects.count, 1) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: backgroundContext + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertTrue(Thread.isMainThread) + XCTAssertTrue(payload.managedObjectContext === backgroundContext) + XCTAssertTrue(Thread.isMainThread) + XCTAssertEqual(payload.insertedObjects.count, 1) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation.fulfill() + } + // on a background context, processPendingChanges() must be called to trigger the notification backgroundContext.performAndWait { let car = Car(context: backgroundContext) car.maker = "FIAT" car.model = "Panda" car.numberPlate = "1" car.maker = "123!" - backgroundContext.processPendingChanges() // on a background context, processPendingChanges() must be called to trigger the notification + backgroundContext.processPendingChanges() } waitForExpectations(timeout: 5) cancellable.cancel() } - func testObserveAsyncInsertionsOnDidChangeNotificationOnBackgroundContext() { + @MainActor + func test_ObserveAsyncInsertionsOnDidChangeNotificationOnBackgroundContext() { let expectation = self.expectation(description: "\(#function)\(#line)") let backgroundContext = container.newBackgroundContext() - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: backgroundContext) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertFalse(Thread.isMainThread) - XCTAssertTrue(payload.managedObjectContext === backgroundContext) - XCTAssertFalse(Thread.isMainThread) // `perform` is async, and it is responsible for posting this notification. - XCTAssertEqual(payload.insertedObjects.count, 1) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: backgroundContext + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertFalse(Thread.isMainThread) + XCTAssertTrue(payload.managedObjectContext === backgroundContext) + XCTAssertFalse(Thread.isMainThread) // `perform` is async, and it is responsible for posting this notification. + XCTAssertEqual(payload.insertedObjects.count, 1) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation.fulfill() + } // perform, as stated in the documentation, calls internally processPendingChanges backgroundContext.perform { @@ -161,23 +169,26 @@ final class NotificationPayloadTests: InMemoryTestCase { cancellable.cancel() } - func testObserveAsyncInsertionsOnDidChangeNotificationOnBackgroundContextAndDispatchQueue() { + @MainActor + func test_ObserveAsyncInsertionsOnDidChangeNotificationOnBackgroundContextAndDispatchQueue() { let expectation = self.expectation(description: "\(#function)\(#line)") let backgroundContext = container.newBackgroundContext() - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: backgroundContext) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertFalse(Thread.isMainThread) - XCTAssertTrue(payload.managedObjectContext === backgroundContext) - XCTAssertFalse(Thread.isMainThread) // `perform` is async, and it is responsible for posting this notification. - XCTAssertEqual(payload.insertedObjects.count, 200) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: backgroundContext + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertFalse(Thread.isMainThread) + XCTAssertTrue(payload.managedObjectContext === backgroundContext) + XCTAssertFalse(Thread.isMainThread) // `perform` is async, and it is responsible for posting this notification. + XCTAssertEqual(payload.insertedObjects.count, 200) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation.fulfill() + } // performBlockAndWait will always run in the calling thread. // Using a DispatchQueue, we are making sure that it's not run on the Main Thread @@ -204,24 +215,26 @@ final class NotificationPayloadTests: InMemoryTestCase { cancellable.cancel() } - - func testObserveInsertionsOnDidChangeNotificationOnPrivateContext() throws { + @MainActor + func test_ObserveInsertionsOnDidChangeNotificationOnPrivateContext() throws { let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.persistentStoreCoordinator = container.persistentStoreCoordinator let expectation = self.expectation(description: "\(#function)\(#line)") - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: privateContext) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertTrue(Thread.isMainThread) - XCTAssertTrue(payload.managedObjectContext === privateContext) - XCTAssertEqual(payload.insertedObjects.count, 1) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: privateContext + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertTrue(Thread.isMainThread) + XCTAssertTrue(payload.managedObjectContext === privateContext) + XCTAssertEqual(payload.insertedObjects.count, 1) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation.fulfill() + } privateContext.performAndWait { let car = Car(context: privateContext) @@ -235,25 +248,28 @@ final class NotificationPayloadTests: InMemoryTestCase { cancellable.cancel() } - func testObserveRefreshedObjectsOnDidChangeNotification() throws { + @MainActor + func test_ObserveRefreshedObjectsOnDidChangeNotification() throws { let context = container.viewContext context.fillWithSampleData() try context.save() let registeredObjectsCount = context.registeredObjects.count let expectation = self.expectation(description: "\(#function)\(#line)") - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertTrue(Thread.isMainThread) - XCTAssertTrue(payload.managedObjectContext === context) - XCTAssertTrue(payload.insertedObjects.isEmpty) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertEqual(payload.refreshedObjects.count, registeredObjectsCount) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: context + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertTrue(Thread.isMainThread) + XCTAssertTrue(payload.managedObjectContext === context) + XCTAssertTrue(payload.insertedObjects.isEmpty) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertEqual(payload.refreshedObjects.count, registeredObjectsCount) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation.fulfill() + } context.refreshAllObjects() @@ -262,10 +278,11 @@ final class NotificationPayloadTests: InMemoryTestCase { } // probably it's not a valid test - func testObserveOnlyInsertionsOnDidChangeUsingBackgroundContextsAndAutomaticallyMergesChangesFromParent() throws { + @MainActor + func test_ObserveOnlyInsertionsOnDidChangeUsingBackgroundContextsAndAutomaticallyMergesChangesFromParent() throws { let backgroundContext1 = container.newBackgroundContext() let backgroundContext2 = container.newBackgroundContext() - backgroundContext2.automaticallyMergesChangesFromParent = true // This cause a change not a save, obviously + backgroundContext2.automaticallyMergesChangesFromParent = true // This cause a change not a save, obviously // From Apple DTS: // Core Data triggers the didChange notification when the context is β€œindeed” changed, or the changes will have impact to you. Here is the logic: @@ -275,19 +292,21 @@ final class NotificationPayloadTests: InMemoryTestCase { let expectation = self.expectation(description: "\(#function)\(#line)") expectation.expectedFulfillmentCount = 1 - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: backgroundContext2) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertFalse(Thread.isMainThread) - XCTAssertTrue(payload.managedObjectContext === backgroundContext2) - XCTAssertEqual(payload.insertedObjects.count, 1) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: backgroundContext2 + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertFalse(Thread.isMainThread) + XCTAssertTrue(payload.managedObjectContext === backgroundContext2) + XCTAssertEqual(payload.insertedObjects.count, 1) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation.fulfill() + } try backgroundContext1.performAndWait { _ in let car = Car(context: backgroundContext1) @@ -308,9 +327,9 @@ final class NotificationPayloadTests: InMemoryTestCase { cancellable.cancel() } - func testObserveMultipleChangesOnMaterializedObjects() throws { + func test_ObserveMultipleChangesOnMaterializedObjects() throws { let viewContext = container.newBackgroundContext() - viewContext.automaticallyMergesChangesFromParent = true // This cause a change not a save, obviously + viewContext.automaticallyMergesChangesFromParent = true // This cause a change not a save, obviously let backgroundContext1 = container.newBackgroundContext() let backgroundContext2 = container.newBackgroundContext() @@ -327,54 +346,55 @@ final class NotificationPayloadTests: InMemoryTestCase { var count = 0 var holds = Set() - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: viewContext) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertTrue(payload.managedObjectContext === viewContext) - switch count { - case 0: - XCTAssertFalse(Thread.isMainThread) - XCTAssertEqual(payload.insertedObjects.count, 1) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - - // To register changes from other contexts, we need to materialize and keep object inserted from other contexts - // otherwise you will receive notifications only for used objects (in this case there are used objects by context0) - payload.insertedObjects.forEach { - $0.willAccessValue(forKey: nil) - holds.insert($0) - } - count += 1 - expectation1.fulfill() - case 1: - XCTAssertFalse(Thread.isMainThread) - XCTAssertEqual(payload.refreshedObjects.count, 1) - count += 1 - expectation2.fulfill() - case 2: - XCTAssertTrue(Thread.isMainThread) - XCTAssertEqual(payload.updatedObjects.count, 1) - count += 1 - expectation3.fulfill() - default: - #if !targetEnvironment(macCatalyst) - // DTS: - // It seems like when β€˜automaticallyMergesChangesFromParent’ is true, Core Data on macOS still merge the changes, - // even though the changes are from the same context, which is not optimized. - // - // FB: - // There are subtle differences in behavior of the runloop between UIApplication and NSApplication. - // Observing just change notifications makes no promises about how many there may be because change notifications are - // posted at the end of the run loop and whenever CoreData feels like it (the application lifecycle spins the main run loop). - // Save notifications get called once per save. - XCTFail("Unexpected change.") - #endif + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: viewContext + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertTrue(payload.managedObjectContext === viewContext) + switch count { + case 0: + XCTAssertFalse(Thread.isMainThread) + XCTAssertEqual(payload.insertedObjects.count, 1) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + + // To register changes from other contexts, we need to materialize and keep object inserted from other contexts + // otherwise you will receive notifications only for used objects (in this case there are used objects by context0) + for object in payload.insertedObjects { + object.willAccessValue(forKey: nil) + holds.insert(object) } + count += 1 + expectation1.fulfill() + case 1: + XCTAssertFalse(Thread.isMainThread) + XCTAssertEqual(payload.refreshedObjects.count, 1) + count += 1 + expectation2.fulfill() + case 2: + XCTAssertTrue(Thread.isMainThread) + XCTAssertEqual(payload.updatedObjects.count, 1) + count += 1 + expectation3.fulfill() + default: + #if !targetEnvironment(macCatalyst) + // DTS: + // It seems like when β€˜automaticallyMergesChangesFromParent’ is true, Core Data on macOS still merge the changes, + // even though the changes are from the same context, which is not optimized. + // + // FB: + // There are subtle differences in behavior of the runloop between UIApplication and NSApplication. + // Observing just change notifications makes no promises about how many there may be because change notifications are + // posted at the end of the run loop and whenever CoreData feels like it (the application lifecycle spins the main run loop). + // Save notifications get called once per save. + XCTFail("Unexpected change.") + #endif } - + } let numberPlate = "123!" try backgroundContext1.performAndWait { _ in @@ -388,7 +408,8 @@ final class NotificationPayloadTests: InMemoryTestCase { wait(for: [expectation1], timeout: 5) try backgroundContext2.performAndWait { _ in - let uniqueCar = try Car.fetchUniqueObject(in: backgroundContext2, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), numberPlate)) + let uniqueCar = try Car.fetchUniqueObject( + in: backgroundContext2, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), numberPlate)) guard let car = uniqueCar else { XCTFail("Car not found") return @@ -400,7 +421,8 @@ final class NotificationPayloadTests: InMemoryTestCase { wait(for: [expectation2], timeout: 5) try viewContext.performAndWait { _ in - let uniqueCar = try Car.fetchUniqueObject(in: viewContext, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), numberPlate)) + let uniqueCar = try Car.fetchUniqueObject( + in: viewContext, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), numberPlate)) guard let car = uniqueCar else { XCTFail("Car not found") return @@ -413,13 +435,14 @@ final class NotificationPayloadTests: InMemoryTestCase { cancellable.cancel() } - func testObserveRefreshesOnMaterializedObjects() throws { + @MainActor + func test_ObserveRefreshesOnMaterializedObjects() throws { let backgroundContext1 = container.newBackgroundContext() let backgroundContext2 = container.newBackgroundContext() // 10 Pandas are created on backgroundContext2 try backgroundContext2.performAndWait { _ in - try (1...10).forEach { numberPlate in + for numberPlate in 1...10 { let car = Car(context: backgroundContext2) car.maker = "FIAT" car.model = "Panda" @@ -435,33 +458,39 @@ final class NotificationPayloadTests: InMemoryTestCase { // 3. Merging updated objects changes the context when the updated objects are in use and not faulted. let viewContext = container.viewContext - viewContext.automaticallyMergesChangesFromParent = true // This cause a change not a save, obviously + viewContext.automaticallyMergesChangesFromParent = true // This cause a change not a save, obviously // We fetch and materialize only 2 Pandas: changes are expected only when they impact these two cars. let fetch = Car.newFetchRequest() - fetch.predicate = NSPredicate(format: "%K IN %@", #keyPath(Car.numberPlate), ["1", "2"] ) + fetch.predicate = NSPredicate(format: "%K IN %@", #keyPath(Car.numberPlate), ["1", "2"]) let cars = try viewContext.fetch(fetch) - cars.forEach { $0.willAccessValue(forKey: nil) } + + for car in cars { + car.willAccessValue(forKey: nil) + } + XCTAssertEqual(cars.count, 2) let expectation1 = self.expectation(description: "DidChange for Panda with number plate: 2") - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: viewContext) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertTrue(payload.managedObjectContext === viewContext) - XCTAssertTrue(payload.insertedObjects.isEmpty) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertEqual(payload.refreshedObjects.count, 1) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation1.fulfill() - } - + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: viewContext + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertTrue(payload.managedObjectContext === viewContext) + XCTAssertTrue(payload.insertedObjects.isEmpty) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertEqual(payload.refreshedObjects.count, 1) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation1.fulfill() + } // car with n. 3, doesn't impact the didChange because it's not materialized in context0 try backgroundContext2.performAndWait { _ in - let uniqueCar = try Car.fetchUniqueObject(in: backgroundContext2, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), "3")) + let uniqueCar = try Car.fetchUniqueObject( + in: backgroundContext2, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), "3")) guard let car = uniqueCar else { XCTFail("Car not found") return @@ -472,7 +501,8 @@ final class NotificationPayloadTests: InMemoryTestCase { // car with n. 6, doesn't impact the didChange because it's not materialized in context0 try backgroundContext1.performAndWait { _ in - let uniqueCar = try Car.fetchUniqueObject(in: backgroundContext1, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), "6")) + let uniqueCar = try Car.fetchUniqueObject( + in: backgroundContext1, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), "6")) guard let car = uniqueCar else { XCTFail("Car not found") return @@ -483,7 +513,8 @@ final class NotificationPayloadTests: InMemoryTestCase { // car with n. 2, impact the didChange because it's materialized in context0 try backgroundContext2.performAndWait { _ in - let uniqueCar = try Car.fetchUniqueObject(in: backgroundContext2, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), "2")) + let uniqueCar = try Car.fetchUniqueObject( + in: backgroundContext2, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), "2")) guard let car = uniqueCar else { XCTFail("Car not found") return @@ -498,7 +529,8 @@ final class NotificationPayloadTests: InMemoryTestCase { // MARK: - NSManagedObjectContextWillSave and NSManagedObjectContextDidSave - func testObserveInsertionsOnWillSaveNotification() throws { + @MainActor + func test_ObserveInsertionsOnWillSaveNotification() throws { let context = container.viewContext let expectation = self.expectation(description: "\(#function)\(#line)") let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextWillSave, object: context) @@ -520,7 +552,8 @@ final class NotificationPayloadTests: InMemoryTestCase { cancellable.cancel() } - func testObserveInsertionsOnDidSaveNotification() throws { + @MainActor + func test_ObserveInsertionsOnDidSaveNotification() throws { let context = container.viewContext var cancellables = [AnyCancellable]() @@ -534,27 +567,23 @@ final class NotificationPayloadTests: InMemoryTestCase { XCTAssertEqual(payload.insertedObjects.count, 2) XCTAssertTrue(payload.deletedObjects.isEmpty) XCTAssertTrue(payload.updatedObjects.isEmpty) - if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) { - XCTAssertNil(payload.queryGenerationToken, "Query Generation Token is available only on SQLite stores.") - } + XCTAssertNil(payload.queryGenerationToken, "Query Generation Token is available only on SQLite stores.") expectation.fulfill() } .store(in: &cancellables) - if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) { - let expectation2 = self.expectation(description: "\(#function)\(#line)") - expectation2.assertForOverFulfill = false - NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSaveObjectIDs, object: context) - .map { ManagedObjectContextDidSaveObjectIDs(notification: $0) } - .sink { payload in - XCTAssertTrue(payload.managedObjectContext === context) - XCTAssertEqual(payload.insertedObjectIDs.count, 2) - XCTAssertTrue(payload.deletedObjectIDs.isEmpty) - XCTAssertTrue(payload.updatedObjectIDs.isEmpty) - expectation2.fulfill() - } - .store(in: &cancellables) - } + let expectation2 = self.expectation(description: "\(#function)\(#line)") + expectation2.assertForOverFulfill = false + NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSaveObjectIDs, object: context) + .map { ManagedObjectContextDidSaveObjectIDs(notification: $0) } + .sink { payload in + XCTAssertTrue(payload.managedObjectContext === context) + XCTAssertEqual(payload.insertedObjectIDs.count, 2) + XCTAssertTrue(payload.deletedObjectIDs.isEmpty) + XCTAssertTrue(payload.updatedObjectIDs.isEmpty) + expectation2.fulfill() + } + .store(in: &cancellables) let car = Car(context: context) car.maker = "FIAT" @@ -568,10 +597,14 @@ final class NotificationPayloadTests: InMemoryTestCase { try context.save() waitForExpectations(timeout: 2) - cancellables.forEach { $0.cancel() } + + for cancellable in cancellables { + cancellable.cancel() + } } - func testObserveInsertionsUpdatesAndDeletesOnDidSaveNotification() throws { + @MainActor + func test_ObserveInsertionsUpdatesAndDeletesOnDidSaveNotification() throws { let context = container.viewContext var cancellables = [AnyCancellable]() @@ -603,20 +636,18 @@ final class NotificationPayloadTests: InMemoryTestCase { } .store(in: &cancellables) - if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) { - let expectation2 = self.expectation(description: "\(#function)\(#line)") - expectation2.assertForOverFulfill = false - NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSaveObjectIDs, object: context) - .map { ManagedObjectContextDidSaveObjectIDs(notification: $0) } - .sink { payload in - XCTAssertTrue(payload.managedObjectContext === context) - XCTAssertEqual(payload.insertedObjectIDs.count, 2) - XCTAssertEqual(payload.deletedObjectIDs.count, 1) - XCTAssertEqual(payload.updatedObjectIDs.count, 1) - expectation2.fulfill() - } - .store(in: &cancellables) - } + let expectation2 = self.expectation(description: "\(#function)\(#line)") + expectation2.assertForOverFulfill = false + NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSaveObjectIDs, object: context) + .map { ManagedObjectContextDidSaveObjectIDs(notification: $0) } + .sink { payload in + XCTAssertTrue(payload.managedObjectContext === context) + XCTAssertEqual(payload.insertedObjectIDs.count, 2) + XCTAssertEqual(payload.deletedObjectIDs.count, 1) + XCTAssertEqual(payload.updatedObjectIDs.count, 1) + expectation2.fulfill() + } + .store(in: &cancellables) // 2 inserts let car3 = Car(context: context) @@ -638,12 +669,16 @@ final class NotificationPayloadTests: InMemoryTestCase { try context.save() waitForExpectations(timeout: 2) - cancellables.forEach { $0.cancel() } + + for cancellable in cancellables { + cancellable.cancel() + } } - func testObserveMultipleChangesUsingPersistentStoreCoordinatorWithChildAndParentContexts() throws { + @MainActor + func test_ObserveMultipleChangesUsingPersistentStoreCoordinatorWithChildAndParentContexts() throws { // Given - let psc = NSPersistentStoreCoordinator(managedObjectModel: model) + let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) let storeURL = URL.newDatabaseURL(withID: UUID()) try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil) @@ -684,48 +719,50 @@ final class NotificationPayloadTests: InMemoryTestCase { // Changes are propagated from the child to the parent during the save. var count = 0 - let cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: parentContext) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - XCTAssertTrue(Thread.isMainThread) - if count == 0 { - XCTAssertTrue(payload.managedObjectContext === parentContext) - XCTAssertEqual(payload.insertedObjects.count, 2) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - count += 1 - } else if count == 1 { - XCTAssertTrue(payload.managedObjectContext === parentContext) - XCTAssertTrue(payload.insertedObjects.isEmpty) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertEqual(payload.updatedObjects.count, 1) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - count += 1 - } else if count == 2 { - XCTAssertTrue(payload.managedObjectContext === parentContext) - XCTAssertTrue(payload.insertedObjects.isEmpty) - XCTAssertEqual(payload.deletedObjects.count, 1) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - count += 1 - } else if count == 3 { - XCTAssertTrue(payload.managedObjectContext === parentContext) - XCTAssertEqual(payload.insertedObjects.count, 1) - XCTAssertTrue(payload.deletedObjects.isEmpty) - XCTAssertTrue(payload.updatedObjects.isEmpty) - XCTAssertTrue(payload.refreshedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedObjects.isEmpty) - XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) - expectation.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: .NSManagedObjectContextObjectsDidChange, object: parentContext + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + XCTAssertTrue(Thread.isMainThread) + if count == 0 { + XCTAssertTrue(payload.managedObjectContext === parentContext) + XCTAssertEqual(payload.insertedObjects.count, 2) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + count += 1 + } else if count == 1 { + XCTAssertTrue(payload.managedObjectContext === parentContext) + XCTAssertTrue(payload.insertedObjects.isEmpty) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertEqual(payload.updatedObjects.count, 1) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + count += 1 + } else if count == 2 { + XCTAssertTrue(payload.managedObjectContext === parentContext) + XCTAssertTrue(payload.insertedObjects.isEmpty) + XCTAssertEqual(payload.deletedObjects.count, 1) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + count += 1 + } else if count == 3 { + XCTAssertTrue(payload.managedObjectContext === parentContext) + XCTAssertEqual(payload.insertedObjects.count, 1) + XCTAssertTrue(payload.deletedObjects.isEmpty) + XCTAssertTrue(payload.updatedObjects.isEmpty) + XCTAssertTrue(payload.refreshedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedObjects.isEmpty) + XCTAssertTrue(payload.invalidatedAllObjects.isEmpty) + expectation.fulfill() } + } let cancellable2 = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: parentContext) .map { ManagedObjectContextDidSaveObjects(notification: $0) } @@ -754,7 +791,8 @@ final class NotificationPayloadTests: InMemoryTestCase { } try childContext.performAndWait { _ in - let uniqueCar1 = try Car.fetchUniqueObject(in: childContext, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), car1Plate)) + let uniqueCar1 = try Car.fetchUniqueObject( + in: childContext, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), car1Plate)) guard let car1 = uniqueCar1 else { XCTFail("Car not found.") return @@ -766,7 +804,8 @@ final class NotificationPayloadTests: InMemoryTestCase { } try childContext.performAndWait { _ in - let uniqueCar2 = try Car.fetchUniqueObject(in: childContext, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), car2Plate)) + let uniqueCar2 = try Car.fetchUniqueObject( + in: childContext, where: NSPredicate(format: "%K == %@", #keyPath(Car.numberPlate), car2Plate)) guard let car2 = uniqueCar2 else { XCTFail("Car not found.") return @@ -784,12 +823,11 @@ final class NotificationPayloadTests: InMemoryTestCase { } try parentContext.performAndWait { _ in - try parentContext.save() // triggers the didSave event + try parentContext.save() // triggers the didSave event } waitForExpectations(timeout: 10) - // cleaning stuff let store = psc.persistentStores.first! try psc.remove(store) @@ -800,44 +838,50 @@ final class NotificationPayloadTests: InMemoryTestCase { // MARK: - Entity Observer Example - func testObserveInsertedOnDidChangeEventForSpecificEntities() { + @MainActor + func test_ObserveInsertedOnDidChangeEventForSpecificEntities() { let context = container.viewContext let expectation1 = expectation(description: "\(#function)\(#line)") // Attention: sometimes entity() returns nil due to a CoreData bug occurring in the Unit Test targets or when Generics are used. // let entity = NSEntityDescription.entity(forEntityName: type.entity().name!, in: context)! - func findObjectsOfType(_ type: T.Type, in objects: Set, observeSubEntities: Bool = true) -> Set { + func findObjectsOfType( + _ type: T.Type, in objects: Set, observeSubEntities: Bool = true + ) -> Set { let entity = type.entity() if observeSubEntities { - return objects.filter { $0.entity.isDescendantEntity(of: entity, recursive: true) || $0.entity == entity } as? Set ?? [] + return objects.filter { $0.entity.isDescendantEntity(of: entity, recursive: true) || $0.entity == entity } + as? Set ?? [] } else { return objects.filter { $0.entity == entity } as? Set ?? [] } } - let cancellable = NotificationCenter.default.publisher(for: Notification.Name.NSManagedObjectContextObjectsDidChange, object: context) - .map { ManagedObjectContextObjectsDidChange(notification: $0) } - .sink { payload in - let inserts = findObjectsOfType(SportCar.self, in: payload.insertedObjects, observeSubEntities: true) - let inserts2 = findObjectsOfType(Car.self, in: payload.insertedObjects, observeSubEntities: true) - let inserts3 = findObjectsOfType(Car.self, in: payload.insertedObjects, observeSubEntities: false) - let deletes = findObjectsOfType(SportCar.self, in: payload.deletedObjects, observeSubEntities: true) - let udpates = findObjectsOfType(SportCar.self, in: payload.updatedObjects, observeSubEntities: true) - let refreshes = findObjectsOfType(SportCar.self, in: payload.refreshedObjects, observeSubEntities: true) - let invalidates = findObjectsOfType(SportCar.self, in: payload.invalidatedObjects, observeSubEntities: true) - let invalidatesAll = payload.invalidatedAllObjects.filter { $0.entity == SportCar.entity() } - - XCTAssertEqual(inserts.count, 1) - XCTAssertEqual(inserts2.count, 2) - XCTAssertEqual(inserts3.count, 1) - XCTAssertTrue(deletes.isEmpty) - XCTAssertTrue(udpates.isEmpty) - XCTAssertTrue(refreshes.isEmpty) - XCTAssertTrue(invalidates.isEmpty) - XCTAssertTrue(invalidatesAll.isEmpty) - expectation1.fulfill() - } + let cancellable = NotificationCenter.default.publisher( + for: Notification.Name.NSManagedObjectContextObjectsDidChange, object: context + ) + .map { ManagedObjectContextObjectsDidChange(notification: $0) } + .sink { payload in + let inserts = findObjectsOfType(SportCar.self, in: payload.insertedObjects, observeSubEntities: true) + let inserts2 = findObjectsOfType(Car.self, in: payload.insertedObjects, observeSubEntities: true) + let inserts3 = findObjectsOfType(Car.self, in: payload.insertedObjects, observeSubEntities: false) + let deletes = findObjectsOfType(SportCar.self, in: payload.deletedObjects, observeSubEntities: true) + let udpates = findObjectsOfType(SportCar.self, in: payload.updatedObjects, observeSubEntities: true) + let refreshes = findObjectsOfType(SportCar.self, in: payload.refreshedObjects, observeSubEntities: true) + let invalidates = findObjectsOfType(SportCar.self, in: payload.invalidatedObjects, observeSubEntities: true) + let invalidatesAll = payload.invalidatedAllObjects.filter { $0.entity == SportCar.entity() } + + XCTAssertEqual(inserts.count, 1) + XCTAssertEqual(inserts2.count, 2) + XCTAssertEqual(inserts3.count, 1) + XCTAssertTrue(deletes.isEmpty) + XCTAssertTrue(udpates.isEmpty) + XCTAssertTrue(refreshes.isEmpty) + XCTAssertTrue(invalidates.isEmpty) + XCTAssertTrue(invalidatesAll.isEmpty) + expectation1.fulfill() + } let sportCar = SportCar(context: context) sportCar.maker = "McLaren" @@ -859,7 +903,8 @@ final class NotificationPayloadTests: InMemoryTestCase { // MARK: - NSPersistentStoreRemoteChange - func testInvestigationPersistentStoreRemoteChangeAndSave() throws { + @MainActor + func test_InvestigationPersistentStoreRemoteChangeAndSave() throws { // Cross coordinators change notifications: let id = UUID() @@ -872,30 +917,34 @@ final class NotificationPayloadTests: InMemoryTestCase { viewContext1.transactionAuthor = "author1" let expectation1 = expectation(description: "NSPersistentStoreRemoteChange Notification sent by container1") - let cancellable1 = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange, object: container1.persistentStoreCoordinator) - .map { PersistentStoreRemoteChange(notification: $0) } - .sink { payload in - XCTAssertNotNil(payload.historyToken) - XCTAssertNotNil(payload.storeUUID) - let uuidString = container1.persistentStoreCoordinator.persistentStores.first?.metadata[NSStoreUUIDKey] as? String - XCTAssertNotNil(uuidString) - XCTAssertEqual(uuidString!, payload.storeUUID.uuidString) - XCTAssertEqual(payload.storeURL, container1.persistentStoreCoordinator.persistentStores.first?.url) - expectation1.fulfill() - } + let cancellable1 = NotificationCenter.default.publisher( + for: .NSPersistentStoreRemoteChange, object: container1.persistentStoreCoordinator + ) + .map { PersistentStoreRemoteChange(notification: $0) } + .sink { payload in + XCTAssertNotNil(payload.historyToken) + XCTAssertNotNil(payload.storeUUID) + let uuidString = container1.persistentStoreCoordinator.persistentStores.first?.metadata[NSStoreUUIDKey] as? String + XCTAssertNotNil(uuidString) + XCTAssertEqual(uuidString!, payload.storeUUID.uuidString) + XCTAssertEqual(payload.storeURL, container1.persistentStoreCoordinator.persistentStores.first?.url) + expectation1.fulfill() + } let expectation2 = expectation(description: "NSPersistentStoreRemoteChange Notification sent by container2") - let cancellable2 = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange, object: container2.persistentStoreCoordinator) - .map { PersistentStoreRemoteChange(notification: $0) } - .sink { payload in - XCTAssertNotNil(payload.historyToken) - XCTAssertNotNil(payload.storeUUID) - let uuidString = container2.persistentStoreCoordinator.persistentStores.first?.metadata[NSStoreUUIDKey] as? String - XCTAssertNotNil(uuidString) - XCTAssertEqual(uuidString!, payload.storeUUID.uuidString) - XCTAssertEqual(payload.storeURL, container2.persistentStoreCoordinator.persistentStores.first?.url) - expectation2.fulfill() - } + let cancellable2 = NotificationCenter.default.publisher( + for: .NSPersistentStoreRemoteChange, object: container2.persistentStoreCoordinator + ) + .map { PersistentStoreRemoteChange(notification: $0) } + .sink { payload in + XCTAssertNotNil(payload.historyToken) + XCTAssertNotNil(payload.storeUUID) + let uuidString = container2.persistentStoreCoordinator.persistentStores.first?.metadata[NSStoreUUIDKey] as? String + XCTAssertNotNil(uuidString) + XCTAssertEqual(uuidString!, payload.storeUUID.uuidString) + XCTAssertEqual(payload.storeURL, container2.persistentStoreCoordinator.persistentStores.first?.url) + expectation2.fulfill() + } let car = Car(context: viewContext1) car.maker = "FIAT" @@ -908,7 +957,8 @@ final class NotificationPayloadTests: InMemoryTestCase { cancellable2.cancel() } - func testInvestigationPersistentStoreRemoteChangeAndBatchOperations() throws { + @MainActor + func test_InvestigationPersistentStoreRemoteChangeAndBatchOperations() throws { // Cross coordinators change notifications: // This notification notifies when history has been made even when batch operations are done. @@ -922,34 +972,40 @@ final class NotificationPayloadTests: InMemoryTestCase { viewContext1.transactionAuthor = "author1" let expectation1 = expectation(description: "NSPersistentStoreRemoteChange Notification sent by container1") - let cancellable1 = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange, object: container1.persistentStoreCoordinator) - .map { PersistentStoreRemoteChange(notification: $0) } - .sink { payload in - XCTAssertNotNil(payload.historyToken) - XCTAssertNotNil(payload.storeUUID) - let uuidString = container1.persistentStoreCoordinator.persistentStores.first?.metadata[NSStoreUUIDKey] as? String - XCTAssertNotNil(uuidString) - XCTAssertEqual(uuidString!, payload.storeUUID.uuidString) - XCTAssertEqual(payload.storeURL, container1.persistentStoreCoordinator.persistentStores.first?.url) - expectation1.fulfill() - } + let cancellable1 = NotificationCenter.default.publisher( + for: .NSPersistentStoreRemoteChange, object: container1.persistentStoreCoordinator + ) + .map { PersistentStoreRemoteChange(notification: $0) } + .sink { payload in + XCTAssertNotNil(payload.historyToken) + XCTAssertNotNil(payload.storeUUID) + let uuidString = container1.persistentStoreCoordinator.persistentStores.first?.metadata[NSStoreUUIDKey] as? String + XCTAssertNotNil(uuidString) + XCTAssertEqual(uuidString!, payload.storeUUID.uuidString) + XCTAssertEqual(payload.storeURL, container1.persistentStoreCoordinator.persistentStores.first?.url) + expectation1.fulfill() + } let expectation2 = expectation(description: "NSPersistentStoreRemoteChange Notification sent by container2") - let cancellable2 = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange, object: container2.persistentStoreCoordinator) - .map { PersistentStoreRemoteChange(notification: $0) } - .sink { payload in - XCTAssertNotNil(payload.historyToken) - XCTAssertNotNil(payload.storeUUID) - let uuidString = container2.persistentStoreCoordinator.persistentStores.first?.metadata[NSStoreUUIDKey] as? String - XCTAssertNotNil(uuidString) - XCTAssertEqual(uuidString!, payload.storeUUID.uuidString) - XCTAssertEqual(payload.storeURL, container2.persistentStoreCoordinator.persistentStores.first?.url) - expectation2.fulfill() - } + let cancellable2 = NotificationCenter.default.publisher( + for: .NSPersistentStoreRemoteChange, object: container2.persistentStoreCoordinator + ) + .map { PersistentStoreRemoteChange(notification: $0) } + .sink { payload in + XCTAssertNotNil(payload.historyToken) + XCTAssertNotNil(payload.storeUUID) + let uuidString = container2.persistentStoreCoordinator.persistentStores.first?.metadata[NSStoreUUIDKey] as? String + XCTAssertNotNil(uuidString) + XCTAssertEqual(uuidString!, payload.storeUUID.uuidString) + XCTAssertEqual(payload.storeURL, container2.persistentStoreCoordinator.persistentStores.first?.url) + expectation2.fulfill() + } - let object = [#keyPath(Car.maker): "FIAT", - #keyPath(Car.numberPlate): "123", - #keyPath(Car.model): "Panda"] + let object = [ + #keyPath(Car.maker): "FIAT", + #keyPath(Car.numberPlate): "123", + #keyPath(Car.model): "Panda", + ] let result = try Car.batchInsert(using: viewContext1, resultType: .count, objects: [object]) XCTAssertEqual(result.count!, 1) @@ -960,9 +1016,9 @@ final class NotificationPayloadTests: InMemoryTestCase { } } -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) final class NotificationPayloadOnDiskTests: OnDiskTestCase { - func testObserveInsertionsOnDidSaveNotification() throws { + @MainActor + func test_ObserveInsertionsOnDidSaveNotification() throws { let context = container.viewContext try context.setQueryGenerationFrom(.current) var cancellables = [AnyCancellable]() @@ -977,29 +1033,25 @@ final class NotificationPayloadOnDiskTests: OnDiskTestCase { XCTAssertEqual(payload.insertedObjects.count, 2) XCTAssertTrue(payload.deletedObjects.isEmpty) XCTAssertTrue(payload.updatedObjects.isEmpty) - if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) { - // This test is primarly used to test the queryGenerationToken object in the notification payload - XCTAssertNotNil(payload.queryGenerationToken) - } + // This test is primarly used to test the queryGenerationToken object in the notification payload + XCTAssertNotNil(payload.queryGenerationToken) expectation.fulfill() } .store(in: &cancellables) - if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) { - let expectation2 = self.expectation(description: "\(#function)\(#line)") - expectation2.assertForOverFulfill = false - NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSaveObjectIDs, object: context) - .map { ManagedObjectContextDidSaveObjectIDs(notification: $0) } - .sink { payload in - XCTAssertTrue(payload.managedObjectContext === context) - XCTAssertEqual(payload.insertedObjectIDs.count, 2) - XCTAssertTrue(payload.deletedObjectIDs.isEmpty) - XCTAssertTrue(payload.updatedObjectIDs.isEmpty) - XCTAssertTrue(payload.insertedObjectIDs.allSatisfy { !$0.isTemporaryID }) - expectation2.fulfill() - } - .store(in: &cancellables) - } + let expectation2 = self.expectation(description: "\(#function)\(#line)") + expectation2.assertForOverFulfill = false + NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSaveObjectIDs, object: context) + .map { ManagedObjectContextDidSaveObjectIDs(notification: $0) } + .sink { payload in + XCTAssertTrue(payload.managedObjectContext === context) + XCTAssertEqual(payload.insertedObjectIDs.count, 2) + XCTAssertTrue(payload.deletedObjectIDs.isEmpty) + XCTAssertTrue(payload.updatedObjectIDs.isEmpty) + XCTAssertTrue(payload.insertedObjectIDs.allSatisfy { !$0.isTemporaryID }) + expectation2.fulfill() + } + .store(in: &cancellables) let car = Car(context: context) car.maker = "FIAT" @@ -1011,37 +1063,39 @@ final class NotificationPayloadOnDiskTests: OnDiskTestCase { car2.model = "Panda" car2.numberPlate = "2" - print(car.objectID, car2.objectID) + //print(car.objectID, car2.objectID) try context.save() waitForExpectations(timeout: 2) - cancellables.forEach { $0.cancel() } + + for cancellable in cancellables { + cancellable.cancel() + } } - func testInvestigationInsertionsInChildContextOnDidSaveNotification() throws { + @MainActor + func test_InvestigationInsertionsInChildContextOnDidSaveNotification() throws { // the scope of this test is to verify wheter or not a NSManagedObjectContextDidSaveObjectIDs notification // fired in a child context will have insertedObjectIDs with temporary IDs (expected) let context = container.viewContext let childViewContext = context.newChildContext(concurrencyType: .mainQueueConcurrencyType) var cancellables = [AnyCancellable]() - if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) { - let expectation2 = self.expectation(description: "\(#function)\(#line)") - expectation2.assertForOverFulfill = false - NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSaveObjectIDs, object: childViewContext) - .map { - ManagedObjectContextDidSaveObjectIDs(notification: $0) - } - .sink { payload in - XCTAssertTrue(payload.managedObjectContext === childViewContext) - XCTAssertEqual(payload.insertedObjectIDs.count, 2) - XCTAssertTrue(payload.deletedObjectIDs.isEmpty) - // we expect to have temporary IDs in the notification - XCTAssertTrue(payload.insertedObjectIDs.allSatisfy { $0.isTemporaryID }) - expectation2.fulfill() - } - .store(in: &cancellables) - } + let expectation2 = self.expectation(description: "\(#function)\(#line)") + expectation2.assertForOverFulfill = false + NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSaveObjectIDs, object: childViewContext) + .map { + ManagedObjectContextDidSaveObjectIDs(notification: $0) + } + .sink { payload in + XCTAssertTrue(payload.managedObjectContext === childViewContext) + XCTAssertEqual(payload.insertedObjectIDs.count, 2) + XCTAssertTrue(payload.deletedObjectIDs.isEmpty) + // we expect to have temporary IDs in the notification + XCTAssertTrue(payload.insertedObjectIDs.allSatisfy { $0.isTemporaryID }) + expectation2.fulfill() + } + .store(in: &cancellables) let car = Car(context: childViewContext) car.maker = "FIAT" @@ -1055,11 +1109,14 @@ final class NotificationPayloadOnDiskTests: OnDiskTestCase { try childViewContext.save() waitForExpectations(timeout: 5) - cancellables.forEach { $0.cancel() } + + for cancellable in cancellables { + cancellable.cancel() + } } - func testInvestigationNSPersistentStoreCoordinatorStoresNotifications() throws { - let psc = NSPersistentStoreCoordinator(managedObjectModel: model) + func test_InvestigationNSPersistentStoreCoordinatorStoresNotifications() throws { + let psc = NSPersistentStoreCoordinator(managedObjectModel: model1) let initialStoreURL = URL.newDatabaseURL(withID: UUID()) let finalStoreURL = URL.newDatabaseURL(withID: UUID()) var cancellables = [AnyCancellable]() @@ -1067,25 +1124,23 @@ final class NotificationPayloadOnDiskTests: OnDiskTestCase { NotificationCenter.default.publisher(for: .NSPersistentStoreCoordinatorStoresWillChange, object: nil) .sink { notification in XCTFail("AFAIK this notification is sent only for deprecated settings for CoreDataUbiquitySupport.") - /* - Sample of a notification.userInfo (generated from anothr project): - - β–Ώ 3 elements - β–Ώ 0 : 2 elements - β–Ώ key : AnyHashable("removed") - - value : "removed" - β–Ώ value : 1 element - - 0 : (URL: file:///var/mobile/Containers/Data/Application/A926CA73-AF4D-44E8-ADE5-246ED7F20D7B/Documents/CoreDataUbiquitySupport/mobile~18720936-2A3A-4C6F-BF8E-DF042ED5A917/MY_NAME/D26F608C-F8AC-4E51-AF6C-007B7DC56B7E/store/db.sqlite) - β–Ώ 1 : 2 elements - β–Ώ key : AnyHashable("NSPersistentStoreUbiquitousTransitionTypeKey") - - value : "NSPersistentStoreUbiquitousTransitionTypeKey" - - value : 4 - β–Ώ 2 : 2 elements - β–Ώ key : AnyHashable("added") - - value : "added" - β–Ώ value : 1 element - - 0 : (URL: file:///var/mobile/Containers/Data/Application/A926CA73-AF4D-44E8-ADE5-246ED7F20D7B/Documents/CoreDataUbiquitySupport/mobile~18720936-2A3A-4C6F-BF8E-DF042ED5A917/MY_NAME/D26F608C-F8AC-4E51-AF6C-007B7DC56B7E/store/db.sqlite) - */ + // Sample of a notification.userInfo (generated from anothr project): + // + // β–Ώ 3 elements + // β–Ώ 0 : 2 elements + // β–Ώ key : AnyHashable("removed") + // - value : "removed" + // β–Ώ value : 1 element + // - 0 : (URL: file:///var/mobile/Containers/Data/Application/A926CA73-AF4D-44E8-ADE5-246ED7F20D7B/Documents/CoreDataUbiquitySupport/mobile~18720936-2A3A-4C6F-BF8E-DF042ED5A917/MY_NAME/D26F608C-F8AC-4E51-AF6C-007B7DC56B7E/store/db.sqlite) + // β–Ώ 1 : 2 elements + // β–Ώ key : AnyHashable("NSPersistentStoreUbiquitousTransitionTypeKey") + // - value : "NSPersistentStoreUbiquitousTransitionTypeKey" + // - value : 4 + // β–Ώ 2 : 2 elements + // β–Ώ key : AnyHashable("added") + // - value : "added" + // β–Ώ value : 1 element + // - 0 : (URL: file:///var/mobile/Containers/Data/Application/A926CA73-AF4D-44E8-ADE5-246ED7F20D7B/Documents/CoreDataUbiquitySupport/mobile~18720936-2A3A-4C6F-BF8E-DF042ED5A917/MY_NAME/D26F608C-F8AC-4E51-AF6C-007B7DC56B7E/store/db.sqlite) }.store(in: &cancellables) NotificationCenter.default.publisher(for: .NSPersistentStoreCoordinatorStoresDidChange, object: nil) @@ -1108,27 +1163,20 @@ final class NotificationPayloadOnDiskTests: OnDiskTestCase { XCTAssertEqual(changedStore.newStore.url, finalStoreURL) XCTAssertEqual(changedStore.migratedIDs.count, 4) - /* - Sample Log - - ➑️ ObjectIDs after save in the old store - - 0xc4549980eaebab6a // A - 0xc4549980eae7ab6a // B + // Sample Log + // ➑️ ObjectIDs after save in the old store + // 0xc4549980eaebab6a // A + // 0xc4549980eae7ab6a // B + // ➑️ ObjectIDs returned as third element by the NSPersistentStoreCoordinatorStoresDidChange + // It contains both the old and new ObjectIds in sequence. + // 0xc4549980eaebab6a // A + // 0xc4549980eae7ab6e // A1 + // 0xc4549980eae7ab6a // B + // 0xc4549980eaebab6e // B1 + // ➑️ ObjectIDs after fetch with the new store + // 0xc4549980eae7ab6e // A1 + // 0xc4549980eaebab6e // B1 - ➑️ ObjectIDs returned as third element by the NSPersistentStoreCoordinatorStoresDidChange - It contains both the old and new ObjectIds in sequence. - - 0xc4549980eaebab6a // A - 0xc4549980eae7ab6e // A1 - 0xc4549980eae7ab6a // B - 0xc4549980eaebab6e // B1 - - ➑️ ObjectIDs after fetch with the new store - - 0xc4549980eae7ab6e // A1 - 0xc4549980eaebab6e // B1 - */ } else if let removedStore = payload.removedStores.first { if removedStore.url == initialStoreURL { // 6 @@ -1179,18 +1227,17 @@ final class NotificationPayloadOnDiskTests: OnDiskTestCase { // triggers a NSPersistentStoreCoordinatorStoresDidChange with a NSUUIDChangedPersistentStoresKey (5) // triggers a NSPersistentStoreCoordinatorWillRemoveStore (3) for initial URL // triggers a NSPersistentStoreCoordinatorStoresDidChange (6) - print("migratePersistentStore") - try psc.persistentStores.forEach { - try psc.migratePersistentStore($0, to: finalStoreURL, options: nil, withType: NSSQLiteStoreType) + + for store in psc.persistentStores { + try psc.migratePersistentStore(store, to: finalStoreURL, options: nil, withType: NSSQLiteStoreType) } // let people = try Person.fetch(in: context) { $0.sortDescriptors = [NSSortDescriptor(key: #keyPath(Person.firstName), ascending: false)] } // used only to create the sample log // triggers a NSPersistentStoreCoordinatorWillRemoveStore (4) for final URL // triggers a NSPersistentStoreCoordinatorStoresDidChange (7) - print("remove") - try psc.persistentStores.forEach { - try psc.remove($0) + for store in psc.persistentStores { + try psc.remove(store) } context._fix_sqlite_warning_when_destroying_a_store() diff --git a/Tests/ProgrammaticMigrationTests.swift b/Tests/ProgrammaticMigration_Tests.swift similarity index 72% rename from Tests/ProgrammaticMigrationTests.swift rename to Tests/ProgrammaticMigration_Tests.swift index f089f2e6..525b942e 100644 --- a/Tests/ProgrammaticMigrationTests.swift +++ b/Tests/ProgrammaticMigration_Tests.swift @@ -1,21 +1,22 @@ // CoreDataPlus -import XCTest import CoreData import Foundation +import XCTest +import os.lock + @testable import CoreDataPlus -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -final class ProgrammaticMigrationTests: XCTestCase { +final class ProgrammaticMigration_Tests: XCTestCase { - func testInferringMappingModelFromV1toV2() throws { - let mappingModel = try XCTUnwrap(SampleModel2.SampleModel2Version.version1.inferredMappingModelToNextModelVersion()) + func test_InferringMappingModelFromV1toV2() throws { + let mappingModel = try XCTUnwrap(SampleModel2.SampleModelVersion2.version1.inferredMappingModelToNextModelVersion()) XCTAssertTrue(mappingModel.isInferred) let mappings = try XCTUnwrap(mappingModel.entityMappings) - let authorMappingModel = try XCTUnwrap(mappings.first(where:{ $0.sourceEntityName == "Author" })) + let authorMappingModel = try XCTUnwrap(mappings.first(where: { $0.sourceEntityName == "Author" })) XCTAssertEqual(authorMappingModel.mappingType, .transformEntityMappingType) - let bookMappingModel = try XCTUnwrap(mappings.first(where:{ $0.sourceEntityName == "Book" })) + let bookMappingModel = try XCTUnwrap(mappings.first(where: { $0.sourceEntityName == "Book" })) XCTAssertEqual(bookMappingModel.mappingType, .transformEntityMappingType) do { @@ -32,7 +33,8 @@ final class ProgrammaticMigrationTests: XCTestCase { } do { - XCTAssertNotNil(bookMappingModel.attributeMappings?.first(where: { $0.name == "frontCover" })) // this is as far I can go + // this is as far I can go + XCTAssertNotNil(bookMappingModel.attributeMappings?.first(where: { $0.name == "frontCover" })) let mappingProperties = try XCTUnwrap(bookMappingModel.mappingProperties) XCTAssertEqual(mappingProperties.mappedProperties.count, 8) @@ -43,20 +45,23 @@ final class ProgrammaticMigrationTests: XCTestCase { } } - func testMigrationFromV1ToV2() throws { + func test_MigrationFromV1ToV2() throws { let url = URL.newDatabaseURL(withID: UUID()) + // the migration works fine even if NSMigratePersistentStoresAutomaticallyOption is set to true, + // but it should be false let options = [ - NSMigratePersistentStoresAutomaticallyOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, NSInferMappingModelAutomaticallyOption: false, - NSPersistentHistoryTrackingKey: true, // ⚠️ cannot be changed once set to true - NSPersistentHistoryTokenKey: true + NSPersistentHistoryTrackingKey: false, // ⚠️ cannot be changed once set to true + NSPersistentHistoryTokenKey: true, ] let description = NSPersistentStoreDescription(url: url) description.configuration = nil - description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) - description.setOption(true as NSNumber, forKey: NSPersistentHistoryTokenKey) + for (key, value) in options { + description.setOption(value as NSObject, forKey: key) + } let oldManagedObjectModel = V1.makeManagedObjectModel() let coordinator = NSPersistentStoreCoordinator(managedObjectModel: oldManagedObjectModel) @@ -76,20 +81,22 @@ final class ProgrammaticMigrationTests: XCTestCase { // Migration let sourceDescription = NSPersistentStoreDescription(url: url) let destinationDescription = NSPersistentStoreDescription(url: url) - options.forEach { key, value in + for (key, value) in options { sourceDescription.setOption(value as NSObject, forKey: key) destinationDescription.setOption(value as NSObject, forKey: key) } - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: .version2) + let newOptions = destinationDescription.options + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: .version2) try migrator.migrate(enableWALCheckpoint: true) // Validation let newManagedObjectModel = V2.makeManagedObjectModel() let newCoordinator = NSPersistentStoreCoordinator(managedObjectModel: newManagedObjectModel) - try newCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: options) + _ = try newCoordinator.addPersistentStore(type: .sqlite, configuration: nil, at: url, options: newOptions) let newContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) newContext.persistentStoreCoordinator = newCoordinator @@ -97,7 +104,8 @@ final class ProgrammaticMigrationTests: XCTestCase { let authorRequest = NSFetchRequest(entityName: "Author") as! NSFetchRequest let authors = try newContext.fetch(authorRequest) XCTAssertEqual(authors.count, 2) - authors.forEach { object in + + for object in authors { object.materialize() object.alias += "-" } @@ -105,7 +113,7 @@ final class ProgrammaticMigrationTests: XCTestCase { let bookRequest = NSFetchRequest(entityName: "Book") let books = try newContext.fetch(bookRequest) XCTAssertEqual(books.count, 52) - books.forEach { object in + for object in books { object.materialize() XCTAssertNotNil(object.value(forKey: #keyPath(BookV2.frontCover))) } @@ -118,19 +126,21 @@ final class ProgrammaticMigrationTests: XCTestCase { //try FileManager.default.removeItem(at: url) } - func testMigrationFromV1ToV2WithMultipleStores() throws { + func test_MigrationFromV1ToV2WithMultipleStores() throws { + // the migration works fine even if NSMigratePersistentStoresAutomaticallyOption is set to true, but it should be false let options = [ - NSMigratePersistentStoresAutomaticallyOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, NSInferMappingModelAutomaticallyOption: false, - NSPersistentHistoryTrackingKey: true, // ⚠️ cannot be changed once set to true - NSPersistentHistoryTokenKey: true + NSPersistentHistoryTrackingKey: true, // ⚠️ cannot be changed once set to true + NSPersistentHistoryTokenKey: true, ] let url = URL.newDatabaseURL(withID: UUID()) let oldManagedObjectModel = V1.makeManagedObjectModel() let coordinator = NSPersistentStoreCoordinator(managedObjectModel: oldManagedObjectModel) - try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: V1.Configurations.one, at: url, options: options) + _ = try coordinator.addPersistentStore( + type: .sqlite, configuration: V1.Configurations.one, at: url, options: options) let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) context.persistentStoreCoordinator = coordinator @@ -144,14 +154,16 @@ final class ProgrammaticMigrationTests: XCTestCase { // Migration let sourceDescription = NSPersistentStoreDescription(url: url) let destinationDescription = NSPersistentStoreDescription(url: url) - options.forEach { key, value in + + for (key, value) in options { sourceDescription.setOption(value as NSObject, forKey: key) destinationDescription.setOption(value as NSObject, forKey: key) } - let migrator = Migrator(sourceStoreDescription: sourceDescription, - destinationStoreDescription: destinationDescription, - targetVersion: .version2) + let migrator = Migrator( + sourceStoreDescription: sourceDescription, + destinationStoreDescription: destinationDescription, + targetVersion: .version2) try migrator.migrate(enableWALCheckpoint: true) // Validation @@ -165,8 +177,10 @@ final class ProgrammaticMigrationTests: XCTestCase { // The new coordinator will load both the stores. let newCoordinator = NSPersistentStoreCoordinator(managedObjectModel: V2.makeManagedObjectModel()) - try newCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part1, at: url, options: options) - try newCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: V2.Configurations.part2, at: urlPart2, options: options) + _ = try newCoordinator.addPersistentStore( + type: .sqlite, configuration: V2.Configurations.part1, at: url, options: options) + _ = try newCoordinator.addPersistentStore( + type: .sqlite, configuration: V2.Configurations.part2, at: urlPart2, options: options) let newContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) newContext.persistentStoreCoordinator = newCoordinator @@ -180,31 +194,34 @@ final class ProgrammaticMigrationTests: XCTestCase { let authorRequest = NSFetchRequest(entityName: "Author") as! NSFetchRequest let authors = try newContext.fetch(authorRequest) XCTAssertEqual(authors.count, 2) - authors.forEach { - $0.materialize() + + for author in authors { + author.materialize() } try newContext.save() let bookRequest = NSFetchRequest(entityName: "Book") as! NSFetchRequest let books = try newContext.fetch(bookRequest) XCTAssertEqual(books.count, 52) - books.forEach { book in + + for book in books { XCTAssertNotNil(book.value(forKey: #keyPath(BookV2.frontCover))) } let feedbackRequest = NSFetchRequest(entityName: "Feedback") as! NSFetchRequest - feedbackRequest.affectedStores = [part1] // important to take values from part1 + feedbackRequest.affectedStores = [part1] // important to take values from part1 let feedbacks = try newContext.fetch(feedbackRequest) XCTAssertEqual(feedbacks.count, 444) - feedbacks.forEach { + for fb in feedbacks { let feedback = FeedbackV2(context: newContext) - feedback.authorAlias = $0.authorAlias - feedback.bookID = $0.bookID - feedback.comment = $0.comment - feedback.rating = $0.rating * 10 - newContext.assign(feedback, to: part2) // or it will stored in the first added persistent store + feedback.authorAlias = fb.authorAlias + feedback.bookID = fb.bookID + feedback.comment = fb.comment + feedback.rating = fb.rating * 10 + newContext.assign(feedback, to: part2) // or it will stored in the first added persistent store } + try newContext.save() let feedbackRequest2 = NSFetchRequest(entityName: "Feedback") as! NSFetchRequest @@ -218,7 +235,7 @@ final class ProgrammaticMigrationTests: XCTestCase { let fetchedAuthor = try newContext.fetch(fetchedAuthorRequest).first let author = try XCTUnwrap(fetchedAuthor) - XCTAssertEqual(author.feedbacks?.count, 6) // 3 each store + XCTAssertEqual(author.feedbacks?.count, 6) // 3 each store } do { @@ -227,7 +244,7 @@ final class ProgrammaticMigrationTests: XCTestCase { let fetchedAuthor = try newContext.fetch(fetchedAuthorRequest).first let author = try XCTUnwrap(fetchedAuthor) - XCTAssertEqual(author.feedbacks?.count, 882) // 441 each store + XCTAssertEqual(author.feedbacks?.count, 882) // 441 each store } newContext._fix_sqlite_warning_when_destroying_a_store() @@ -235,14 +252,16 @@ final class ProgrammaticMigrationTests: XCTestCase { try FileManager.default.removeItem(at: urlPart2) } - func testMigrationFromV1toV3() throws { + func test_MigrationFromV1toV3() throws { let url = URL.newDatabaseURL(withID: UUID()) - + + // the migration works fine even if NSMigratePersistentStoresAutomaticallyOption is set to true, + // but it should be false let options = [ - NSMigratePersistentStoresAutomaticallyOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, NSInferMappingModelAutomaticallyOption: false, - NSPersistentHistoryTrackingKey: true, // ⚠️ cannot be changed once set to true - NSPersistentHistoryTokenKey: true + NSPersistentHistoryTrackingKey: true, // ⚠️ cannot be changed once set to true + NSPersistentHistoryTokenKey: true, ] let description = NSPersistentStoreDescription(url: url) @@ -266,28 +285,36 @@ final class ProgrammaticMigrationTests: XCTestCase { }) // Migration + + XCTAssertTrue(SampleModel2.SampleModelVersion2.version1.isLightWeightMigrationPossibleToNextModelVersion()) + XCTAssertTrue(SampleModel2.SampleModelVersion2.version2.isLightWeightMigrationPossibleToNextModelVersion()) + XCTAssertFalse(SampleModel2.SampleModelVersion2.version3.isLightWeightMigrationPossibleToNextModelVersion()) - let migrator = Migrator(sourceStoreDescription: description, - destinationStoreDescription: description, - targetVersion: .version3) - var completion = 0.0 + let migrator = Migrator( + sourceStoreDescription: description, + destinationStoreDescription: description, + targetVersion: .version3) + + let completion = OSAllocatedUnfairLock(initialState: 0.0) let token = migrator.progress.observe(\.fractionCompleted, options: [.new]) { (progress, change) in - print(progress.fractionCompleted) - completion = progress.fractionCompleted + //print(progress.fractionCompleted) + completion.withLock { + $0 = progress.fractionCompleted + } } try migrator.migrate(enableWALCheckpoint: true) { metadata in if metadata.mappingModel.isInferred { - let manager = LightweightMigrationManager(sourceModel: metadata.sourceModel, destinationModel: metadata.destinationModel) - manager.updateProgressInterval = 0.001 // we need to set a very low refresh interval to get some fake progress updates - manager.estimatedTime = 0.1 - return manager + return NSMigrationManager(sourceModel: metadata.sourceModel, destinationModel: metadata.destinationModel) } else { // In FeedbackMigrationManager.swift there are 2 possibile solutions: // solution #1 - if metadata.mappingModel.entityMappingsByName["FeedbackToFeedbackPartOne"] != nil && metadata.mappingModel.entityMappingsByName["FeedbackToFeedbackPartTwo"] != nil { - return FeedbackMigrationManager(sourceModel: metadata.sourceModel, destinationModel: metadata.destinationModel) + if metadata.mappingModel.entityMappingsByName["FeedbackToFeedbackPartOne"] != nil + && metadata.mappingModel.entityMappingsByName["FeedbackToFeedbackPartTwo"] != nil + { + return FeedbackMigrationManager( + sourceModel: metadata.sourceModel, destinationModel: metadata.destinationModel) } else { return NSMigrationManager(sourceModel: metadata.sourceModel, destinationModel: metadata.destinationModel) } @@ -298,12 +325,12 @@ final class ProgrammaticMigrationTests: XCTestCase { } // Validation - XCTAssertEqual(completion, 1.0) + XCTAssertEqual(completion.withLock { $0 }, 1.0) token.invalidate() let newManagedObjectModel = V3.makeManagedObjectModel() let newCoordinator = NSPersistentStoreCoordinator(managedObjectModel: newManagedObjectModel) - try newCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: options) + _ = try newCoordinator.addPersistentStore(type: .sqlite, configuration: nil, at: url, options: options) let newContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) newContext.persistentStoreCoordinator = newCoordinator @@ -317,26 +344,28 @@ final class ProgrammaticMigrationTests: XCTestCase { XCTAssertEqual(author.books.count, 49) let feedbacksForAndrea = try XCTUnwrap(author.feedbacks) XCTAssertEqual(feedbacksForAndrea.count, 441) - feedbacksForAndrea.forEach { - if $0.comment.contains("great") { + + for fb in feedbacksForAndrea { + if fb.comment.contains("great") { // min value assigned randomly is 1.3, during the migration all the ratings get a +10 - XCTAssertTrue($0.rating >= 11.3) + XCTAssertTrue(fb.rating >= 11.3) } else { // The max value assigned randomly is 5.8 - XCTAssertTrue($0.rating <= 5.8, "Rating \($0.rating) should be lesser than 5.8") + XCTAssertTrue(fb.rating <= 5.8, "Rating \(fb.rating) should be lesser than 5.8") } } let books = try XCTUnwrap(author.books as? Set) - books.forEach { - XCTAssertEqual($0.pages.count, 99) + + for book in books { + XCTAssertEqual(book.pages.count, 99) } newContext._fix_sqlite_warning_when_destroying_a_store() try FileManager.default.removeItem(at: url) } - func testInvestigationNSExpression() { + func test_InvestigationNSExpression() { // https://nshipster.com/nsexpression/ // https://funwithobjc.tumblr.com/post/2922267976/using-custom-functions-with-nsexpression // https://nshipster.com/kvc-collection-operators/ @@ -348,7 +377,7 @@ final class ProgrammaticMigrationTests: XCTestCase { } do { - let expression = NSPredicate(format: "1 + 2 > 2") // for logical expressions use NSPredicate + let expression = NSPredicate(format: "1 + 2 > 2") // for logical expressions use NSPredicate let value = expression.evaluate(with: nil) XCTAssertEqual(value, true) } @@ -356,7 +385,7 @@ final class ProgrammaticMigrationTests: XCTestCase { do { let numbers = [1, 2, 3, 4, 4, 5, 9, 11] let args = [NSExpression(forConstantValue: numbers)] - let expression = NSExpression(forFunction:"max:", arguments: args) + let expression = NSExpression(forFunction: "max:", arguments: args) let value = expression.expressionValue(with: nil, context: nil) as? Int XCTAssertEqual(value, 11) } @@ -367,7 +396,8 @@ final class ProgrammaticMigrationTests: XCTestCase { // and the return value of the method must also be an object! // FUNCTION(operand, 'function', arguments, ...) - let expression = NSExpression(format:#"FUNCTION(%@, 'substring2ToIndex:', %@)"#, argumentArray: ["hello world", NSNumber(1)]) + let expression = NSExpression( + format: #"FUNCTION(%@, 'substring2ToIndex:', %@)"#, argumentArray: ["hello world", NSNumber(1)]) // same as: //let expression = NSExpression(format:#"FUNCTION("hello world", 'substring2ToIndex:', %@)"#, argumentArray: [NSNumber(1)]) let value = expression.expressionValue(with: nil, context: nil) as? NSString @@ -379,7 +409,7 @@ final class ProgrammaticMigrationTests: XCTestCase { extension NSString { @objc(substring2ToIndex:) func substring2(to index: NSNumber) -> NSString { - return self.substring(to: index.intValue) as NSString + self.substring(to: index.intValue) as NSString } } diff --git a/Tests/ProgrammaticallyDefinedModelTests.swift b/Tests/ProgrammaticallyDefinedModel_Tests.swift similarity index 56% rename from Tests/ProgrammaticallyDefinedModelTests.swift rename to Tests/ProgrammaticallyDefinedModel_Tests.swift index 8ae7960c..a422d28d 100644 --- a/Tests/ProgrammaticallyDefinedModelTests.swift +++ b/Tests/ProgrammaticallyDefinedModel_Tests.swift @@ -1,24 +1,28 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest @testable import CoreDataPlus -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -final class ProgrammaticallyDefinedModelTests: OnDiskWithProgrammaticallyModelTestCase { - func testSetup() throws { +// MARK: - V1 + +final class ProgrammaticallyDefinedModel_Tests: OnDiskWithProgrammaticallyModelTestCase { + func test_Setup() throws { let context = container.viewContext context.fillWithSampleData2() try context.save() context.reset() - + let books = try Book.fetchObjects(in: context) XCTAssertEqual(books.count, 52) let authors = try Author.fetchObjects(in: context) XCTAssertEqual(authors.count, 2) - let fetchedAuthor = try Author.fetchObjects(in: context) { $0.predicate = NSPredicate(format: "%K == %@", #keyPath(Author.alias), "Alessandro") }.first + let fetchedAuthor = try Author.fetchObjects(in: context) { + $0.predicate = NSPredicate(format: "%K == %@", #keyPath(Author.alias), "Alessandro") + } + .first let author = try XCTUnwrap(fetchedAuthor) let feedbacks = try XCTUnwrap(author.feedbacks) @@ -27,19 +31,23 @@ final class ProgrammaticallyDefinedModelTests: OnDiskWithProgrammaticallyModelTe XCTAssertEqual(author.favFeedbacks?.count, 2) } - func testTweakFetchedPropertyAtRuntime() throws { + func test_TweakFetchedPropertyAtRuntime() throws { let context = container.viewContext context.fillWithSampleData2() try context.save() context.reset() - let fetchedAuthor = try Author.fetchObjects(in: context) { $0.predicate = NSPredicate(format: "%K == %@", #keyPath(Author.alias), "Alessandro") }.first + let fetchedAuthor = try Author.fetchObjects(in: context) { + $0.predicate = NSPredicate(format: "%K == %@", #keyPath(Author.alias), "Alessandro") + } + .first let author = try XCTUnwrap(fetchedAuthor) XCTAssertEqual(author.favFeedbacks?.count, 2) let fetchedProperties = Author.entity().properties.compactMap { $0 as? NSFetchedPropertyDescription } - let favFeedbacksFetchedProperty = try XCTUnwrap(fetchedProperties.filter ({ $0.name == Author.FetchedProperty.favFeedbacks }).first) + let favFeedbacksFetchedProperty = try XCTUnwrap( + fetchedProperties.filter({ $0.name == Author.FetchedProperty.favFeedbacks }).first) // During the creation of the model, an key 'search' with value 'great' has been added to the fetched property // If we change its value at runtime, the result will reflect that. @@ -48,12 +56,45 @@ final class ProgrammaticallyDefinedModelTests: OnDiskWithProgrammaticallyModelTe } } +// MARK: - V2 + +final class ProgrammaticallyDefinedModelV2Tests: XCTestCase { + // Tests to make it sure that the model V2 is correctly defined + func test_SetupV2() throws { + let url = URL.newDatabaseURL(withID: UUID()) + let model = V2.makeManagedObjectModel() + let container = NSPersistentContainer(name: "SampleModel2", managedObjectModel: model) + let description = NSPersistentStoreDescription() + description.url = url + description.shouldMigrateStoreAutomatically = false + description.shouldInferMappingModelAutomatically = false + container.persistentStoreDescriptions = [description] + container.loadPersistentStores { (description, error) in + XCTAssertNil(error) + } + let context = container.viewContext + context.fillWithSampleData2UsingModelV2() + + try context.save() + context.reset() + + let fetchedAuthor = try AuthorV2.fetchObjects(in: context) { + $0.predicate = NSPredicate(format: "%K == %@", #keyPath(Author.alias), "Alessandro") + }.first + + let author = try XCTUnwrap(fetchedAuthor) + let feedbacks = try XCTUnwrap(author.feedbacks) + XCTAssertEqual(feedbacks.map({ $0.rating }), [3.5, 4.2, 4.3]) + + XCTAssertEqual(author.favFeedbacks?.count, 2) + } +} + // MARK: - V3 -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) final class ProgrammaticallyDefinedModelV3Tests: XCTestCase { // Tests to make it sure that the model V3 is correctly defined - func testSetupV3() throws { + func test_SetupV3() throws { let url = URL.newDatabaseURL(withID: UUID()) let container = NSPersistentContainer(name: "SampleModel2", managedObjectModel: V3.makeManagedObjectModel()) let description = NSPersistentStoreDescription() @@ -70,7 +111,9 @@ final class ProgrammaticallyDefinedModelV3Tests: XCTestCase { try context.save() context.reset() - let fetchedAuthor = try AuthorV3.fetchObjects(in: context) { $0.predicate = NSPredicate(format: "%K == %@", #keyPath(Author.alias), "Alessandro") }.first + let fetchedAuthor = try AuthorV3.fetchObjects(in: context) { + $0.predicate = NSPredicate(format: "%K == %@", #keyPath(Author.alias), "Alessandro") + }.first let author = try XCTUnwrap(fetchedAuthor) let feedbacks = try XCTUnwrap(author.feedbacks) @@ -79,13 +122,13 @@ final class ProgrammaticallyDefinedModelV3Tests: XCTestCase { XCTAssertEqual(author.favFeedbacks?.count, 2) } - func testInvestigationVersionHashes() { + func test_InvestigationVersionHashes() { // http://openradar.appspot.com/FB9044112 let coverVersionHash = V3.makeManagedObjectModel().entityVersionHashesByName["Cover"] let bookVersionHash = V3.makeManagedObjectModel().entityVersionHashesByName["Book"] XCTAssertEqual(V3.makeCoverEntity().versionHash, V3.makeCoverEntity().versionHash) XCTAssertEqual(V3.makeBookEntity().versionHash, V3.makeBookEntity().versionHash) - XCTAssertNotEqual(V3.makeCoverEntity().versionHash, coverVersionHash) // Bug: these are expected to be equal - XCTAssertNotEqual(V3.makeBookEntity().versionHash, bookVersionHash) // Bug: these are expected to be equal + XCTAssertNotEqual(V3.makeCoverEntity().versionHash, coverVersionHash) // Bug: these are expected to be equal + XCTAssertNotEqual(V3.makeBookEntity().versionHash, bookVersionHash) // Bug: these are expected to be equal } } diff --git a/Tests/Resources/SampleModel/Entities/Car.swift b/Tests/Resources/SampleModel/Entities/Car.swift index 3f2401f5..b9111a5d 100644 --- a/Tests/Resources/SampleModel/Entities/Car.swift +++ b/Tests/Resources/SampleModel/Entities/Car.swift @@ -1,8 +1,8 @@ // CoreDataPlus -import Foundation import CoreData import CoreDataPlus +import Foundation // Entity hierarchy vs Class hierarchy // @@ -41,15 +41,16 @@ public class Car: BaseEntity { @NSManaged public var model: String? @NSManaged public var numberPlate: String! @NSManaged public var owner: Person? - @NSManaged public var currentDrivingSpeed: Int // transient property - @NSManaged public var color: Color? // transformable property - + @NSManaged public var currentDrivingSpeed: Int // transient property + @NSManaged public var color: Color? // transformable property + // Additional notes // - color: in the "Data Model Inspector", the "class name" field is set to Color; this is an optional requirement but quite useful because CoreData will validate the type when assigned to the color property. } @objc(SportCar) -public class SportCar: Car { } +public class SportCar: Car { +} @objc(ExpensiveSportCar) final public class ExpensiveSportCar: SportCar { @@ -59,5 +60,9 @@ final public class ExpensiveSportCar: SportCar { // V2 @objc(LuxuryCar) final public class LuxuryCar: SportCar { + // Some tests fallback to the string representation of LuxuryCar (fallback of entityName instead of entity().name because + // entity() is not accessible. If that happens in production, it's better to override the property like so: + //public override class var entityName: String { "LuxuryCar" } + @NSManaged public var isLimitedEdition: Bool } diff --git a/Tests/Resources/SampleModel/Entities/Maker.swift b/Tests/Resources/SampleModel/Entities/Maker.swift index ad015561..1dd13601 100644 --- a/Tests/Resources/SampleModel/Entities/Maker.swift +++ b/Tests/Resources/SampleModel/Entities/Maker.swift @@ -6,11 +6,13 @@ import CoreData @objc(Maker) final public class Maker: NSManagedObject { @NSManaged public var name: String - @NSManaged public var cars: NSSet? // This is why it must be a NSSet https://twitter.com/an0/status/1157072652290445314 and https://developer.apple.com/forums/thread/651325 + // This is why it must be a NSSet https://twitter.com/an0/status/1157072652290445314 and https://developer.apple.com/forums/thread/651325 + // "The problem is that bridging will cause all the managed objects to be faulted into memory, which can become a serious performance problem when you have more than a thousand (or so) related objects. The generated subclasses have to work for everyone, so we can’t ship that." + @NSManaged public var cars: NSSet? public var _cars: Set? { get { - return cars as? Set + cars as? Set } set { if let newCars = newValue { diff --git a/Tests/Resources/SampleModel/Entities/NSManagedObject+DelayedDeletable.swift b/Tests/Resources/SampleModel/Entities/NSManagedObject+DelayedDeletable.swift index b4f9bdef..12a30bb6 100644 --- a/Tests/Resources/SampleModel/Entities/NSManagedObject+DelayedDeletable.swift +++ b/Tests/Resources/SampleModel/Entities/NSManagedObject+DelayedDeletable.swift @@ -29,14 +29,14 @@ extension DelayedDeletable where Self: NSManagedObject { /// /// Predicate to filter for objects that haven’t a deletion date. public static var notMarkedForLocalDeletionPredicate: NSPredicate { - return NSPredicate(format: "%K == NULL", markedForDeletionKey) + NSPredicate(format: "%K == NULL", markedForDeletionKey) } /// Protocol `DelayedDeletable`. /// /// Predicate to filter for objects that have a deletion date. public static var markedForLocalDeletionPredicate: NSPredicate { - return NSPredicate(format: "%K != NULL", markedForDeletionKey) + NSPredicate(format: "%K != NULL", markedForDeletionKey) } } @@ -47,7 +47,7 @@ extension DelayedDeletable where Self: NSManagedObject { /// /// Returns true if `self` has been marked for deletion. public var hasChangedForDelayedDeletion: Bool { - return changedValue(forKey: markedForDeletionKey) as? Date != nil + changedValue(forKey: markedForDeletionKey) as? Date != nil } /// Marks an object to be deleted at a later point in time (if not already marked). @@ -72,7 +72,10 @@ extension NSFetchRequestResult where Self: NSManagedObject & DelayedDeletable { /// - resultType: The type of the batch delete result (default: `NSBatchDeleteRequestResultType.resultTypeStatusOnly`). /// - Returns: a NSBatchDeleteResult result. /// - Throws: An error in cases of a batch delete operation failure. - public static func batchDeleteMarkedForDeletion(with context: NSManagedObjectContext, olderThan cutOffDate: Date = Date(timeIntervalSinceNow: -TimeInterval(120)), resultType: NSBatchDeleteRequestResultType = .resultTypeStatusOnly) throws -> NSBatchDeleteResult { + public static func batchDeleteMarkedForDeletion( + with context: NSManagedObjectContext, olderThan cutOffDate: Date = Date(timeIntervalSinceNow: -TimeInterval(120)), + resultType: NSBatchDeleteRequestResultType = .resultTypeStatusOnly + ) throws -> NSBatchDeleteResult { let predicate = NSPredicate(format: "%K <= %@", markedForDeletionKey, cutOffDate as NSDate) return try batchDelete(using: context, predicate: predicate, resultType: resultType) diff --git a/Tests/Resources/SampleModel/Entities/Person.swift b/Tests/Resources/SampleModel/Entities/Person.swift index 9ccb7104..e7e5dbb0 100644 --- a/Tests/Resources/SampleModel/Entities/Person.swift +++ b/Tests/Resources/SampleModel/Entities/Person.swift @@ -1,20 +1,21 @@ // CoreDataPlus -import Foundation import CoreData import CoreDataPlus +import Foundation @objc(Person) final public class Person: NSManagedObject { - @NSManaged public private(set) var id: UUID // preserved after deletion (tombstone) + @NSManaged public private(set) var id: UUID // preserved after deletion (tombstone) @NSManaged public var firstName: String @NSManaged public var lastName: String - @NSManaged public var cars: NSSet? // This is why it must be a NSSet https://twitter.com/an0/status/1157072652290445314 and https://developer.apple.com/forums/thread/651325 - @NSManaged public var isDriving: Bool // transient property + // This is why it must be a NSSet https://twitter.com/an0/status/1157072652290445314 and https://developer.apple.com/forums/thread/651325 + @NSManaged public var cars: NSSet? + @NSManaged public var isDriving: Bool // transient property public var _cars: Set? { get { - return cars as? Set + cars as? Set } set { if let newCars = newValue { @@ -50,7 +51,7 @@ extension Person { // if only transients properties have changed, don't refresh the update date guard hasPersistentChangedValues else { return } - refreshUpdateDate(observingChanges: false) // we don't want to get notified when this value changes. + refreshUpdateDate(observingChanges: false) // we don't want to get notified when this value changes. } } diff --git a/Tests/Resources/SampleModel/Fixtures/README.md b/Tests/Resources/SampleModel/Fixtures/README.md index f06e6700..63d86d48 100644 --- a/Tests/Resources/SampleModel/Fixtures/README.md +++ b/Tests/Resources/SampleModel/Fixtures/README.md @@ -11,13 +11,13 @@ When you open a Swift package with Xcode, Xcode knows how to handle common Apple That means Xcode will compile `SampleModel.xcdatamodeld` into `SampleModel.md` and `V2toV3.xcmappingmodel` into `V2toV3.cdm` and will copy them in the Resources folder of the test bundle automatically. -When using the terminal, though, we need to have already compiled versions of the model and mapping models and copy them in the resources bundle during the build phase. +When using the CLI, though, we need to have already compiled versions of the model and mapping models and copy them in the resources bundle during the build phase. That's why the `Fixtures` folder contains these binaries: - `SampleModel.momd` - `V2toV3.cdm` -The main problem to have tests working from both Xcode and terminal is that, when building from Xcode, to avoid conflict errors, we need to exclude the compiled binaries described above because Xcode will create them automatically for us, while when building from terminal these binaries must be included and copied. +The main problem to have tests working from both Xcode and CLI (terminal) is that, when building from Xcode, to avoid conflict errors, we need to exclude the compiled binaries described above because Xcode will create them automatically for us, while when building from CLI these binaries must be included and copied. In the `Package.swift` these inclusions and exclusions are done automatically based on whether or not tests are being run from the command line. diff --git a/Tests/Resources/SampleModel/Fixtures/SampleModelV1.sqlite b/Tests/Resources/SampleModel/Fixtures/SampleModel_V1.sqlite similarity index 100% rename from Tests/Resources/SampleModel/Fixtures/SampleModelV1.sqlite rename to Tests/Resources/SampleModel/Fixtures/SampleModel_V1.sqlite diff --git a/Tests/Resources/SampleModel/Fixtures/SampleModelV2.sqlite b/Tests/Resources/SampleModel/Fixtures/SampleModel_V2.sqlite similarity index 100% rename from Tests/Resources/SampleModel/Fixtures/SampleModelV2.sqlite rename to Tests/Resources/SampleModel/Fixtures/SampleModel_V2.sqlite diff --git a/Tests/Resources/SampleModel/MappingModels/V2to3MakerPolicy.swift b/Tests/Resources/SampleModel/MappingModels/V2to3MakerPolicy.swift index dea7688e..8378d3bb 100644 --- a/Tests/Resources/SampleModel/MappingModels/V2to3MakerPolicy.swift +++ b/Tests/Resources/SampleModel/MappingModels/V2to3MakerPolicy.swift @@ -3,15 +3,23 @@ import CoreData final class V2to3MakerPolicy: NSEntityMigrationPolicy { - override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws { + override func createDestinationInstances(forSource sInstance: NSManagedObject, + in mapping: NSEntityMapping, + manager: NSMigrationManager + ) throws { try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager) - guard let makerName = sInstance.value(forKey: MakerKey) as? String else { + guard + let makerName = sInstance.value(forKey: makerKey) as? String + else { return } - guard let car = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first else { - fatalError("must return car") } + guard + let car = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first + else { + fatalError("must return car") + } guard let context = car.managedObjectContext else { fatalError("must have context") @@ -28,13 +36,13 @@ final class V2to3MakerPolicy: NSEntityMigrationPolicy { // and memory pressure) // let maker = context.findOrCreateMaker(withName: makerName, in: manager) - if var currentCars = maker.value(forKey: CarsKey) as? Set { + if var currentCars = maker.value(forKey: carsKey) as? Set { currentCars.insert(car) - maker.setValue(currentCars, forKey: CarsKey) + maker.setValue(currentCars, forKey: carsKey) } else { var cars = Set() cars.insert(car) - maker.setValue(cars, forKey: CarsKey) + maker.setValue(cars, forKey: carsKey) } } override func endInstanceCreation(forMapping mapping: NSEntityMapping, manager: NSMigrationManager) throws { @@ -43,15 +51,15 @@ final class V2to3MakerPolicy: NSEntityMigrationPolicy { } } -private let CarsKey = "cars" -private let MakerKey = "maker" -private let NameKey = "name" -private let MakerEntityName = "Maker" -private let CountryEntityName = "Country" +private let carsKey = "cars" +private let makerKey = "maker" +private let nameKey = "name" +private let makerEntityName = "Maker" +private let countryEntityName = "Country" extension NSManagedObject { fileprivate func isMaker(withName name: String) -> Bool { - return entity.name == MakerEntityName && (value(forKey: NameKey) as? String) == name + entity.name == makerEntityName && (value(forKey: nameKey) as? String) == name } } @@ -65,7 +73,7 @@ extension NSManagedObjectContext { userInfo = [AnyHashable: Any]() } var makersLookup: [String: NSManagedObject] - if let lookup = userInfo["makers"] as? [String:NSManagedObject] { + if let lookup = userInfo["makers"] as? [String: NSManagedObject] { makersLookup = lookup } else { makersLookup = [String: NSManagedObject]() @@ -76,8 +84,8 @@ extension NSManagedObjectContext { return maker } - let maker = NSEntityDescription.insertNewObject(forEntityName: MakerEntityName, into: self) - maker.setValue(name, forKey: NameKey) + let maker = NSEntityDescription.insertNewObject(forEntityName: makerEntityName, into: self) + maker.setValue(name, forKey: nameKey) makersLookup[name] = maker userInfo["makers"] = makersLookup manager.userInfo = userInfo @@ -86,8 +94,8 @@ extension NSManagedObjectContext { fileprivate func findOrCreateMaker(withName name: String) -> NSManagedObject { guard let maker = materializedObject(matching: { $0.isMaker(withName: name) }) else { - let maker = NSEntityDescription.insertNewObject(forEntityName: MakerEntityName, into: self) - maker.setValue(name, forKey: NameKey) + let maker = NSEntityDescription.insertNewObject(forEntityName: makerEntityName, into: self) + maker.setValue(name, forKey: nameKey) return maker } return maker diff --git a/Tests/Resources/SampleModel/NSManagedObjectContext+SampleModel.swift b/Tests/Resources/SampleModel/NSManagedObjectContext+SampleModel.swift index 73ffaa9f..261b672c 100644 --- a/Tests/Resources/SampleModel/NSManagedObjectContext+SampleModel.swift +++ b/Tests/Resources/SampleModel/NSManagedObjectContext+SampleModel.swift @@ -1,7 +1,7 @@ // CoreDataPlus -import Foundation import CoreData +import Foundation extension NSManagedObjectContext { /// Fills the context with a sample data set. (145 objects) @@ -236,7 +236,7 @@ extension NSManagedObjectContext { person8.cars = [car7] person9.cars = [sportCar3] person10.cars = [car8, car9] - person11.cars = [car10, car11, car12] // and a lots of panda πŸš— + person11.cars = [car10, car11, car12] // and a lots of panda πŸš— person12.cars = [sportCar4] person13.cars = [car13, car14] person14.cars = [car13, car14] diff --git a/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel.xcdatamodel/contents b/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel.xcdatamodel/contents index 9add3e4b..29f58cf0 100644 --- a/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel.xcdatamodel/contents +++ b/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -32,10 +32,4 @@ - - - - - - \ No newline at end of file diff --git a/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel2.xcdatamodel/contents b/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel2.xcdatamodel/contents index eed9e515..358a52fa 100644 --- a/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel2.xcdatamodel/contents +++ b/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel2.xcdatamodel/contents @@ -1,11 +1,11 @@ - + - - - - - + + + + + @@ -17,13 +17,13 @@ - + - - - - + + + + @@ -32,10 +32,4 @@ - - - - - - \ No newline at end of file diff --git a/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel3.xcdatamodel/contents b/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel3.xcdatamodel/contents index 1f37d11d..143884ad 100644 --- a/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel3.xcdatamodel/contents +++ b/Tests/Resources/SampleModel/SampleModel.xcdatamodeld/SampleModel3.xcdatamodel/contents @@ -1,12 +1,12 @@ - + - - - - - - + + + + + + @@ -14,11 +14,11 @@ - + - - + + @@ -26,11 +26,11 @@ - - - - - + + + + + @@ -39,11 +39,4 @@ - - - - - - - \ No newline at end of file diff --git a/Tests/Resources/SampleModel/SampleModelVersion.swift b/Tests/Resources/SampleModel/SampleModelVersion.swift index cbbd0f71..6ce0818c 100644 --- a/Tests/Resources/SampleModel/SampleModelVersion.swift +++ b/Tests/Resources/SampleModel/SampleModelVersion.swift @@ -2,22 +2,27 @@ import CoreData import XCTest +import os.lock + @testable import CoreDataPlus -private var cache = [String: NSManagedObjectModel]() +// Make sure models are loaded in memory +let model1 = SampleModelVersion.version1._managedObjectModel() +let model2 = SampleModelVersion.version2._managedObjectModel() +let model3 = SampleModelVersion.version3._managedObjectModel() -public enum SampleModelVersion: String, CaseIterable { +public enum SampleModelVersion: String, CaseIterable, LegacyMigration { case version1 = "SampleModel" case version2 = "SampleModel2" case version3 = "SampleModel3" } extension SampleModelVersion: ModelVersion { - public static var allVersions: [SampleModelVersion] { return SampleModelVersion.allCases } - public static var currentVersion: SampleModelVersion { return .version1 } - public var modelName: String { return "SampleModel" } + public static var allVersions: [SampleModelVersion] { SampleModelVersion.allCases } + public static var currentVersion: SampleModelVersion { .version1 } + public var modelName: String { "SampleModel" } - public var successor: SampleModelVersion? { + public var next: SampleModelVersion? { switch self { case .version1: return .version2 case .version2: return .version3 @@ -25,18 +30,19 @@ extension SampleModelVersion: ModelVersion { } } - public var versionName: String { return rawValue } + public var versionName: String { rawValue } public var modelBundle: Bundle { Bundle.tests } public func managedObjectModel() -> NSManagedObjectModel { - if let model = cache[self.versionName], #available(iOS 12.0, tvOS 12.0, watchOS 5.0, macOS 10.14, *) { - return model + switch self { + case .version1: + model1 + case .version2: + model2 + case .version3: + model3 } - - let model = _managedObjectModel() - cache[self.versionName] = model - return model } } diff --git a/Tests/Resources/SampleModel2/BookCoverToCoverMigrationPolicy.swift b/Tests/Resources/SampleModel2/BookCoverToCoverMigrationPolicy.swift index 160262e3..81ac4d53 100644 --- a/Tests/Resources/SampleModel2/BookCoverToCoverMigrationPolicy.swift +++ b/Tests/Resources/SampleModel2/BookCoverToCoverMigrationPolicy.swift @@ -7,7 +7,9 @@ import CoreData @objc(BookCoverToCoverMigrationPolicy) class BookCoverToCoverMigrationPolicy: NSEntityMigrationPolicy { - override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws { + override func createDestinationInstances( + forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager + ) throws { // This method is invoked by the migration manager on each source instance (as specified by the sourceExpression in the mapping) // to create the corresponding destination instance(s). // It also associates the source and destination instances by calling NSMigrationManager’s associate(sourceInstance:withDestinationInstance:for:) method. @@ -15,18 +17,17 @@ class BookCoverToCoverMigrationPolicy: NSEntityMigrationPolicy { // If we don't call the super implementation we need to do the association programmatically like so: // Note: since you already have a destinationInstance, you won't need to call anymore manager.destinationInstances(forEntityMappingName:sourceInstances:) -// let destinationInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext) -// let destinationInstanceKeys = destinationInstance.entity.attributesByName.keys // relationship keys aren't defined here (which is fine) -// destinationInstanceKeys.forEach { (key) in -// if let value = sInstance.value(forKey: key) { -// if let nsobject = value as? NSObject, nsobject.isEqual(NSNull()) { -// return -// } -// destinationInstance.setValue(value, forKey: key) -// } -// } -// manager.associate(sourceInstance: sInstance, withDestinationInstance: destinationInstance, for: mapping) - + // let destinationInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext) + // let destinationInstanceKeys = destinationInstance.entity.attributesByName.keys // relationship keys aren't defined here (which is fine) + // destinationInstanceKeys.forEach { (key) in + // if let value = sInstance.value(forKey: key) { + // if let nsobject = value as? NSObject, nsobject.isEqual(NSNull()) { + // return + // } + // destinationInstance.setValue(value, forKey: key) + // } + // } + // manager.associate(sourceInstance: sInstance, withDestinationInstance: destinationInstance, for: mapping) // This is how we can use the NSEntityMapping userInfo to pass additional data to the policy. // This way we can, for instance, re-use the same policy for different migrations and change its loginc @@ -39,7 +40,9 @@ class BookCoverToCoverMigrationPolicy: NSEntityMigrationPolicy { return } - guard let book = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first else { + guard + let book = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first + else { fatalError("must return book") } @@ -48,31 +51,33 @@ class BookCoverToCoverMigrationPolicy: NSEntityMigrationPolicy { } // ⚠️ -// let sBooks = sInstance.value(forKey: "pages") as? Set ?? Set() -// let dBooks = manager.destinationInstances(forEntityMappingName: "PageToPage", sourceInstances: Array(sBooks)) -// book.setValue(Set(dBooks), forKey: "pages") + // let sBooks = sInstance.value(forKey: "pages") as? Set ?? Set() + // let dBooks = manager.destinationInstances(forEntityMappingName: "PageToPage", sourceInstances: Array(sBooks)) + // book.setValue(Set(dBooks), forKey: "pages") let cover = NSEntityDescription.insertNewObject(forEntityName: "Cover", into: context) cover.setValue(frontCover.text.data(using: .utf8), forKey: #keyPath(CoverV3.data)) cover.setValue(book, forKey: #keyPath(CoverV3.book)) } - override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws { + override func createRelationships( + forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager + ) throws { // In a properly designed data model, this method will rarely, if ever, be needed. // The intention of this method (which is called in the second pass) is to build any relationships for the new destination entity // that was created in the previous method. However, if all the relationships in the model are double-sided, this method is not necessary // because we already set up one side of them. } -// override func performCustomValidation(forMapping mapping: NSEntityMapping, manager: NSMigrationManager) throws { -// try super.performCustomValidation(forMapping: mapping, manager: manager) -// } + // override func performCustomValidation(forMapping mapping: NSEntityMapping, manager: NSMigrationManager) throws { + // try super.performCustomValidation(forMapping: mapping, manager: manager) + // } //@objc(destinationTitleForSourceBookTitle:manager:) @objc func destinationTitle(forSourceBookTitle sTitle: String, manager: NSMigrationManager) -> String { // https://horseshoe7.wordpress.com/2017/09/13/manual-core-data-migrations-lessons-learned/ // see makeBookMapping() method on how to call custom methods in a policy - return sTitle + sTitle } } diff --git a/Tests/Resources/SampleModel2/Entities/Author.swift b/Tests/Resources/SampleModel2/Entities/Author.swift index cd4653e0..6ffeecfa 100644 --- a/Tests/Resources/SampleModel2/Entities/Author.swift +++ b/Tests/Resources/SampleModel2/Entities/Author.swift @@ -1,7 +1,7 @@ // CoreDataPlus -import Foundation import CoreData +import Foundation // MARK: - V1 @@ -12,12 +12,11 @@ public class Writer: NSManagedObject { @objc(Author) public class Author: Writer { - @NSManaged public var alias: String // unique + @NSManaged public var alias: String // unique @NSManaged public var siteURL: URL? - @NSManaged public var books: NSSet // of Books + @NSManaged public var books: NSSet // of Books } - extension Author { public enum FetchedProperty { static let feedbacks = "feedbacks" @@ -27,13 +26,13 @@ extension Author { // Xcode doesn't generate the accessor for fetched properties (if you are using Xcode code gen). // feedbacks ordered by rating ASC - public var feedbacks: [Feedback]? { // it should probably be a NSArray to avoid prefetching all the objects - return value(forKey: FetchedProperty.feedbacks) as? [Feedback] + public var feedbacks: [Feedback]? { // it should probably be a NSArray to avoid prefetching all the objects + value(forKey: FetchedProperty.feedbacks) as? [Feedback] } // feedbacks with the "great" word in their comments public var favFeedbacks: [Feedback]? { - return value(forKey: FetchedProperty.favFeedbacks) as? [Feedback] + value(forKey: FetchedProperty.favFeedbacks) as? [Feedback] } } @@ -42,7 +41,6 @@ extension Author { // Author: // - siteURL is removed - @objc(WriterV2) public class WriterV2: NSManagedObject { @NSManaged public var age: Int16 @@ -50,8 +48,8 @@ public class WriterV2: NSManagedObject { @objc(AuthorV2) public class AuthorV2: WriterV2 { - @NSManaged public var alias: String // unique - @NSManaged public var books: NSSet // of Books + @NSManaged public var alias: String // unique + @NSManaged public var books: NSSet // of Books } extension AuthorV2 { @@ -63,13 +61,13 @@ extension AuthorV2 { // Xcode doesn't generate the accessor for fetched properties (if you are using Xcode code gen). // feedbacks ordered by rating ASC - public var feedbacks: [FeedbackV2]? { // it should probably be a NSArray to avoid prefetching all the objects - return value(forKey: FetchedProperty.feedbacks) as? [FeedbackV2] + public var feedbacks: [FeedbackV2]? { // it should probably be a NSArray to avoid prefetching all the objects + value(forKey: FetchedProperty.feedbacks) as? [FeedbackV2] } // feedbacks with the "great" word in their comments public var favFeedbacks: [FeedbackV2]? { - return value(forKey: FetchedProperty.favFeedbacks) as? [FeedbackV2] + value(forKey: FetchedProperty.favFeedbacks) as? [FeedbackV2] } } @@ -85,9 +83,9 @@ public class WriterV3: NSManagedObject { @objc(AuthorV3) public class AuthorV3: WriterV3 { - @NSManaged public var alias: String // unique + @NSManaged public var alias: String // unique @NSManaged public var socialURL: URL? - @NSManaged public var books: NSSet // of Books + @NSManaged public var books: NSSet // of Books } extension AuthorV3 { @@ -99,12 +97,12 @@ extension AuthorV3 { // Xcode doesn't generate the accessor for fetched properties (if you are using Xcode code gen). // feedbacks ordered by rating ASC - public var feedbacks: [FeedbackV3]? { // it should probably be a NSArray to avoid prefetching all the objects - return value(forKey: FetchedProperty.feedbacks) as? [FeedbackV3] + public var feedbacks: [FeedbackV3]? { // it should probably be a NSArray to avoid prefetching all the objects + value(forKey: FetchedProperty.feedbacks) as? [FeedbackV3] } // feedbacks with the "great" word in their comments public var favFeedbacks: [FeedbackV3]? { - return value(forKey: FetchedProperty.favFeedbacks) as? [FeedbackV3] + value(forKey: FetchedProperty.favFeedbacks) as? [FeedbackV3] } } diff --git a/Tests/Resources/SampleModel2/Entities/Book.swift b/Tests/Resources/SampleModel2/Entities/Book.swift index 3c809309..a644f02e 100644 --- a/Tests/Resources/SampleModel2/Entities/Book.swift +++ b/Tests/Resources/SampleModel2/Entities/Book.swift @@ -1,13 +1,13 @@ // CoreDataPlus -import Foundation import CoreData +import Foundation // MARK: - V1 @objc(Book) public class Book: NSManagedObject { - @NSManaged public var uniqueID: UUID // unique + @NSManaged public var uniqueID: UUID // unique @NSManaged public var title: String @NSManaged public var price: NSDecimalNumber @@ -36,7 +36,7 @@ public class Book: NSManagedObject { @NSManaged public var cover: Cover @NSManaged public var publishedAt: Date @NSManaged public var author: Author - @NSManaged public var pages: NSSet // of Pages + @NSManaged public var pages: NSSet // of Pages @NSManaged public var pagesCount: Int public override func validateForInsert() throws { @@ -73,14 +73,14 @@ public class GraphicNovel: Book { @objc(BookV2) public class BookV2: NSManagedObject { - @NSManaged public var uniqueID: UUID // unique + @NSManaged public var uniqueID: UUID // unique @NSManaged public var title: String @NSManaged public var price: NSDecimalNumber public var priceAsDecimal: Decimal { price.decimalValue } @NSManaged public var frontCover: Cover @NSManaged public var publishedAt: Date @NSManaged public var author: AuthorV2 - @NSManaged public var pages: NSSet // of Pages + @NSManaged public var pages: NSSet // of Pages @NSManaged public var pagesCount: Int } @@ -112,14 +112,14 @@ public class GraphicNovelV2: BookV2 { @objc(BookV3) public class BookV3: NSManagedObject { - @NSManaged public var uniqueID: UUID // unique + @NSManaged public var uniqueID: UUID // unique @NSManaged public var title: String @NSManaged public var price: NSDecimalNumber public var priceAsDecimal: Decimal { price.decimalValue } @NSManaged public var frontCover: CoverV3 @NSManaged public var publishedAt: Date @NSManaged public var author: AuthorV3 - @NSManaged public var pages: NSSet // of Pages + @NSManaged public var pages: NSSet // of Pages @NSManaged public var pagesCount: Int } diff --git a/Tests/Resources/SampleModel2/Entities/Content.swift b/Tests/Resources/SampleModel2/Entities/Content.swift index 4f78c62b..868bad7d 100644 --- a/Tests/Resources/SampleModel2/Entities/Content.swift +++ b/Tests/Resources/SampleModel2/Entities/Content.swift @@ -16,7 +16,9 @@ public class Content: NSObject, NSSecureCoding { } public required init?(coder decoder: NSCoder) { - guard let text = decoder.decodeObject(of: [NSString.self], forKey: #keyPath(Content.text)) as? String else { return nil } + guard let text = decoder.decodeObject(of: [NSString.self], forKey: #keyPath(Content.text)) as? String else { + return nil + } self.text = text } } diff --git a/Tests/Resources/SampleModel2/Entities/Feedback.swift b/Tests/Resources/SampleModel2/Entities/Feedback.swift index 131c418b..8edcd8a4 100644 --- a/Tests/Resources/SampleModel2/Entities/Feedback.swift +++ b/Tests/Resources/SampleModel2/Entities/Feedback.swift @@ -31,4 +31,3 @@ public class FeedbackV3: NSManagedObject { @NSManaged public var comment: String @NSManaged public var rating: Double } - diff --git a/Tests/Resources/SampleModel2/Entities/Page.swift b/Tests/Resources/SampleModel2/Entities/Page.swift index f0b2484f..65441325 100644 --- a/Tests/Resources/SampleModel2/Entities/Page.swift +++ b/Tests/Resources/SampleModel2/Entities/Page.swift @@ -37,4 +37,3 @@ public class PageV3: NSManagedObject { var isEmpty: Bool { content == .none } } - diff --git a/Tests/Resources/SampleModel2/FeedbackMigrationManager.swift b/Tests/Resources/SampleModel2/FeedbackMigrationManager.swift index 1e358f75..3aab0a39 100644 --- a/Tests/Resources/SampleModel2/FeedbackMigrationManager.swift +++ b/Tests/Resources/SampleModel2/FeedbackMigrationManager.swift @@ -8,7 +8,9 @@ import CoreData @objc class FeedbackMigrationManager: NSMigrationManager { @objc(customfetchRequestForSourceEntityNamed:predicateString:) - func customFetchRequest(forSourceEntityNamed entityName:String, predicateString: String) -> NSFetchRequest { + func customFetchRequest(forSourceEntityNamed entityName: String, predicateString: String) -> NSFetchRequest< + NSFetchRequestResult + > { // 🚩 Investigating how to implement and call custom methods in the manager // see testMigrationFromV1toV3 and makeFeedbackMappingPartOne() let request = NSFetchRequest(entityName: entityName) diff --git a/Tests/Resources/SampleModel2/Fixtures/SampleModel2_V1.sqlite b/Tests/Resources/SampleModel2/Fixtures/SampleModel2_V1.sqlite new file mode 100644 index 00000000..d16ce86f Binary files /dev/null and b/Tests/Resources/SampleModel2/Fixtures/SampleModel2_V1.sqlite differ diff --git a/Tests/Resources/SampleModel2/Fixtures/SampleModel2_V2.sqlite b/Tests/Resources/SampleModel2/Fixtures/SampleModel2_V2.sqlite new file mode 100644 index 00000000..3b1893d7 Binary files /dev/null and b/Tests/Resources/SampleModel2/Fixtures/SampleModel2_V2.sqlite differ diff --git a/Tests/Resources/SampleModel2/NSManagedObjectContext+SampleModel2.swift b/Tests/Resources/SampleModel2/NSManagedObjectContext+SampleModel2.swift index 9d5a557b..f9c46732 100644 --- a/Tests/Resources/SampleModel2/NSManagedObjectContext+SampleModel2.swift +++ b/Tests/Resources/SampleModel2/NSManagedObjectContext+SampleModel2.swift @@ -1,13 +1,20 @@ // CoreDataPlus -import Foundation import CoreData +import Foundation extension NSManagedObjectContext { - // 1 Author with 3 Books func fillWithSampleData2() { - fillWithAuthor1() - fillWithAuthor2() + fillWithAuthor1() // Author with 2 books and 1 Graphic Novel + fillWithAuthor2() // Author with 49 books + } + + func fillWithSampleData2UsingModelV2() { + fillWithAuthor1UsingModelV2() + } + + func fillWithSampleData2UsingModelV3() { + fillWithAuthor1UsingModelV3() } func fillWithAuthor1() { @@ -22,7 +29,7 @@ extension NSManagedObjectContext { book1.title = "title 1 - author 1" book1.uniqueID = UUID() - (1..<100).forEach { index in + for index in 1..<100 { let page = Page(context: self) page.book = book1 page.number = Int32(index) @@ -39,7 +46,7 @@ extension NSManagedObjectContext { book2.uniqueID = UUID() let book2Pages = NSMutableSet() - (1..<2).forEach { index in + for index in 1..<2 { let page = Page(context: self) page.book = book2 page.number = Int32(index) @@ -58,7 +65,7 @@ extension NSManagedObjectContext { graphicNovel1.cover = Cover(text: "Cover Graphic Novel") let graphicNovel1Pages = NSMutableSet() - (1..<20).forEach { index in + for index in 1..<20 { let page = Page(context: self) page.book = graphicNovel1 page.number = Int32(index) @@ -99,7 +106,7 @@ extension NSManagedObjectContext { author.alias = "Andrea" author.age = 30 - (1..<50).forEach { index in + for index in 1..<50 { let book = Book(context: self) book.price = NSDecimalNumber(10.11) book.publishedAt = Date() @@ -107,7 +114,7 @@ extension NSManagedObjectContext { book.uniqueID = UUID() book.author = author - (1..<100).forEach { index in + for index in 1..<100 { let page = Page(context: self) page.book = book page.number = Int32(index) @@ -116,7 +123,7 @@ extension NSManagedObjectContext { book.addToPages(page) } - (1..<10).forEach { _ in + for _ in 1..<10 { let feedbackBook1 = Feedback(context: self) feedbackBook1.bookID = book.uniqueID feedbackBook1.rating = [1.3, 2.4, 3.5, 4.6, 5.8].randomElement()! @@ -126,9 +133,89 @@ extension NSManagedObjectContext { } } } + + func fillWithAuthor1UsingModelV2() { + let author = AuthorV2(context: self) + author.alias = "Alessandro" + author.age = 40 - func fillWithSampleData2UsingModelV3() { - fillWithAuthor1UsingModelV3() + let book1 = BookV2(context: self) + //book1.price = Decimal(10.11) + book1.price = NSDecimalNumber(10.11) + book1.publishedAt = Date() + book1.title = "title 1 - author 1" + book1.uniqueID = UUID() + + for index in 1..<100 { + let page = PageV2(context: self) + page.book = book1 + page.number = Int32(index) + page.isBookmarked = true + page.content = Content(text: "content for page \(index)") + book1.addToPages(page) + } + + let book2 = BookV2(context: self) + //book2.price = Decimal(3.3333333333) + book2.price = NSDecimalNumber(3.3333333333) + book2.publishedAt = Date() + book2.title = "title 2 - author 1" + book2.uniqueID = UUID() + + let book2Pages = NSMutableSet() + for index in 1..<2 { + let page = PageV2(context: self) + page.book = book2 + page.number = Int32(index) + page.isBookmarked = false + page.content = Content(text: "content for page \(index)") + book2Pages.add(page) + } + book2.addToPages(book2Pages) + + let graphicNovel1 = GraphicNovelV2(context: self) + graphicNovel1.price = NSDecimalNumber(3.3333333333) + graphicNovel1.publishedAt = Date() + graphicNovel1.title = "title graphic novel - author 1" + graphicNovel1.uniqueID = UUID() + graphicNovel1.isBlackAndWhite = true + graphicNovel1.frontCover = Cover(text: "Cover Graphic Novel") + + let graphicNovel1Pages = NSMutableSet() + for index in 1..<20 { + let page = PageV2(context: self) + page.book = graphicNovel1 + page.number = Int32(index) + page.isBookmarked = index == 11 + page.content = Content(text: "content for page \(index)") + graphicNovel1Pages.add(page) + } + graphicNovel1.addToPages(graphicNovel1Pages) + + book1.author = author + let author1Books = NSMutableSet() + author1Books.add(book1) + author1Books.add(book2) + author1Books.add(graphicNovel1) + author.books = author1Books + + let feedbackBook1 = FeedbackV2(context: self) + feedbackBook1.bookID = book1.uniqueID + feedbackBook1.rating = 4.2 + feedbackBook1.comment = "great book" + feedbackBook1.authorAlias = author.alias + + let feedbackBook2 = FeedbackV2(context: self) + feedbackBook2.bookID = book2.uniqueID + feedbackBook2.rating = 3.5 + feedbackBook2.comment = "interesting book" + feedbackBook2.authorAlias = author.alias + + let feedbackGrapthicNovel1 = FeedbackV2(context: self) + feedbackGrapthicNovel1.bookID = graphicNovel1.uniqueID + feedbackGrapthicNovel1.rating = 4.3 + feedbackGrapthicNovel1.comment = "great novel" + feedbackGrapthicNovel1.authorAlias = author.alias } func fillWithAuthor1UsingModelV3() { @@ -148,7 +235,7 @@ extension NSManagedObjectContext { book1.frontCover = coverForBook1 coverForBook1.book = book1 - (1..<100).forEach { index in + for index in 1..<100 { let page = PageV3(context: self) page.book = book1 page.number = Int32(index) @@ -170,7 +257,7 @@ extension NSManagedObjectContext { coverForBook2.book = book2 let book2Pages = NSMutableSet() - (1..<2).forEach { index in + for index in 1..<2 { let page = PageV3(context: self) page.book = book2 page.number = Int32(index) @@ -193,7 +280,7 @@ extension NSManagedObjectContext { coverForGraphicNovel1.book = graphicNovel1 let graphicNovel1Pages = NSMutableSet() - (1..<20).forEach { index in + for index in 1..<20 { let page = PageV3(context: self) page.book = graphicNovel1 page.number = Int32(index) diff --git a/Tests/Resources/SampleModel2/SampleModel2+V1.swift b/Tests/Resources/SampleModel2/SampleModel2+V1.swift index de399312..e9804405 100644 --- a/Tests/Resources/SampleModel2/SampleModel2+V1.swift +++ b/Tests/Resources/SampleModel2/SampleModel2+V1.swift @@ -1,15 +1,16 @@ // CoreDataPlus import CoreData + @testable import CoreDataPlus extension SampleModel2.V1 { enum Configurations { static let one = "SampleConfigurationV1" } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + static func makeManagedObjectModel() -> NSManagedObjectModel { - if let model = SampleModel2.modelCache["V1"] { + if let model = SampleModel2.modelCache.withLock({ $0["V1"] }) { return model } @@ -21,28 +22,30 @@ extension SampleModel2.V1 { let page = makePageEntity() let feedback = makeFeedbackEntity() - // Definition using the CoreDataPlus convenience init - let feedbackList = NSFetchedPropertyDescription(name: Author.FetchedProperty.feedbacks, destinationEntity: feedback) { - $0.predicate = NSPredicate(format: "%K == $FETCH_SOURCE.%K", #keyPath(Feedback.authorAlias), #keyPath(Author.alias)) + let feedbackList = NSFetchedPropertyDescription(name: Author.FetchedProperty.feedbacks, destinationEntity: feedback) + { + $0.predicate = NSPredicate( + format: "%K == $FETCH_SOURCE.%K", #keyPath(Feedback.authorAlias), #keyPath(Author.alias)) $0.sortDescriptors = [NSSortDescriptor(key: #keyPath(Feedback.rating), ascending: true)] } // Definition without the CoreDataPlus convenience init -// let request = NSFetchRequest(entity: feedback) -// request.resultType = .managedObjectResultType -// request.predicate = NSPredicate(format: "%K == $FETCH_SOURCE.%K", #keyPath(Feedback.authorAlias), #keyPath(Author.alias)) -// request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Feedback.rating), ascending: true)] -// let feedbackList = NSFetchedPropertyDescription() -// feedbackList.name = Author.FetchedProperty.feedbacks -// feedbackList.fetchRequest = request + // let request = NSFetchRequest(entity: feedback) + // request.resultType = .managedObjectResultType + // request.predicate = NSPredicate(format: "%K == $FETCH_SOURCE.%K", #keyPath(Feedback.authorAlias), #keyPath(Author.alias)) + // request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Feedback.rating), ascending: true)] + // let feedbackList = NSFetchedPropertyDescription() + // feedbackList.name = Author.FetchedProperty.feedbacks + // feedbackList.fetchRequest = request author.add(feedbackList) // Definition without the CoreDataPlus convenience init let request2 = NSFetchRequest(entity: feedback) request2.resultType = .managedObjectResultType - request2.predicate = NSPredicate(format: "authorAlias == $FETCH_SOURCE.alias AND (comment CONTAINS [c] $FETCHED_PROPERTY.userInfo.search)") + request2.predicate = NSPredicate( + format: "authorAlias == $FETCH_SOURCE.alias AND (comment CONTAINS [c] $FETCHED_PROPERTY.userInfo.search)") let favFeedbackList = NSFetchedPropertyDescription() favFeedbackList.name = Author.FetchedProperty.favFeedbacks favFeedbackList.fetchRequest = request2 @@ -91,7 +94,7 @@ extension SampleModel2.V1 { bookToPages.inverseRelationship = pageToBook pageToBook.inverseRelationship = bookToPages - author.add(authorToBooks) // author.properties += [authorToBooks] + author.add(authorToBooks) // author.properties += [authorToBooks] book.properties += [bookToAuthor, bookToPages] page.properties += [pageToBook] @@ -101,8 +104,8 @@ extension SampleModel2.V1 { let entities = [writer, author, book, graphicNovel, page, feedback] managedObjectModel.entities = entities managedObjectModel.setEntities(entities, forConfigurationName: Configurations.one) - - SampleModel2.modelCache["V1"] = managedObjectModel + SampleModel2.modelCache.withLock { $0["V1"] = managedObjectModel } + return managedObjectModel } @@ -138,13 +141,13 @@ extension SampleModel2.V1 { // [ [parent’s constraint1, parent’s constraint2, ...], [child’s constraint1, child’s constraint2, ...] ] entity.uniquenessConstraints = [[#keyPath(Author.alias)]] - let index = NSFetchIndexDescription(name: "authorIndex", elements: [NSFetchIndexElementDescription(property: alias, collationType: .binary)]) + let index = NSFetchIndexDescription( + name: "authorIndex", elements: [NSFetchIndexElementDescription(property: alias, collationType: .binary)]) entity.indexes.append(index) return entity } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) static private func makeBookEntity() -> NSEntityDescription { var entity = NSEntityDescription() entity = NSEntityDescription() @@ -158,7 +161,9 @@ extension SampleModel2.V1 { title.isOptional = false // validation predicates are evaluated on validateForInsert(), overriding validateForInsert() without calling its // super implementation will ignore these predicates - let rule1 = (NSPredicate(format: "length >= 3 AND length <= 50"),"Title must have a length between 3 and 50 chars.") + let rule1 = ( + NSPredicate(format: "length >= 3 AND length <= 50"), "Title must have a length between 3 and 50 chars." + ) let rule2 = (NSPredicate(format: "SELF CONTAINS %@", "title"), "Title must contain 'title'.") title.setValidationPredicates([rule1.0, rule2.0], withValidationWarnings: [rule1.1, rule2.1]) @@ -166,7 +171,8 @@ extension SampleModel2.V1 { price.isOptional = false let defaultCover = Cover(text: "default cover") - let cover = NSAttributeDescription.transformable(for: Cover.self, name: #keyPath(Book.cover), defaultValue: defaultCover) + let cover = NSAttributeDescription.transformable( + for: Cover.self, name: #keyPath(Book.cover), defaultValue: defaultCover) cover.isOptional = false let publishedAt = NSAttributeDescription.date(name: #keyPath(Book.publishedAt)) @@ -175,9 +181,10 @@ extension SampleModel2.V1 { //let publishedAtPredicate = NSPredicate(format: "timeIntervalSinceReferenceDate < %@", twelveHoursAgo.timeIntervalSinceReferenceDate) //publishedAt.setValidationPredicates([publishedAtPredicate], withValidationWarnings: ["Date error"]) - let pagesCount = NSDerivedAttributeDescription(name: #keyPath(Book.pagesCount), - type: .integer64AttributeType, - derivationExpression: NSExpression(format: "pages.@count")) + let pagesCount = NSDerivedAttributeDescription( + name: #keyPath(Book.pagesCount), + type: .integer64, + derivationExpression: NSExpression(format: "pages.@count")) pagesCount.isOptional = true entity.properties = [uniqueID, title, price, cover, publishedAt, pagesCount] @@ -198,10 +205,10 @@ extension SampleModel2.V1 { isBookmarked.isOptional = false isBookmarked.defaultValue = false - let content = NSAttributeDescription.customTransformable(for: Content.self, name: #keyPath(Page.content)) { //(content: Content?) -> Data? in + let content = NSAttributeDescription.customTransformable(for: Content.self, name: #keyPath(Page.content)) { guard let content = $0 else { return nil } return try? NSKeyedArchiver.archivedData(withRootObject: content, requiringSecureCoding: true) - } reverse: { //data -> Content? in + } reverse: { //data -> Content? in guard let data = $0 else { return nil } return try? NSKeyedUnarchiver.unarchivedObject(ofClass: Content.self, from: data) } diff --git a/Tests/Resources/SampleModel2/SampleModel2+V2.swift b/Tests/Resources/SampleModel2/SampleModel2+V2.swift index a88f903b..e26c4e60 100644 --- a/Tests/Resources/SampleModel2/SampleModel2+V2.swift +++ b/Tests/Resources/SampleModel2/SampleModel2+V2.swift @@ -1,16 +1,17 @@ // CoreDataPlus import CoreData + @testable import CoreDataPlus extension V2 { enum Configurations { - static let part1 = "SampleConfigurationV2Part1" // all the entities - static let part2 = "SampleConfigurationV2Part2" // only Feedback + static let part1 = "SampleConfigurationV2Part1" // all the entities + static let part2 = "SampleConfigurationV2Part2" // only Feedback } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + static func makeManagedObjectModel() -> NSManagedObjectModel { - if let model = SampleModel2.modelCache["V2"] { + if let model = SampleModel2.modelCache.withLock({ $0["V2"] }) { return model } @@ -23,8 +24,11 @@ extension V2 { let feedback = makeFeedbackEntity() // Definition using the CoreDataPlus convenience init - let feedbackList = NSFetchedPropertyDescription(name: AuthorV2.FetchedProperty.feedbacks, destinationEntity: feedback) { - $0.predicate = NSPredicate(format: "%K == $FETCH_SOURCE.%K", #keyPath(FeedbackV2.authorAlias), #keyPath(AuthorV2.alias)) + let feedbackList = NSFetchedPropertyDescription( + name: AuthorV2.FetchedProperty.feedbacks, destinationEntity: feedback + ) { + $0.predicate = NSPredicate( + format: "%K == $FETCH_SOURCE.%K", #keyPath(FeedbackV2.authorAlias), #keyPath(AuthorV2.alias)) $0.sortDescriptors = [NSSortDescriptor(key: #keyPath(FeedbackV2.rating), ascending: true)] } author.add(feedbackList) @@ -32,7 +36,8 @@ extension V2 { // Definition without the CoreDataPlus convenience init let request2 = NSFetchRequest(entity: feedback) request2.resultType = .managedObjectResultType - request2.predicate = NSPredicate(format: "authorAlias == $FETCH_SOURCE.alias AND (comment CONTAINS [c] $FETCHED_PROPERTY.userInfo.search)") + request2.predicate = NSPredicate( + format: "authorAlias == $FETCH_SOURCE.alias AND (comment CONTAINS [c] $FETCHED_PROPERTY.userInfo.search)") let favFeedbackList = NSFetchedPropertyDescription() favFeedbackList.name = AuthorV2.FetchedProperty.favFeedbacks favFeedbackList.fetchRequest = request2 @@ -81,7 +86,7 @@ extension V2 { bookToPages.inverseRelationship = pageToBook pageToBook.inverseRelationship = bookToPages - author.add(authorToBooks) // author.properties += [authorToBooks] + author.add(authorToBooks) // author.properties += [authorToBooks] book.properties += [bookToAuthor, bookToPages] page.properties += [pageToBook] @@ -101,15 +106,15 @@ extension V2 { managedObjectModel.entities = entities managedObjectModel.setEntities([writer, author, book, page, feedback], forConfigurationName: Configurations.part1) managedObjectModel.setEntities([feedback], forConfigurationName: Configurations.part2) + SampleModel2.modelCache.withLock { $0["V2"] = managedObjectModel } - SampleModel2.modelCache["V2"] = managedObjectModel return managedObjectModel } static func makeWriterEntity() -> NSEntityDescription { let entity = NSEntityDescription(for: WriterV2.self, withName: String(describing: Writer.self)) entity.isAbstract = true - + let age = NSAttributeDescription.int16(name: #keyPath(WriterV2.age)) age.isOptional = false @@ -121,7 +126,7 @@ extension V2 { static func makeAuthorEntity() -> NSEntityDescription { var entity = NSEntityDescription() entity = NSEntityDescription() - entity.name = String(describing: Author.self) // 🚩 the entity name should stay the same + entity.name = String(describing: Author.self) // 🚩 the entity name should stay the same entity.managedObjectClassName = String(describing: AuthorV2.self) // ❌ Removed siteURL @@ -132,13 +137,13 @@ extension V2 { entity.properties = [alias] entity.uniquenessConstraints = [[#keyPath(AuthorV2.alias)]] - let index = NSFetchIndexDescription(name: "authorIndex", elements: [NSFetchIndexElementDescription(property: alias, collationType: .binary)]) + let index = NSFetchIndexDescription( + name: "authorIndex", elements: [NSFetchIndexElementDescription(property: alias, collationType: .binary)]) entity.indexes.append(index) return entity } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) static func makeBookEntity() -> NSEntityDescription { var entity = NSEntityDescription() entity = NSEntityDescription() @@ -150,7 +155,9 @@ extension V2 { let title = NSAttributeDescription.string(name: #keyPath(BookV2.title)) title.isOptional = false - let rule1 = (NSPredicate(format: "length >= 3 AND length <= 50"),"Title must have a length between 3 and 50 chars.") + let rule1 = ( + NSPredicate(format: "length >= 3 AND length <= 50"), "Title must have a length between 3 and 50 chars." + ) let rule2 = (NSPredicate(format: "SELF CONTAINS %@", "title"), "Title must contain 'title'.") title.setValidationPredicates([rule1.0, rule2.0], withValidationWarnings: [rule1.1, rule2.1]) @@ -159,16 +166,18 @@ extension V2 { // βœ… Renamed cover as frontCover let defaultCover = Cover(text: "default cover") - let cover = NSAttributeDescription.transformable(for: Cover.self, name: #keyPath(BookV2.frontCover), defaultValue: defaultCover) + let cover = NSAttributeDescription.transformable( + for: Cover.self, name: #keyPath(BookV2.frontCover), defaultValue: defaultCover) cover.isOptional = false cover.renamingIdentifier = #keyPath(Book.cover) let publishedAt = NSAttributeDescription.date(name: #keyPath(BookV2.publishedAt)) publishedAt.isOptional = false - let pagesCount = NSDerivedAttributeDescription(name: #keyPath(BookV2.pagesCount), - type: .integer64AttributeType, - derivationExpression: NSExpression(format: "pages.@count")) + let pagesCount = NSDerivedAttributeDescription( + name: #keyPath(BookV2.pagesCount), + type: .integer64, + derivationExpression: NSExpression(format: "pages.@count")) pagesCount.isOptional = true entity.properties = [uniqueID, title, price, cover, publishedAt, pagesCount] @@ -189,10 +198,10 @@ extension V2 { isBookmarked.isOptional = false isBookmarked.defaultValue = false - let content = NSAttributeDescription.customTransformable(for: Content.self, name: #keyPath(PageV2.content)) { //(content: Content?) -> Data? in + let content = NSAttributeDescription.customTransformable(for: Content.self, name: #keyPath(PageV2.content)) { guard let content = $0 else { return nil } return try? NSKeyedArchiver.archivedData(withRootObject: content, requiringSecureCoding: true) - } reverse: { //data -> Content? in + } reverse: { //data -> Content? in guard let data = $0 else { return nil } return try? NSKeyedUnarchiver.unarchivedObject(ofClass: Content.self, from: data) } diff --git a/Tests/Resources/SampleModel2/SampleModel2+V3.swift b/Tests/Resources/SampleModel2/SampleModel2+V3.swift index 7b1deedf..f693e999 100644 --- a/Tests/Resources/SampleModel2/SampleModel2+V3.swift +++ b/Tests/Resources/SampleModel2/SampleModel2+V3.swift @@ -6,16 +6,16 @@ // @distinctUnionOfSets expects an NSSet containing NSSet objects, and returns an NSSet. Because sets can’t contain duplicate values anyway, there is only the distinct operator.) import CoreData + @testable import CoreDataPlus extension V3 { enum Configurations { - static let one = "SampleConfigurationV3" // all the entities + static let one = "SampleConfigurationV3" // all the entities } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) static func makeManagedObjectModel() -> NSManagedObjectModel { - if let model = SampleModel2.modelCache["V3"] { + if let model = SampleModel2.modelCache.withLock({ $0["V3"] }) { return model } @@ -29,8 +29,11 @@ extension V3 { let feedback = makeFeedbackEntity() // Definition using the CoreDataPlus convenience init - let feedbackList = NSFetchedPropertyDescription(name: AuthorV3.FetchedProperty.feedbacks, destinationEntity: feedback) { - $0.predicate = NSPredicate(format: "%K == $FETCH_SOURCE.%K", #keyPath(FeedbackV3.authorAlias), #keyPath(AuthorV3.alias)) + let feedbackList = NSFetchedPropertyDescription( + name: AuthorV3.FetchedProperty.feedbacks, destinationEntity: feedback + ) { + $0.predicate = NSPredicate( + format: "%K == $FETCH_SOURCE.%K", #keyPath(FeedbackV3.authorAlias), #keyPath(AuthorV3.alias)) $0.sortDescriptors = [NSSortDescriptor(key: #keyPath(FeedbackV3.rating), ascending: true)] } author.add(feedbackList) @@ -38,7 +41,8 @@ extension V3 { // Definition without the CoreDataPlus convenience init let request2 = NSFetchRequest(entity: feedback) request2.resultType = .managedObjectResultType - request2.predicate = NSPredicate(format: "authorAlias == $FETCH_SOURCE.alias AND (comment CONTAINS [c] $FETCHED_PROPERTY.userInfo.search)") + request2.predicate = NSPredicate( + format: "authorAlias == $FETCH_SOURCE.alias AND (comment CONTAINS [c] $FETCHED_PROPERTY.userInfo.search)") let favFeedbackList = NSFetchedPropertyDescription() favFeedbackList.name = AuthorV3.FetchedProperty.favFeedbacks favFeedbackList.fetchRequest = request2 @@ -113,7 +117,7 @@ extension V3 { bookToPages.inverseRelationship = pageToBook pageToBook.inverseRelationship = bookToPages - author.add(authorToBooks) // author.properties += [authorToBooks] + author.add(authorToBooks) // author.properties += [authorToBooks] book.properties += [bookToAuthor, bookToPages, bookToCover] cover.properties += [coverToBook] page.properties += [pageToBook] @@ -124,8 +128,8 @@ extension V3 { let entities = [writer, author, book, graphicNovel, page, feedback, cover] managedObjectModel.entities = entities managedObjectModel.setEntities(entities, forConfigurationName: Configurations.one) + SampleModel2.modelCache.withLock { $0["V3"] = managedObjectModel } - SampleModel2.modelCache["V3"] = managedObjectModel return managedObjectModel } @@ -144,10 +148,10 @@ extension V3 { static func makeAuthorEntity() -> NSEntityDescription { var entity = NSEntityDescription() entity = NSEntityDescription() - entity.name = String(describing: Author.self) // 🚩 the entity name should stay the same + entity.name = String(describing: Author.self) // 🚩 the entity name should stay the same entity.managedObjectClassName = String(describing: AuthorV3.self) - // βœ… added in V3 + // βœ… socialURL added in V3 let socialURL = NSAttributeDescription.uri(name: #keyPath(AuthorV3.socialURL)) socialURL.isOptional = true socialURL.renamingIdentifier = #keyPath(AuthorV3.socialURL) @@ -158,13 +162,13 @@ extension V3 { entity.properties = [alias, socialURL] entity.uniquenessConstraints = [[#keyPath(AuthorV3.alias)]] - let index = NSFetchIndexDescription(name: "authorIndex", elements: [NSFetchIndexElementDescription(property: alias, collationType: .binary)]) + let index = NSFetchIndexDescription( + name: "authorIndex", elements: [NSFetchIndexElementDescription(property: alias, collationType: .binary)]) entity.indexes.append(index) return entity } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) static func makeCoverEntity() -> NSEntityDescription { var entity = NSEntityDescription() entity = NSEntityDescription() @@ -178,7 +182,6 @@ extension V3 { return entity } - @available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) static func makeBookEntity() -> NSEntityDescription { var entity = NSEntityDescription() entity = NSEntityDescription() @@ -190,24 +193,27 @@ extension V3 { let title = NSAttributeDescription.string(name: #keyPath(BookV3.title)) title.isOptional = false - let rule1 = (NSPredicate(format: "length >= 3 AND length <= 50"),"Title must have a length between 3 and 50 chars.") + let rule1 = ( + NSPredicate(format: "length >= 3 AND length <= 50"), "Title must have a length between 3 and 50 chars." + ) let rule2 = (NSPredicate(format: "SELF CONTAINS %@", "title"), "Title must contain 'title'.") title.setValidationPredicates([rule1.0, rule2.0], withValidationWarnings: [rule1.1, rule2.1]) let price = NSAttributeDescription.decimal(name: #keyPath(BookV3.price)) price.isOptional = false - // βœ… Renamed cover as frontCover let defaultCover = Cover(text: "default cover") - let cover = NSAttributeDescription.transformable(for: Cover.self, name: #keyPath(BookV3.frontCover), defaultValue: defaultCover) + let cover = NSAttributeDescription.transformable( + for: Cover.self, name: #keyPath(BookV3.frontCover), defaultValue: defaultCover) cover.isOptional = false let publishedAt = NSAttributeDescription.date(name: #keyPath(BookV3.publishedAt)) publishedAt.isOptional = false - let pagesCount = NSDerivedAttributeDescription(name: #keyPath(BookV3.pagesCount), - type: .integer64AttributeType, - derivationExpression: NSExpression(format: "pages.@count")) + let pagesCount = NSDerivedAttributeDescription( + name: #keyPath(BookV3.pagesCount), + type: .integer64, + derivationExpression: NSExpression(format: "pages.@count")) pagesCount.isOptional = true entity.properties = [uniqueID, title, price, cover, publishedAt, pagesCount] @@ -228,10 +234,10 @@ extension V3 { isBookmarked.isOptional = false isBookmarked.defaultValue = false - let content = NSAttributeDescription.customTransformable(for: Content.self, name: #keyPath(PageV3.content)) { //(content: Content?) -> Data? in + let content = NSAttributeDescription.customTransformable(for: Content.self, name: #keyPath(PageV3.content)) { guard let content = $0 else { return nil } return try? NSKeyedArchiver.archivedData(withRootObject: content, requiringSecureCoding: true) - } reverse: { //data -> Content? in + } reverse: { //data -> Content? in guard let data = $0 else { return nil } return try? NSKeyedUnarchiver.unarchivedObject(ofClass: Content.self, from: data) } @@ -294,7 +300,6 @@ extension V3 { // Fix: since the models are cached (and hence the entities descriptions with their version hasesh too), we use NSManagedObjectModel entityVersionHashesByName; // if the model isn't recreated on demand, the entities version hashes will stay the same. // If we use Xcode UI, we need to do some manual cleaning: https://github.com/diogot/CoreDataModelMigrationBug -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) extension V3 { static func makeCoverMapping() -> NSEntityMapping { @@ -345,7 +350,10 @@ extension V3 { title.name = #keyPath(BookV3.title) // 🚩 Investigating how to implement and call custom methods in the policy //title.valueExpression = NSExpression(format: "$source.\(#keyPath(BookV2.title))") - title.valueExpression = NSExpression(format: #"FUNCTION($entityPolicy, "destinationTitleForSourceBookTitle:manager:", $source.\#(#keyPath(BookV2.title)),$manager)"#) + title.valueExpression = NSExpression( + format: + #"FUNCTION($entityPolicy, "destinationTitleForSourceBookTitle:manager:", $source.\#(#keyPath(BookV2.title)),$manager)"# + ) let price = NSPropertyMapping() price.name = #keyPath(BookV3.price) @@ -361,25 +369,34 @@ extension V3 { let pages = NSPropertyMapping() pages.name = #keyPath(BookV3.pages) - pages.valueExpression = NSExpression(format: #"FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "PageToPage", $source.\#(#keyPath(BookV2.pages)))"#) - + pages.valueExpression = NSExpression( + format: + #"FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "PageToPage", $source.\#(#keyPath(BookV2.pages)))"# + ) let author = NSPropertyMapping() author.name = #keyPath(BookV3.author) - author.valueExpression = NSExpression(format: #"FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "AuthorToAuthor", $source.\#(#keyPath(BookV2.author)))"#) + author.valueExpression = NSExpression( + format: + #"FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "AuthorToAuthor", $source.\#(#keyPath(BookV2.author)))"# + ) let frontCover = NSPropertyMapping() frontCover.name = #keyPath(BookV3.frontCover) - mapping.attributeMappings = [pagesCount, - price, - publishedAt, - title, - uniqueID + mapping.attributeMappings = [ + pagesCount, + price, + publishedAt, + title, + uniqueID, ] mapping.relationshipMappings = [author, frontCover, pages] - mapping.sourceExpression = NSExpression(format: #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Book", "TRUEPREDICATE"), $manager.sourceContext, NO)"#) + mapping.sourceExpression = NSExpression( + format: + #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Book", "TRUEPREDICATE"), $manager.sourceContext, NO)"# + ) return mapping } @@ -408,7 +425,10 @@ extension V3 { title.name = #keyPath(BookV3.title) // 🚩 Investigating how to implement and call custom methods in the policy //title.valueExpression = NSExpression(format: "$source.\(#keyPath(GraphicNovelV2.title))") - title.valueExpression = NSExpression(format: #"FUNCTION($entityPolicy, "destinationTitleForSourceBookTitle:manager:", $source.\#(#keyPath(BookV2.title)),$manager)"#) + title.valueExpression = NSExpression( + format: + #"FUNCTION($entityPolicy, "destinationTitleForSourceBookTitle:manager:", $source.\#(#keyPath(BookV2.title)),$manager)"# + ) let price = NSPropertyMapping() price.name = #keyPath(BookV3.price) @@ -424,11 +444,17 @@ extension V3 { let pages = NSPropertyMapping() pages.name = #keyPath(BookV3.pages) - pages.valueExpression = NSExpression(format: #"FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "PageToPage", $source.\#(#keyPath(BookV2.pages)))"#) + pages.valueExpression = NSExpression( + format: + #"FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "PageToPage", $source.\#(#keyPath(BookV2.pages)))"# + ) let author = NSPropertyMapping() author.name = #keyPath(BookV3.author) - author.valueExpression = NSExpression(format: #"FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "AuthorToAuthor", $source.\#(#keyPath(GraphicNovelV2.author)))"#) + author.valueExpression = NSExpression( + format: + #"FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "AuthorToAuthor", $source.\#(#keyPath(GraphicNovelV2.author)))"# + ) let frontCover = NSPropertyMapping() frontCover.name = #keyPath(BookV3.frontCover) @@ -437,16 +463,20 @@ extension V3 { isBlackAndWhite.name = #keyPath(GraphicNovelV3.isBlackAndWhite) isBlackAndWhite.valueExpression = NSExpression(format: "$source.\(#keyPath(GraphicNovelV2.isBlackAndWhite))") - mapping.attributeMappings = [isBlackAndWhite, - pageCount, - price, - publishedAt, - title, - uniqueID, + mapping.attributeMappings = [ + isBlackAndWhite, + pageCount, + price, + publishedAt, + title, + uniqueID, ] mapping.relationshipMappings = [author, frontCover, pages] - mapping.sourceExpression = NSExpression(format: #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "GraphicNovel", "TRUEPREDICATE"), $manager.sourceContext, NO)"#) + mapping.sourceExpression = NSExpression( + format: + #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "GraphicNovel", "TRUEPREDICATE"), $manager.sourceContext, NO)"# + ) return mapping } @@ -482,7 +512,10 @@ extension V3 { // 🚩 Investigating how to implement and call custom methods in the manager // the FETCH uses customfetchRequestForSourceEntityNamed:predicateString: defined in FeedbackMigrationManager instead of: // mapping.sourceExpression = NSExpression(format: #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Feedback", %@), $manager.sourceContext, NO)"#, argumentArray: [predicate.description]) - mapping.sourceExpression = NSExpression(format: #"FETCH(FUNCTION($manager, "customfetchRequestForSourceEntityNamed:predicateString:" , "Feedback", %@), $manager.sourceContext, NO)"#, argumentArray: [predicate.description]) + mapping.sourceExpression = NSExpression( + format: + #"FETCH(FUNCTION($manager, "customfetchRequestForSourceEntityNamed:predicateString:" , "Feedback", %@), $manager.sourceContext, NO)"#, + argumentArray: [predicate.description]) return mapping } @@ -515,7 +548,10 @@ extension V3 { mapping.attributeMappings = [authorAlias, bookID, comment, rating] let predicate = NSPredicate(format: "NOT (%K CONTAINS [c] %@)", "comment", "great") - mapping.sourceExpression = NSExpression(format: #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Feedback", %@), $manager.sourceContext, NO)"#, argumentArray: [predicate.description]) + mapping.sourceExpression = NSExpression( + format: + #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Feedback", %@), $manager.sourceContext, NO)"#, + argumentArray: [predicate.description]) return mapping } @@ -546,7 +582,10 @@ extension V3 { rating.valueExpression = NSExpression(format: "$source.\(#keyPath(FeedbackV2.rating))") mapping.attributeMappings = [authorAlias, bookID, comment, rating] - mapping.sourceExpression = NSExpression(format: #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Feedback", "TRUEPREDICATE"), $manager.sourceContext, NO)"#) + mapping.sourceExpression = NSExpression( + format: + #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Feedback", "TRUEPREDICATE"), $manager.sourceContext, NO)"# + ) return mapping } @@ -568,7 +607,10 @@ extension V3 { let book = NSPropertyMapping() book.name = #keyPath(PageV3.book) - book.valueExpression = NSExpression(format: #"FUNCTION($manager, "destinationInstancesForSourceRelationshipNamed:sourceInstances:", "book", $source.\#(#keyPath(PageV2.book)))"#) + book.valueExpression = NSExpression( + format: + #"FUNCTION($manager, "destinationInstancesForSourceRelationshipNamed:sourceInstances:", "book", $source.\#(#keyPath(PageV2.book)))"# + ) let isBookmarked = NSPropertyMapping() isBookmarked.name = #keyPath(PageV3.isBookmarked) @@ -580,7 +622,10 @@ extension V3 { mapping.attributeMappings = [content, isBookmarked, number] mapping.relationshipMappings = [book] - mapping.sourceExpression = NSExpression(format: #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Page", "TRUEPREDICATE"), $manager.sourceContext, NO)"#) + mapping.sourceExpression = NSExpression( + format: + #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Page", "TRUEPREDICATE"), $manager.sourceContext, NO)"# + ) return mapping } @@ -610,11 +655,17 @@ extension V3 { let books = NSPropertyMapping() books.name = #keyPath(AuthorV3.books) - books.valueExpression = NSExpression(format: #"FUNCTION($manager, "destinationInstancesForSourceRelationshipNamed:sourceInstances:", "books", $source.\#(#keyPath(AuthorV2.books)))"#) + books.valueExpression = NSExpression( + format: + #"FUNCTION($manager, "destinationInstancesForSourceRelationshipNamed:sourceInstances:", "books", $source.\#(#keyPath(AuthorV2.books)))"# + ) mapping.attributeMappings = [age, alias, socialURL] mapping.relationshipMappings = [books] - mapping.sourceExpression = NSExpression(format: #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Author", "TRUEPREDICATE"), $manager.sourceContext, NO)"#) + mapping.sourceExpression = NSExpression( + format: + #"FETCH(FUNCTION($manager, "fetchRequestForSourceEntityNamed:predicateString:" , "Author", "TRUEPREDICATE"), $manager.sourceContext, NO)"# + ) return mapping } @@ -681,4 +732,3 @@ extension V3 { return [mappingModel1, mappingModel2] } } - diff --git a/Tests/Resources/SampleModel2/SampleModel2.swift b/Tests/Resources/SampleModel2/SampleModel2.swift index 37691dfe..676275f4 100644 --- a/Tests/Resources/SampleModel2/SampleModel2.swift +++ b/Tests/Resources/SampleModel2/SampleModel2.swift @@ -1,69 +1,74 @@ // CoreDataPlus import CoreData +import os.lock + @testable import CoreDataPlus public typealias V1 = SampleModel2.V1 public typealias V2 = SampleModel2.V2 public typealias V3 = SampleModel2.V3 +//public typealias SampleModel2Version = SampleModel2.SampleModel2Version + public enum SampleModel2 { - static var modelCache = [String: NSManagedObjectModel]() - public enum V1 { } - public enum V2 { } - public enum V3 { } + static let modelCache = OSAllocatedUnfairLock(uncheckedState: [String: NSManagedObjectModel]()) + public enum V1 {} + public enum V2 {} + public enum V3 {} } -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) extension SampleModel2 { - public enum SampleModel2Version: String, CaseIterable { + public enum SampleModelVersion2: String, CaseIterable, LegacyMigration { case version1 = "SampleModel2V1" case version2 = "SampleModel2V2" case version3 = "SampleModel3V3" } } -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -extension SampleModel2.SampleModel2Version: ModelVersion { - public static var allVersions: [SampleModel2.SampleModel2Version] { return SampleModel2.SampleModel2Version.allCases } - public static var currentVersion: SampleModel2.SampleModel2Version { return .version1 } - public var modelName: String { return "SampleModel2" } +let model2_1 = V1.makeManagedObjectModel() +let model2_2 = V2.makeManagedObjectModel() +let model2_3 = V3.makeManagedObjectModel() + +extension SampleModel2.SampleModelVersion2: ModelVersion { + public static var allVersions: [SampleModel2.SampleModelVersion2] { SampleModel2.SampleModelVersion2.allCases } + public static var currentVersion: SampleModel2.SampleModelVersion2 { .version1 } + public var modelName: String { "SampleModel2" } - public var successor: SampleModel2.SampleModel2Version? { + public var next: SampleModel2.SampleModelVersion2? { switch self { - case .version1: return .version2 - case .version2: return .version3 - default: return nil + case .version1: return .version2 + case .version2: return .version3 + default: return nil } } - public var versionName: String { return rawValue } + public var versionName: String { rawValue } public var modelBundle: Bundle { Bundle.tests } public func managedObjectModel() -> NSManagedObjectModel { switch self { - case .version1: return V1.makeManagedObjectModel() - case .version2: return V2.makeManagedObjectModel() - case .version3: return V3.makeManagedObjectModel() + case .version1: model2_1 + case .version2: model2_2 + case .version3: model2_3 } } } -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -extension SampleModel2.SampleModel2Version { +extension SampleModel2.SampleModelVersion2 { public func mappingModelsToNextModelVersion() -> [NSMappingModel]? { switch self { - case .version1: - let mappingModel = SampleModel2.SampleModel2Version.version1.inferredMappingModelToNextModelVersion()! - // Removed Author siteURL - // Renamed Book cover into frontCover - return [mappingModel] - case .version2: - let mappingModels = V3.makeMappingModelV2toV3() - return mappingModels - default: - return [] + case .version1: + let mappingModel = SampleModel2.SampleModelVersion2.version1.inferredMappingModelToNextModelVersion()! + // Removed Author siteURL + // Renamed Book cover into frontCover + return [mappingModel] + case .version2: + let mappingModels = V3.makeMappingModelV2toV3() + return mappingModels + default: + return [] } } } diff --git a/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3.mom b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3.mom new file mode 100644 index 00000000..c1aae807 Binary files /dev/null and b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3.mom differ diff --git a/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3.omo b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3.omo new file mode 100644 index 00000000..0c2006eb Binary files /dev/null and b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3.omo differ diff --git a/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3_v2.mom b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3_v2.mom new file mode 100644 index 00000000..6cd720de Binary files /dev/null and b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3_v2.mom differ diff --git a/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3_v3.mom b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3_v3.mom new file mode 100644 index 00000000..5ce93d05 Binary files /dev/null and b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/SampleModel3_v3.mom differ diff --git a/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/VersionInfo.plist b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/VersionInfo.plist new file mode 100644 index 00000000..de7899c6 Binary files /dev/null and b/Tests/Resources/SampleModel3/Fixtures/SampleModel3.momd/VersionInfo.plist differ diff --git a/Tests/Resources/SampleModel3/Fixtures/SampleModel3_V1.sqlite b/Tests/Resources/SampleModel3/Fixtures/SampleModel3_V1.sqlite new file mode 100644 index 00000000..9a3010d7 Binary files /dev/null and b/Tests/Resources/SampleModel3/Fixtures/SampleModel3_V1.sqlite differ diff --git a/Tests/Resources/SampleModel3/SampleModel3.swift b/Tests/Resources/SampleModel3/SampleModel3.swift new file mode 100644 index 00000000..5b6ae722 --- /dev/null +++ b/Tests/Resources/SampleModel3/SampleModel3.swift @@ -0,0 +1,34 @@ +// CoreDataPlus + +import CoreData + +// MARK: - V1 + +@objc(User) +public class User: NSManagedObject { + @NSManaged public var name: String! // unique + @NSManaged public var petName: String? +} + +//// MARK: - V2 +// +//@objc(UserV2) +//public class UserV2: NSManagedObject { +// @NSManaged public var name: String! // unique +// @NSManaged public var petName: String? +// @NSManaged public var pet: Pet? +//} +// +//@objc(Pet) +//public class Pet: NSManagedObject { +// @NSManaged public var name: String! // unique +// @NSManaged public var owner: UserV2! +//} +// +//// MARK: - V2 +// +//@objc(UserV3) +//public class UserV3: NSManagedObject { +// @NSManaged public var name: String! // unique +// @NSManaged public var pet: Pet? +//} diff --git a/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/.xccurrentversion b/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/.xccurrentversion new file mode 100644 index 00000000..1808829a --- /dev/null +++ b/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + SampleModel3.xcdatamodel + + diff --git a/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3.xcdatamodel/contents b/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3.xcdatamodel/contents new file mode 100644 index 00000000..5a73d95d --- /dev/null +++ b/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3.xcdatamodel/contents @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3_v2.xcdatamodel/contents b/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3_v2.xcdatamodel/contents new file mode 100644 index 00000000..38fe3967 --- /dev/null +++ b/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3_v2.xcdatamodel/contents @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3_v3.xcdatamodel/contents b/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3_v3.xcdatamodel/contents new file mode 100644 index 00000000..a3419573 --- /dev/null +++ b/Tests/Resources/SampleModel3/SampleModel3.xcdatamodeld/SampleModel3_v3.xcdatamodel/contents @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/Resources/SampleModel3/SampleModelVersion3.swift b/Tests/Resources/SampleModel3/SampleModelVersion3.swift new file mode 100644 index 00000000..3e5ee5b8 --- /dev/null +++ b/Tests/Resources/SampleModel3/SampleModelVersion3.swift @@ -0,0 +1,99 @@ +// CoreDataPlus + +import CoreData +import XCTest +import os.lock + +@testable import CoreDataPlus + +// Make sure models are loaded in memory +let model3_1 = SampleModelVersion3.version1._managedObjectModel() +let model3_2 = SampleModelVersion3.version2._managedObjectModel() +let model3_3 = SampleModelVersion3.version3._managedObjectModel() + +public enum SampleModelVersion3: String, CaseIterable, StagedMigration { + case version1 = "SampleModel3" + case version2 = "SampleModel3_v2" + case version3 = "SampleModel3_v3" +} + +extension SampleModelVersion3: ModelVersion { + public static var allVersions: [SampleModelVersion3] { SampleModelVersion3.allCases } + public static var currentVersion: SampleModelVersion3 { .version1 } + public var modelName: String { "SampleModel3" } + + public var next: SampleModelVersion3? { + switch self { + case .version1: return .version2 + case .version2: return .version3 + default: return nil + } + } + + public var versionName: String { rawValue } + public var modelBundle: Bundle { Bundle.tests } + + public func managedObjectModel() -> NSManagedObjectModel { + switch self { + case .version1: + model3_1 + case .version2: + model3_2 + case .version3: + model3_3 + } + } +} + +extension SampleModelVersion3 { + @available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) + public func migrationStageToNextModelVersion() -> NSMigrationStage? { + switch self { + // There can't be stages with the same versionCheckSum (you can't have a NSLightweightMigrationStage and a + // NSCustomMigrationStage referencing the same target versionCheckSum) + case .version1: + let stage = NSCustomMigrationStage(migratingFrom: self.managedObjectModelReference(), // v1 + to: self.next!.managedObjectModelReference()) // v2 + stage.label = "V1 to V2 (Add Pet entity and denormalize User entity)" + + stage.willMigrateHandler = { migrationManager, stage in + // in willMigrateHandler Pet is not yet defined + } + + stage.didMigrateHandler = { migrationManager, stage in + guard let container = migrationManager.container else { return } + + let context = container.newBackgroundContext() + try context.performAndWait { + let fetchRequest = NSFetchRequest(entityName: "User") + let users = try context.fetch(fetchRequest) + for user in users { + if let petName = user.value(forKey: "petName") as? String { + let pet = NSEntityDescription.insertNewObject(forEntityName: "Pet", into: context) + pet.setValue(petName, forKey: "name") + user.setValue(pet, forKey: "pet") + } + } + try context.save() + } + } + + return stage + case .version2: + let stage = NSLightweightMigrationStage([self.next!.versionChecksum]) // v3 + stage.label = "V2 to V3 (remove petName from User entity)" + return stage + default: + return nil + } + } +} + +// SampleModel1 and SampleModel2 can't be used for staged migration: +// +// SampleModel1 has a mapping model and it seems that the NSStagedMigrationManager won't work because +// it tries to use that instead. +// +// SampleModel2 is defined programmatically and I didn't find a way to make it work + + diff --git a/Tests/StagedMigrations_Tests.swift b/Tests/StagedMigrations_Tests.swift new file mode 100644 index 00000000..585e6940 --- /dev/null +++ b/Tests/StagedMigrations_Tests.swift @@ -0,0 +1,167 @@ +// CoreDataPlus + +import CoreData +import XCTest +@testable import CoreDataPlus + +@available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) +final class StagedMigrations_Tests: XCTestCase { + + func test_MigrationFromV1ToV2() throws { + let sourceURL = try Self.createSQLiteSample3ForV1() + let steps = SampleModelVersion3.version1.stagedMigrationSteps(to: .version2) + XCTAssertEqual(steps.count, 1) + + XCTAssertFalse(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version1)) + XCTAssertTrue(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version2)) + + let version = try SampleModelVersion3(persistentStoreURL: sourceURL) + XCTAssertTrue(version == .version1) + + let stages = steps.compactMap { $0.stage } + let migrator = NSStagedMigrationManager(stages) + let container = NSPersistentContainer(name: "SampleModel3", + managedObjectModel: SampleModelVersion3.version2.managedObjectModel()) + + let storeDescription = try XCTUnwrap(container.persistentStoreDescriptions.first) + storeDescription.url = sourceURL + storeDescription.setOption(migrator, forKey: NSPersistentStoreStagedMigrationManagerOptionKey) + + container.loadPersistentStores { storeDescription, error in + if let error = error { + XCTFail(error.localizedDescription) + } + } + + let migratedContext = container.viewContext + let users = try migratedContext.fetch(NSFetchRequest(entityName: "User")) + for user in users { + let pet = user.value(forKey: "pet") as? NSManagedObject + let petName = user.value(forKey: "petName") as? String + XCTAssertNotNil(pet) + XCTAssertNotNil(petName) + XCTAssertEqual(pet?.value(forKey: "name") as? String, petName) + } + + migratedContext._fix_sqlite_warning_when_destroying_a_store() + try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) + } + + func test_MigrationFromV1ToV3() throws { + let sourceURL = try Self.createSQLiteSample3ForV1() + let steps = SampleModelVersion3.version1.stagedMigrationSteps(to: .version3) + XCTAssertEqual(steps.count, 2) + + XCTAssertFalse(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version1)) + XCTAssertTrue(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version2)) + XCTAssertTrue(try isMigrationNecessary(for: sourceURL, to: SampleModelVersion3.version3)) + + XCTAssertTrue(SampleModelVersion3.version1.isLightWeightMigrationPossibleToNextModelVersion()) + XCTAssertTrue(SampleModelVersion3.version2.isLightWeightMigrationPossibleToNextModelVersion()) + XCTAssertFalse(SampleModelVersion3.version3.isLightWeightMigrationPossibleToNextModelVersion()) // no V4 + + let version = try SampleModelVersion3(persistentStoreURL: sourceURL) + XCTAssertTrue(version == .version1) + + let stages = steps.compactMap { $0.stage } + let migrator = NSStagedMigrationManager(stages) + let container = NSPersistentContainer(name: "SampleModel3", + managedObjectModel: SampleModelVersion3.version3.managedObjectModel()) + + let storeDescription = try XCTUnwrap(container.persistentStoreDescriptions.first) + storeDescription.url = sourceURL + storeDescription.setOption(migrator, forKey: NSPersistentStoreStagedMigrationManagerOptionKey) + //storeDescription.shouldAddStoreAsynchronously = true + + let expectation = expectation(description: "\(#function)\(#line)") + container.loadPersistentStores { storeDescription, error in + if let error = error { + XCTFail(error.localizedDescription) + } else { + expectation.fulfill() + } + } + + wait(for: [expectation]) + + let migratedContext = container.viewContext + let users = try migratedContext.fetch(NSFetchRequest(entityName: "User")) + for user in users { + let pet = user.value(forKey: "pet") as? NSManagedObject + XCTAssertNotNil(pet, "Pet should not be nil after migration to V3") + XCTAssertNotNil(pet?.value(forKey: "name"), "Pet name is not optional after migration to V3") + } + + migratedContext._fix_sqlite_warning_when_destroying_a_store() + try NSPersistentStoreCoordinator.destroyStore(at: sourceURL) + } + +// func test_generateSample() throws { +// let container = NSPersistentContainer(name: "SampleModel3", +// managedObjectModel: SampleModelVersion3.version1.managedObjectModel()) +// +// let url = URL.newDatabaseURL(withID: UUID()) +// +// let description = NSPersistentStoreDescription() +// description.url = url +// container.persistentStoreDescriptions = [description] +// +// container.loadPersistentStores { description, error in +// XCTAssertNil(error) +// } +// +// let context = container.viewContext +// +// for x in 0..<3 { +// for i in 0..<4 { +// let user = User(context: context) +// user.name = "User_\(x)" +// user.petName = "Dog_\(x)_\(i)" +// } +// } +// +// try context.save() +// +// print(url.path()) +// } + +} + +@available(iOS 17.0, tvOS 17.0, watchOS 10.0, macOS 14.0, visionOS 1.0, iOSApplicationExtension 17.0, macCatalystApplicationExtension 17.0, *) +extension StagedMigrations_Tests { + static func createSQLiteSample2ForV1() throws -> URL { + let bundle = Bundle.tests + let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel2_V1", withExtension: "sqlite")) + + // Being the test run multiple times, we create an unique copy for every test + let uuid = UUID().uuidString + let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel2_V1_copy-\(uuid).sqlite") + try FileManager.default.copyItem(at: _sourceURL, to: sourceURL) + XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) + return sourceURL + } + + static func createSQLiteSample2ForV2() throws -> URL { + let bundle = Bundle.tests + let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel2_V2", withExtension: "sqlite")) + + // Being the test run multiple times, we create an unique copy for every test + let uuid = UUID().uuidString + let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel2_V2_copy-\(uuid).sqlite") + try FileManager.default.copyItem(at: _sourceURL, to: sourceURL) + XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) + return sourceURL + } + + static func createSQLiteSample3ForV1() throws -> URL { + let bundle = Bundle.tests + let _sourceURL = try XCTUnwrap(bundle.url(forResource: "SampleModel3_V1", withExtension: "sqlite")) + + // Being the test run multiple times, we create an unique copy for every test + let uuid = UUID().uuidString + let sourceURL = bundle.bundleURL.appendingPathComponent("SampleModel3_V1_copy-\(uuid).sqlite") + try FileManager.default.copyItem(at: _sourceURL, to: sourceURL) + XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path)) + return sourceURL + } +} diff --git a/Tests/TransformerTests.swift b/Tests/Transformer_Tests.swift similarity index 82% rename from Tests/TransformerTests.swift rename to Tests/Transformer_Tests.swift index 3dca6648..db47207e 100644 --- a/Tests/TransformerTests.swift +++ b/Tests/Transformer_Tests.swift @@ -1,10 +1,11 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -final class TransformerTests: OnDiskTestCase { +final class Transformer_Tests: OnDiskTestCase { @objc(Dummy) class Dummy: NSObject, NSSecureCoding { static var supportsSecureCoding: Bool { true } @@ -24,7 +25,7 @@ final class TransformerTests: OnDiskTestCase { } } - func testSaveAndFetchTransformableValue() throws { + func test_SaveAndFetchTransformableValue() throws { let context = container.viewContext let car = Car(context: context) car.maker = "FIAT" @@ -40,7 +41,7 @@ final class TransformerTests: OnDiskTestCase { XCTAssertEqual(color.name, "red") } - func testSaveAndFetchTransformableValueUsingCustomTransformer() throws { + func test_SaveAndFetchTransformableValueUsingCustomTransformer() throws { let context = container.viewContext let car = Car(context: context) car.maker = "FIAT" @@ -56,7 +57,7 @@ final class TransformerTests: OnDiskTestCase { XCTAssertEqual(color.name, "red") } - func testTransformerUnregister() { + func test_TransformerUnregister() { XCTAssertFalse(Foundation.ValueTransformer.valueTransformerNames().contains(Transformer.transformerName)) Transformer.register() XCTAssertTrue(Foundation.ValueTransformer.valueTransformerNames().contains(Transformer.transformerName)) @@ -64,8 +65,9 @@ final class TransformerTests: OnDiskTestCase { XCTAssertFalse(Foundation.ValueTransformer.valueTransformerNames().contains(Transformer.transformerName)) } - func testDataTransformerUnregister() { - XCTAssertFalse(Foundation.ValueTransformer.valueTransformerNames().contains(CustomTransformer.transformerName)) + func test_DataTransformerUnregister() { + XCTAssertFalse( + Foundation.ValueTransformer.valueTransformerNames().contains(CustomTransformer.transformerName)) CustomTransformer.register { guard let color = $0 else { return nil } return try? NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true) @@ -73,12 +75,14 @@ final class TransformerTests: OnDiskTestCase { guard let data = $0 else { return nil } return try? NSKeyedUnarchiver.unarchivedObject(ofClass: Dummy.self, from: data) } - XCTAssertTrue(Foundation.ValueTransformer.valueTransformerNames().contains(CustomTransformer.transformerName)) + XCTAssertTrue( + Foundation.ValueTransformer.valueTransformerNames().contains(CustomTransformer.transformerName)) CustomTransformer.unregister() - XCTAssertFalse(Foundation.ValueTransformer.valueTransformerNames().contains(CustomTransformer.transformerName)) + XCTAssertFalse( + Foundation.ValueTransformer.valueTransformerNames().contains(CustomTransformer.transformerName)) } - func testTransformer() throws { + func test_Transformer() throws { let transformer = Transformer() let data = transformer.reverseTransformedValue(Color(name: "green")) XCTAssertNotNil(data) @@ -88,7 +92,7 @@ final class TransformerTests: OnDiskTestCase { XCTAssertEqual(name, "green") } - func testCustomTransformer() throws { + func test_CustomTransformer() throws { let transformer = CustomTransformer { color -> Data? in guard let color = color else { return nil } return try? NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true) @@ -104,13 +108,15 @@ final class TransformerTests: OnDiskTestCase { XCTAssertEqual(name, "green") } - func testDataTransformerWithNSArray() throws { + func test_DataTransformerWithNSArray() throws { let transformer = CustomTransformer { array -> Data? in guard let array = array else { return nil } return try? NSKeyedArchiver.archivedData(withRootObject: array, requiringSecureCoding: true) } reverseTransform: { data -> NSArray? in guard let data = data else { return nil } - return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSArray + return try? NSKeyedUnarchiver.unarchivedObject( + ofClasses: [Color.self, Dummy.self, NSNumber.self, NSArray.self], + from: data) as? NSArray } let array = NSMutableArray() diff --git a/Tests/Utils/BaseTestCase.swift b/Tests/Utils/BaseTestCase.swift index ad8a4310..526378a8 100644 --- a/Tests/Utils/BaseTestCase.swift +++ b/Tests/Utils/BaseTestCase.swift @@ -1,18 +1,19 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus class BaseTestCase: XCTestCase { class override func setUp() { -// CustomTransformer.register { -// guard let color = $0 else { return nil } -// return try? NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true) -// } reverseTransform: { -// guard let data = $0 else { return nil } -// return try? NSKeyedUnarchiver.unarchivedObject(ofClass: Color.self, from: data) -// } + // CustomTransformer.register { + // guard let color = $0 else { return nil } + // return try? NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true) + // } reverseTransform: { + // guard let data = $0 else { return nil } + // return try? NSKeyedUnarchiver.unarchivedObject(ofClass: Color.self, from: data) + // } Transformer.register() } @@ -21,3 +22,9 @@ class BaseTestCase: XCTestCase { //CustomTransformer.unregister() } } + +// TODO: Xcode 15 bug +// +// No NSValueTransformer with class name XXX was found for attribute YYY on entity ZZZ for custom `NSSecureUnarchiveFromDataTransformer` +// https://forums.developer.apple.com/forums/thread/740492 +// https://stackoverflow.com/questions/77340664/core-data-no-nsvaluetransformer-with-class-name-xxx-was-found-for-attribute-yy/77623593#77623593 diff --git a/Tests/Utils/CoreDataErrors.swift b/Tests/Utils/CoreDataErrors.swift index 784d9979..db0aaab9 100644 --- a/Tests/Utils/CoreDataErrors.swift +++ b/Tests/Utils/CoreDataErrors.swift @@ -13,7 +13,7 @@ let errorKeys = [ NSAffectedStoresErrorKey, NSAffectedObjectsErrorKey, NSPersistentStoreSaveConflictsErrorKey, - NSSQLiteErrorDomain + NSSQLiteErrorDomain, ] let errors = [ diff --git a/Tests/Utils/Deprecated.swift b/Tests/Utils/Deprecated.swift index 4b123632..c37c3de2 100644 --- a/Tests/Utils/Deprecated.swift +++ b/Tests/Utils/Deprecated.swift @@ -16,7 +16,9 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Returns: The first materialized matching object (if any). /// - Throws: It throws an error in cases of failure. @available(*, deprecated, message: "Deprecated.") - public static func materializedObjectOrFetch(in context: NSManagedObjectContext, where predicate: NSPredicate) throws -> Self? { + public static func materializedObjectOrFetch(in context: NSManagedObjectContext, where predicate: NSPredicate) throws + -> Self? + { // first we should fetch an existing object in the context as a performance optimization guard let object = materializedObject(in: context, where: predicate) else { // if it's not in memory, we should execute a fetch to see if it exists @@ -38,7 +40,11 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - Returns: A matching object or a configured new one. /// - Throws: It throws an error in cases of failure. @available(*, deprecated, message: "Deprecated.") - public static func findOneOrCreate(in context: NSManagedObjectContext, where predicate: NSPredicate, with configuration: (Self) -> Void) throws -> Self { + public static func findOneOrCreate( + in context: NSManagedObjectContext, where predicate: NSPredicate, with configuration: (Self) -> Void + ) throws + -> Self + { guard let object = try materializedObjectOrFetch(in: context, where: predicate) else { let newObject = Self(context: context) configuration(newObject) @@ -63,11 +69,13 @@ extension NSFetchRequestResult where Self: NSManagedObject { /// - configuration: Configuration closure called **only** when creating a new object. /// - Returns: A matching object or a configured new one. /// - Throws: It throws an error in cases of failure. - public static func findUniqueOrCreate(in context: NSManagedObjectContext, - where predicate: NSPredicate, - affectedStores: [NSPersistentStore]? = nil, - assignedStore: NSPersistentStore? = nil, - with configuration: (Self) -> Void) throws -> Self { + public static func findUniqueOrCreate( + in context: NSManagedObjectContext, + where predicate: NSPredicate, + affectedStores: [NSPersistentStore]? = nil, + assignedStore: NSPersistentStore? = nil, + with configuration: (Self) -> Void + ) throws -> Self { let uniqueObject = try fetchUniqueObject(in: context, where: predicate, affectedStores: affectedStores) guard let object = uniqueObject else { let newObject = Self(context: context) @@ -90,7 +98,9 @@ extension NSManagedObjectContext { /// - changes: Changes to be applied in the current context before the saving operation. If they fail throwing an execption, the context will be reset. /// - completion: Block executed (on the context’s queue.) at the end of the saving operation. @available(*, deprecated, message: "Deprecated.") - public final func performSave(after changes: @escaping (NSManagedObjectContext) throws -> Void, completion: ( (NSError?) -> Void )? = nil ) { + public final func performSave( + after changes: @escaping (NSManagedObjectContext) throws -> Void, completion: ((NSError?) -> Void)? = nil + ) { // https://stackoverflow.com/questions/37837979/using-weak-strong-self-usage-in-block-core-data-swift // `perform` executes the block and then releases it. // In Swift terms it is @noescape (and in the future it may be marked that way and you won't need to use self. in noescape closures). diff --git a/Tests/Utils/InMemoryTestCase.swift b/Tests/Utils/InMemoryTestCase.swift index 1fd1573e..c307be9b 100644 --- a/Tests/Utils/InMemoryTestCase.swift +++ b/Tests/Utils/InMemoryTestCase.swift @@ -1,7 +1,8 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus // MARK: - In Memory XCTestCase @@ -30,20 +31,21 @@ final class InMemoryPersistentContainer: NSPersistentContainer { // all the SQLite stores with that URL in the same process will connect to the shared in memory database // Different coordinators sharing the same in memory store will also dispatch remote change notifications to // each other - var url = URL(fileURLWithPath: "/dev/null") // it's the same URL we get by default when we create a description like so: let description = NSPersistentStoreDescription() + + // "/dev/null" it's the same URL we get by default when we create a description like so: let description = NSPersistentStoreDescription() + var url = URL(fileURLWithPath: "/dev/null") if let named = named { url.appendPathComponent(named) } - let container = InMemoryPersistentContainer(name: "SampleModel", managedObjectModel: model) + let container = InMemoryPersistentContainer(name: "SampleModel", managedObjectModel: model1) let description = container.persistentStoreDescriptions.first! description.url = url //description.type = NSInMemoryStoreType // Setting this value will fail some tests at the moment // Enable history tracking and remote notifications container.persistentStoreDescriptions[0].setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) - if #available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { - container.persistentStoreDescriptions[0].setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) - } + container.persistentStoreDescriptions[0].setOption( + true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) container.loadPersistentStores { (description, error) in XCTAssertNil(error) @@ -51,4 +53,3 @@ final class InMemoryPersistentContainer: NSPersistentContainer { return container } } - diff --git a/Tests/Utils/OnDiskTestCase.swift b/Tests/Utils/OnDiskTestCase.swift index 59513e46..b2ba5471 100644 --- a/Tests/Utils/OnDiskTestCase.swift +++ b/Tests/Utils/OnDiskTestCase.swift @@ -1,7 +1,8 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus // MARK: - On Disk XCTestCase @@ -37,7 +38,7 @@ final class OnDiskPersistentContainer: NSPersistentContainer { static func makeNew(id: UUID) -> OnDiskPersistentContainer { let url = URL.newDatabaseURL(withID: id) - let container = OnDiskPersistentContainer(name: "SampleModel", managedObjectModel: model) + let container = OnDiskPersistentContainer(name: "SampleModel", managedObjectModel: model1) let description = container.persistentStoreDescriptions.first! description.url = url // disable automatic migration (true by default) @@ -47,9 +48,8 @@ final class OnDiskPersistentContainer: NSPersistentContainer { // Enable history tracking and remote notifications container.persistentStoreDescriptions[0].setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) - if #available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { - container.persistentStoreDescriptions[0].setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) - } + container.persistentStoreDescriptions[0].setOption( + true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) container.loadPersistentStores { (description, error) in XCTAssertNil(error) diff --git a/Tests/Utils/OnDiskWithProgrammaticallyModelTestCase.swift b/Tests/Utils/OnDiskWithProgrammaticallyModelTestCase.swift index e7ee49eb..cd74653d 100644 --- a/Tests/Utils/OnDiskWithProgrammaticallyModelTestCase.swift +++ b/Tests/Utils/OnDiskWithProgrammaticallyModelTestCase.swift @@ -1,10 +1,10 @@ // CoreDataPlus -import XCTest import CoreData +import XCTest + @testable import CoreDataPlus -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) class OnDiskWithProgrammaticallyModelTestCase: XCTestCase { var container: NSPersistentContainer! @@ -28,20 +28,20 @@ class OnDiskWithProgrammaticallyModelTestCase: XCTestCase { // MARK: - On Disk NSPersistentContainer with Programmatically Model -@available(iOS 13.0, iOSApplicationExtension 13.0, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) final class OnDiskWithProgrammaticallyModelPersistentContainer: NSPersistentContainer { static func makeNew() -> OnDiskWithProgrammaticallyModelPersistentContainer { - Self.makeNew(id: UUID()) + Self.makeNew(id: UUID().uuidString) } - static func makeNew(id: UUID) -> OnDiskWithProgrammaticallyModelPersistentContainer { - let url = URL.newDatabaseURL(withID: id) - let container = OnDiskWithProgrammaticallyModelPersistentContainer(name: "SampleModel2", - managedObjectModel: V1.makeManagedObjectModel()) + static func makeNew(id: String, + forStagedMigration enableStagedMigration: Bool = false, + model: NSManagedObjectModel = V1.makeManagedObjectModel()) -> OnDiskWithProgrammaticallyModelPersistentContainer { + let url = URL.newDatabaseURL(withName: id) + let container = OnDiskWithProgrammaticallyModelPersistentContainer(name: "SampleModel2", managedObjectModel: model) let description = NSPersistentStoreDescription() description.url = url - description.shouldMigrateStoreAutomatically = false - description.shouldInferMappingModelAutomatically = false + description.shouldMigrateStoreAutomatically = enableStagedMigration + description.shouldInferMappingModelAutomatically = enableStagedMigration // Enable history tracking and remote notifications description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) diff --git a/Tests/Utils/Utils.swift b/Tests/Utils/Utils.swift index 0a329cd0..f9341d7d 100644 --- a/Tests/Utils/Utils.swift +++ b/Tests/Utils/Utils.swift @@ -1,9 +1,13 @@ // CoreDataPlus import CoreData + @testable import CoreDataPlus -let model = SampleModelVersion.version1.managedObjectModel() +// It should be fine to mark these as Sendable because they can be shared between different threads. +// https://duckrowing.com/2010/03/11/using-core-data-on-multiple-threads/ +extension NSManagedObjectContext: @unchecked Sendable {} +extension NSManagedObjectModel: @unchecked Sendable {} // MARK: - URL @@ -26,8 +30,8 @@ extension URL { return databaseURL } - static var temporaryDirectoryURL: URL { - let url = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory:true) + static var temporaryDirectory: URL { + let url = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent(bundleIdentifier) .appendingPathComponent(UUID().uuidString) try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) @@ -36,11 +40,19 @@ extension URL { } extension Foundation.Bundle { - fileprivate class Dummy { } + fileprivate class Dummy {} + + static var tests: Bundle { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: Dummy.self) + #endif + } /// Returns the resource bundle associated with the current Swift module. /// Note: the implementation is very close to the one provided by the Swift Package with `Bundle.module` (that is not available for XCTests). - static var tests: Bundle = { + nonisolated(unsafe) static var moduleOldImplementation: Bundle = { let bundleName = "CoreDataPlus_Tests" let candidates = [ @@ -65,7 +77,11 @@ extension Foundation.Bundle { // https://forums.swift.org/t/5-3-resources-support-not-working-on-with-swift-test/40381/10 // https://github.com/apple/swift-package-manager/pull/2905 // https://bugs.swift.org/browse/SR-13560 - let url = Bundle(for: Dummy.self).bundleURL.deletingLastPathComponent().appendingPathComponent(bundleName + ".bundle") + let url = Bundle(for: Dummy.self) + .bundleURL + .deletingLastPathComponent() + .appendingPathComponent(bundleName + ".bundle") + if let bundle = Bundle(url: url) { return bundle } diff --git a/bin/format.sh b/bin/format.sh new file mode 100644 index 00000000..081a0bac --- /dev/null +++ b/bin/format.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +swift-format ../Sources ../Tests --recursive --parallel --in-place --configuration ../.swift-format diff --git a/bin/lint.sh b/bin/lint.sh new file mode 100644 index 00000000..e9e25144 --- /dev/null +++ b/bin/lint.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +swift-format lint ../Sources ../Tests --recursive --parallel --strict --configuration ../.swift-format