├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── danger.yml │ ├── pod-lint.yml │ └── spm.yml ├── .gitignore ├── .ruby-version ├── .spi.yml ├── .swiftlint.yml ├── CHANGELOG.md ├── Dangerfile ├── Example ├── ExampleApp.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ ├── IDETemplateMacros.plist │ │ └── xcschemes │ │ └── ExampleApp.xcscheme ├── ExampleApp │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── CollectionViewCell.swift │ ├── CollectionViewController.swift │ ├── CompanyCellConfig.swift │ ├── CompanyViewController.swift │ ├── EmployeeCellConfig.swift │ ├── EmployeeViewController.swift │ └── Extensions.swift ├── ExampleAppUITests │ └── ExampleAppUITests.swift └── ExampleModel │ ├── ExampleModel.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDETemplateMacros.plist │ └── ExampleModel │ ├── Company.swift │ ├── Employee.swift │ ├── ExampleModel.h │ ├── ExampleModel.xcdatamodeld │ ├── .xccurrentversion │ ├── Version 1.xcdatamodel │ │ └── contents │ ├── Version 2.xcdatamodel │ │ └── contents │ └── Version 3.xcdatamodel │ │ └── contents │ ├── Model.swift │ ├── Version1_to_Version2.xcdatamodeld │ └── Version1_to_Version2.xcdatamodel │ │ └── contents │ ├── Version1_to_Version2.xcmappingmodel │ └── xcmapping.xml │ └── Version2_to_Version3.xcmappingmodel │ └── xcmapping.xml ├── Gemfile ├── Gemfile.lock ├── Guides └── Getting Started.md ├── JSQCoreDataKit.podspec ├── JSQCoreDataKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ └── JSQCoreDataKit.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── CoreDataEntityProtocol.swift ├── CoreDataModel.swift ├── CoreDataStack.swift ├── CoreDataStackProvider.swift ├── FetchedResultsCellConfiguration.swift ├── FetchedResultsController.swift ├── FetchedResultsCoordinator.swift ├── FetchedResultsDiffableDataSource.swift ├── FetchedResultsSupplementaryConfiguration.swift ├── Migrate.swift ├── ModelFileExtension.swift ├── NSManagedObjectContext+Extensions.swift ├── PrivacyInfo.xcprivacy └── StoreType.swift ├── Tests ├── ContextSyncTests.swift ├── CoreDataEntityProtocolTests.swift ├── ExampleModelTests.swift ├── MigrationTests.swift ├── ModelTests.swift ├── ResetStackTests.swift ├── SaveTests.swift ├── StackFactoryTests.swift ├── StackTests.swift ├── StoreTypeTests.swift └── TestCase.swift ├── docs ├── Classes.html ├── Classes │ └── CoreDataStack.html ├── Enums.html ├── Enums │ ├── MigrationError.html │ ├── ModelFileExtension.html │ └── StoreType.html ├── Extensions.html ├── Extensions │ └── NSManagedObjectContext.html ├── Guides.html ├── Protocols.html ├── Protocols │ └── CoreDataEntityProtocol.html ├── Structs.html ├── Structs │ ├── CoreDataModel.html │ └── CoreDataStackProvider.html ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── getting-started.html ├── img │ ├── carat.png │ ├── dash.png │ ├── gh.png │ └── spinner.gif ├── index.html ├── js │ ├── jazzy.js │ ├── jazzy.search.js │ ├── jquery.min.js │ ├── lunr.min.js │ └── typeahead.jquery.js ├── search.json └── undocumented.json └── scripts ├── build_docs.zsh └── lint.zsh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "bundler" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | labels: [] 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | labels: [] 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Virtual Environments 2 | # https://github.com/actions/virtual-environments/ 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | env: 15 | PROJECT: JSQCoreDataKit.xcodeproj 16 | SCHEME: JSQCoreDataKit 17 | 18 | EXAMPLE_PROJECT: Example/ExampleApp.xcodeproj 19 | EXAMPLE_SCHEME: ExampleApp 20 | 21 | DEVELOPER_DIR: /Applications/Xcode_15.1.app/Contents/Developer 22 | 23 | IOS_DEST: "platform=iOS Simulator,name=iPhone 15,OS=latest" 24 | TVOS_DEST: "platform=tvOS Simulator,name=Apple TV,OS=latest" 25 | WATCHOS_DEST: "platform=watchOS Simulator,name=Apple Watch Series 9 (41mm),OS=latest" 26 | MACOS_DEST: "platform=macOS,arch=x86_64" 27 | 28 | jobs: 29 | env-details: 30 | name: Environment details 31 | runs-on: macos-13 32 | steps: 33 | - name: xcode version 34 | run: xcodebuild -version -sdk 35 | 36 | - name: list simulators 37 | run: | 38 | xcrun simctl delete unavailable 39 | xcrun simctl list 40 | 41 | test-iOS: 42 | name: iOS unit test 43 | runs-on: macos-13 44 | steps: 45 | - name: git checkout 46 | uses: actions/checkout@v4 47 | 48 | - name: unit tests 49 | run: | 50 | set -o pipefail 51 | xcodebuild clean test \ 52 | -project "$PROJECT" \ 53 | -scheme "$SCHEME" \ 54 | -destination "$IOS_DEST" \ 55 | CODE_SIGN_IDENTITY="-" | xcpretty -c 56 | 57 | test-tvOS: 58 | name: tvOS unit test 59 | runs-on: macos-13 60 | steps: 61 | - name: git checkout 62 | uses: actions/checkout@v4 63 | 64 | - name: unit tests 65 | run: | 66 | set -o pipefail 67 | xcodebuild clean test \ 68 | -project "$PROJECT" \ 69 | -scheme "$SCHEME" \ 70 | -destination "$TVOS_DEST" \ 71 | CODE_SIGN_IDENTITY="-" | xcpretty -c 72 | 73 | test-watchOS: 74 | name: watchOS unit test 75 | runs-on: macos-13 76 | steps: 77 | - name: git checkout 78 | uses: actions/checkout@v4 79 | 80 | - name: test 81 | run: | 82 | set -o pipefail 83 | xcodebuild clean test \ 84 | -project "$PROJECT" \ 85 | -scheme "$SCHEME" \ 86 | -destination "$WATCHOS_DEST" \ 87 | CODE_SIGN_IDENTITY="-" | xcpretty -c 88 | 89 | test-macOS: 90 | name: macOS unit test 91 | runs-on: macos-13 92 | steps: 93 | - name: git checkout 94 | uses: actions/checkout@v4 95 | 96 | - name: unit tests 97 | run: | 98 | set -o pipefail 99 | xcodebuild clean test \ 100 | -project "$PROJECT" \ 101 | -scheme "$SCHEME" \ 102 | -destination "$MACOS_DEST" \ 103 | CODE_SIGN_IDENTITY="-" | xcpretty -c 104 | 105 | test-example: 106 | name: Example Project Test 107 | runs-on: macos-13 108 | steps: 109 | - name: git checkout 110 | uses: actions/checkout@v4 111 | 112 | - name: example tests 113 | run: | 114 | set -o pipefail 115 | xcodebuild clean test \ 116 | -project "$EXAMPLE_PROJECT" \ 117 | -scheme "$EXAMPLE_SCHEME" \ 118 | -destination "$IOS_DEST" \ 119 | CODE_SIGN_IDENTITY="-" | xcpretty -c 120 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Virtual Environments 2 | # https://github.com/actions/virtual-environments/ 3 | 4 | name: Danger 5 | 6 | on: 7 | pull_request: 8 | types: [synchronize, opened, reopened, labeled, unlabeled, edited] 9 | 10 | env: 11 | DEVELOPER_DIR: /Applications/Xcode_15.1.app/Contents/Developer 12 | 13 | jobs: 14 | main: 15 | name: Review, Lint, Verify 16 | runs-on: macos-13 17 | steps: 18 | - name: git checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: ruby versions 22 | run: | 23 | ruby --version 24 | gem --version 25 | bundler --version 26 | 27 | # https://github.com/ruby/setup-ruby 28 | - name: ruby setup 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | bundler-cache: true 32 | 33 | - name: jazzy docs 34 | run: bundle exec jazzy --output docs/ 35 | 36 | - name: danger 37 | env: 38 | DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} 39 | run: bundle exec danger --verbose 40 | -------------------------------------------------------------------------------- /.github/workflows/pod-lint.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Virtual Environments 2 | # https://github.com/actions/virtual-environments/ 3 | 4 | name: Pod Lint 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | env: 15 | DEVELOPER_DIR: /Applications/Xcode_15.1.app/Contents/Developer 16 | 17 | jobs: 18 | main: 19 | name: Pod Lint 20 | runs-on: macos-13 21 | steps: 22 | - name: git checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: ruby versions 26 | run: | 27 | ruby --version 28 | gem --version 29 | bundler --version 30 | 31 | # https://github.com/ruby/setup-ruby 32 | - name: ruby setup 33 | uses: ruby/setup-ruby@v1 34 | with: 35 | bundler-cache: true 36 | 37 | - name: pod lint 38 | run: bundle exec pod lib lint --verbose 39 | -------------------------------------------------------------------------------- /.github/workflows/spm.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Virtual Environments 2 | # https://github.com/actions/virtual-environments/ 3 | 4 | name: SwiftPM Integration 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | env: 15 | DEVELOPER_DIR: /Applications/Xcode_15.1.app/Contents/Developer 16 | 17 | jobs: 18 | main: 19 | name: SwiftPM Build 20 | runs-on: macos-13 21 | steps: 22 | - name: git checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: xcode version 26 | run: xcodebuild -version -sdk 27 | 28 | - name: swift build 29 | run: swift build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # swiftpm 4 | .swiftpm 5 | .build 6 | 7 | # docs 8 | docs/docsets/ 9 | 10 | # Xcode 11 | # 12 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 13 | 14 | ## User settings 15 | xcuserdata/ 16 | 17 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 18 | *.xcscmblueprint 19 | *.xccheckout 20 | 21 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 22 | build/ 23 | DerivedData/ 24 | *.moved-aside 25 | *.pbxuser 26 | !default.pbxuser 27 | *.mode1v3 28 | !default.mode1v3 29 | *.mode2v3 30 | !default.mode2v3 31 | *.perspectivev3 32 | !default.perspectivev3 33 | 34 | ## Obj-C/Swift specific 35 | *.hmap 36 | 37 | ## App packaging 38 | *.ipa 39 | *.dSYM.zip 40 | *.dSYM 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | # 50 | # Add this line if you want to avoid checking in source code from the Xcode workspace 51 | # *.xcworkspace 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build/ 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. 63 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots/**/*.png 70 | fastlane/test_output 71 | 72 | # Code Injection 73 | # 74 | # After new code Injection tools there's a generated folder /iOSInjectionProject 75 | # https://github.com/johnno1962/injectionforxcode 76 | 77 | iOSInjectionProject/ 78 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.0 2 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: JSQCoreDataKit 5 | platform: ios 6 | - documentation_targets: JSQCoreDataKit 7 | platform: macos 8 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # SwiftLint configuration 2 | # Rule directory: https://realm.github.io/SwiftLint/rule-directory.html 3 | # GitHub: https://github.com/realm/SwiftLint 4 | 5 | excluded: 6 | - Pods 7 | - docs 8 | - build 9 | - scripts 10 | - swiftpm 11 | - .bundle 12 | - vendor 13 | 14 | disabled_rules: 15 | - force_cast 16 | - type_name 17 | 18 | opt_in_rules: 19 | # performance 20 | - empty_count 21 | - first_where 22 | - sorted_first_last 23 | - contains_over_first_not_nil 24 | - last_where 25 | - reduce_into 26 | - contains_over_filter_count 27 | - contains_over_filter_is_empty 28 | - empty_collection_literal 29 | 30 | # idiomatic 31 | - fatal_error_message 32 | - xctfail_message 33 | - discouraged_object_literal 34 | - discouraged_optional_boolean 35 | - discouraged_optional_collection 36 | - for_where 37 | - function_default_parameter_at_end 38 | - legacy_random 39 | - no_extension_access_modifier 40 | - redundant_type_annotation 41 | - static_operator 42 | - toggle_bool 43 | - unavailable_function 44 | - no_space_in_method_call 45 | - discouraged_assert 46 | # - legacy_objc_type 47 | # - file_name 48 | - file_name_no_space 49 | - discouraged_none_name 50 | - return_value_from_void_function 51 | - prefer_zero_over_explicit_init 52 | - shorthand_optional_binding 53 | - xct_specific_matcher 54 | - unneeded_synthesized_initializer 55 | 56 | # style 57 | - attributes 58 | - number_separator 59 | - operator_usage_whitespace 60 | - sorted_imports 61 | - vertical_parameter_alignment_on_call 62 | - void_return 63 | - closure_spacing 64 | - empty_enum_arguments 65 | - implicit_return 66 | - modifier_order 67 | - multiline_arguments 68 | - multiline_parameters 69 | - trailing_closure 70 | - unneeded_parentheses_in_closure_argument 71 | - vertical_whitespace_between_cases 72 | - prefer_self_in_static_references 73 | - comma_inheritance 74 | - direct_return 75 | - period_spacing 76 | - superfluous_else 77 | # - sorted_enum_cases 78 | - non_overridable_class_declaration 79 | 80 | # lint 81 | - overridden_super_call 82 | - override_in_extension 83 | - yoda_condition 84 | - array_init 85 | - empty_xctest_method 86 | - identical_operands 87 | - prohibited_super_call 88 | - duplicate_enum_cases 89 | - legacy_multiple 90 | - accessibility_label_for_image 91 | - lower_acl_than_parent 92 | - unhandled_throwing_task 93 | - private_swiftui_state 94 | 95 | # Rules run by `swiftlint analyze` (experimental) 96 | analyzer_rules: 97 | # - unused_import 98 | - unused_declaration 99 | - explicit_self 100 | - capture_variable 101 | 102 | line_length: 200 103 | file_length: 600 104 | 105 | type_body_length: 500 106 | function_body_length: 250 107 | 108 | cyclomatic_complexity: 10 109 | 110 | nesting: 111 | type_level: 2 112 | function_level: 2 113 | check_nesting_in_closures_and_statements: true 114 | always_allow_one_type_in_functions: false 115 | 116 | identifier_name: 117 | allowed_symbols: ['_'] 118 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Changed library files, but didn't add/update tests 3 | # ----------------------------------------------------------------------------- 4 | all_changed_files = (git.added_files + git.modified_files + git.deleted_files) 5 | 6 | has_source_changes = !all_changed_files.grep(/Sources/).empty? 7 | has_test_changes = !all_changed_files.grep(/Tests/).empty? 8 | if has_source_changes && !has_test_changes 9 | warn("Library files were updated without test coverage. Please update or add tests, if possible!") 10 | end 11 | 12 | # ----------------------------------------------------------------------------- 13 | # Pull request is too large to review 14 | # ----------------------------------------------------------------------------- 15 | if git.lines_of_code > 600 16 | warn("This is a large pull request! Can you break it up into multiple smaller ones instead?") 17 | end 18 | 19 | # ----------------------------------------------------------------------------- 20 | # All pull requests need a description 21 | # ----------------------------------------------------------------------------- 22 | if github.pr_body.length < 25 23 | fail("Please provide a detailed summary in the pull request description.") 24 | end 25 | 26 | # ----------------------------------------------------------------------------- 27 | # All pull requests should be submitted to main branch 28 | # ----------------------------------------------------------------------------- 29 | if github.branch_for_base != "main" 30 | warn("Pull requests should be submitted to the main branch only.") 31 | end 32 | 33 | # ----------------------------------------------------------------------------- 34 | # CHANGELOG entries are required for changes to library files 35 | # ----------------------------------------------------------------------------- 36 | no_changelog_entry = !git.modified_files.include?("CHANGELOG.md") 37 | if has_source_changes && no_changelog_entry 38 | warn("There is no CHANGELOG entry. Do you need to add one?") 39 | end 40 | 41 | # ----------------------------------------------------------------------------- 42 | # Milestones are required for all PRs to track what's included in each release 43 | # ----------------------------------------------------------------------------- 44 | has_milestone = github.pr_json["milestone"] != nil 45 | warn('All pull requests should have a milestone.', sticky: false) unless has_milestone 46 | 47 | # ----------------------------------------------------------------------------- 48 | # Docs are regenerated when releasing 49 | # ----------------------------------------------------------------------------- 50 | has_doc_changes = !git.modified_files.grep(/docs\//).empty? 51 | if has_doc_changes 52 | fail("Documentation cannot be edited directly.") 53 | message(%(Docs are automatically regenerated when creating new releases using [Jazzy](https://github.com/realm/jazzy). 54 | If you want to update docs, please update the doc comments or markdown files directly.)) 55 | end 56 | 57 | # ----------------------------------------------------------------------------- 58 | # Verify correct `pod install` and `bundle install` 59 | # ----------------------------------------------------------------------------- 60 | def files_changed_as_set(files) 61 | changed_files = files.select { |file| git.modified_files.include? file } 62 | not_changed_files = files.select { |file| !changed_files.include? file } 63 | all_files_changed = not_changed_files.empty? 64 | no_files_changed = changed_files.empty? 65 | return all_files_changed || no_files_changed 66 | end 67 | 68 | # Verify correct `pod install` 69 | pod_locks = ["Podfile.lock", "Pods/Manifest.lock"] 70 | pod_files = ["Podfile"] + pod_locks 71 | 72 | # If Podfile has been modified, so must the lock files. 73 | did_update_podfile = git.modified_files.include?("Podfile") 74 | if did_update_podfile && !files_changed_as_set(pod_files) 75 | fail("CocoaPods error: #{pod_files} should all be changed at the same time. 76 | Run `pod install` and commit the changes to fix.") 77 | end 78 | 79 | # Podfile has not been modified. We must be running `pod update`. 80 | # Only the two lock files must be changed together. 81 | if !did_update_podfile && !files_changed_as_set(pod_locks) 82 | fail("CocoaPods error: #{pod_locks} should all be changed at the same time. 83 | Run `pod install` and commit the changes to fix.") 84 | end 85 | 86 | # Prevent editing `Pods/` source directly. 87 | # If Pods has changed, then Podfile.lock must have changed too. 88 | has_modified_pods = !(git.added_files + git.modified_files + git.deleted_files).grep(/Pods/).empty? 89 | did_update_podlock = git.modified_files.include?("Podfile.lock") 90 | if has_modified_pods && !did_update_podlock 91 | fail("It looks like you are modifying CocoaPods source in `Pods/`. 3rd-party dependencies should not be edited. 92 | To update or change pods, please update the `Podfile` and run `pod install`.") 93 | end 94 | 95 | # Verify correct `bundle install` 96 | # If Gemfile has been modified, so must the lock file. 97 | did_update_gemfile = git.modified_files.include?("Gemfile") 98 | gem_files = ["Gemfile", "Gemfile.lock"] 99 | if did_update_gemfile && !files_changed_as_set(gem_files) 100 | fail("Bundler error: #{gem_files} should all be changed at the same time. 101 | Run `bundle install` and commit the changes to fix.") 102 | end 103 | 104 | # ----------------------------------------------------------------------------- 105 | # Run SwiftLint 106 | # ----------------------------------------------------------------------------- 107 | swiftlint.verbose = true 108 | swiftlint.config_file = './.swiftlint.yml' 109 | swiftlint.lint_files(inline_mode: true, fail_on_error: true) 110 | 111 | # ----------------------------------------------------------------------------- 112 | # Jazzy docs - check for new, undocumented symbols 113 | # ----------------------------------------------------------------------------- 114 | jazzy.check fail: :all 115 | -------------------------------------------------------------------------------- /Example/ExampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Example/ExampleApp.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Created by Jesse Squires 8 | // https://www.jessesquires.com 9 | // 10 | // 11 | // Documentation 12 | // https://jessesquires.github.io/JSQCoreDataKit 13 | // 14 | // 15 | // GitHub 16 | // https://github.com/jessesquires/JSQCoreDataKit 17 | // 18 | // 19 | // License 20 | // Copyright © 2015-present Jesse Squires 21 | // Released under an MIT license: https://opensource.org/licenses/MIT 22 | // 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 65 | 66 | 67 | 68 | 72 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 115 | 116 | 122 | 123 | 124 | 125 | 127 | 128 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /Example/ExampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import UIKit 20 | 21 | @UIApplicationMain 22 | final class AppDelegate: UIResponder, UIApplicationDelegate { 23 | let window = UIWindow() 24 | 25 | func application(_ application: UIApplication, 26 | // swiftlint:disable:next discouraged_optional_collection 27 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 28 | let viewController = CompanyViewController() 29 | let navigationController = UINavigationController(rootViewController: viewController) 30 | self.window.rootViewController = navigationController 31 | self.window.makeKeyAndVisible() 32 | return true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Example/ExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/ExampleApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Example/ExampleApp/CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import UIKit 20 | 21 | final class CollectionViewCell: UICollectionViewListCell { 22 | 23 | func configure(primaryText: String, secondaryText: String) { 24 | var contentConfiguration = UIListContentConfiguration.subtitleCell() 25 | contentConfiguration.text = primaryText 26 | contentConfiguration.secondaryText = secondaryText 27 | self.contentConfiguration = contentConfiguration 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Example/ExampleApp/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import UIKit 20 | 21 | class CollectionViewController: UICollectionViewController { 22 | 23 | init() { 24 | super.init(collectionViewLayout: UICollectionViewCompositionalLayout.list()) 25 | } 26 | 27 | @available(*, unavailable) 28 | required init?(coder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | self.navigationItem.rightBarButtonItems = [ 35 | UIBarButtonItem.add(target: self, selector: #selector(didTapAdd(_:))), 36 | UIBarButtonItem.trash(target: self, selector: #selector(didTapDelete(_:))) 37 | ] 38 | } 39 | 40 | @objc 41 | private func didTapAdd(_ sender: UIBarButtonItem) { 42 | self.addAction() 43 | } 44 | 45 | func addAction() { } 46 | 47 | @objc 48 | private func didTapDelete(_ sender: UIBarButtonItem) { 49 | self.deleteAction() 50 | } 51 | 52 | func deleteAction() { } 53 | } 54 | -------------------------------------------------------------------------------- /Example/ExampleApp/CompanyCellConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | import JSQCoreDataKit 22 | import UIKit 23 | 24 | struct CompanyCellConfig: FetchedResultsCellConfiguration { 25 | 26 | func configure(cell: CollectionViewCell, with object: Company) { 27 | cell.configure(primaryText: object.name, secondaryText: "$\(object.profits).00") 28 | cell.accessories = [.disclosureIndicator()] 29 | } 30 | } 31 | 32 | struct CompanyHeaderConfig: FetchedResultsSupplementaryConfiguration { 33 | let kind = UICollectionView.elementKindSectionHeader 34 | 35 | func configure(view: UICollectionViewCell, with object: Company?, in section: NSFetchedResultsSectionInfo) { 36 | var contentConfiguration = UIListContentConfiguration.groupedHeader() 37 | contentConfiguration.text = "All Companies" 38 | view.contentConfiguration = contentConfiguration 39 | view.isHidden = (section.numberOfObjects == 0) 40 | } 41 | } 42 | 43 | struct CompanyFooterConfig: FetchedResultsSupplementaryConfiguration { 44 | let kind = UICollectionView.elementKindSectionFooter 45 | 46 | func configure(view: UICollectionViewCell, with object: Company?, in section: NSFetchedResultsSectionInfo) { 47 | var contentConfiguration = UIListContentConfiguration.groupedFooter() 48 | contentConfiguration.text = "There are \(section.numberOfObjects) companies." 49 | view.contentConfiguration = contentConfiguration 50 | view.isHidden = (section.numberOfObjects == 0) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Example/ExampleApp/CompanyViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | import JSQCoreDataKit 22 | import UIKit 23 | 24 | final class CompanyViewController: CollectionViewController { 25 | 26 | var stack: CoreDataStack! 27 | 28 | var coordinator: FetchedResultsCoordinator? 29 | 30 | // MARK: View lifecycle 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | self.title = "JSQCoreDataKit" 35 | 36 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 37 | let provider = CoreDataStackProvider(model: model) 38 | provider.createStack { result in 39 | switch result { 40 | case .success(let stack): 41 | self.stack = stack 42 | self.setupCoordinator() 43 | self.coordinator?.performFetch() 44 | 45 | case .failure(let err): 46 | assertionFailure("Error creating stack: \(err)") 47 | } 48 | } 49 | } 50 | 51 | // MARK: Actions 52 | 53 | override func addAction() { 54 | self.stack.mainContext.performAndWait { 55 | _ = Company.newCompany(self.stack.mainContext) 56 | self.stack.mainContext.saveSync() 57 | } 58 | } 59 | 60 | override func deleteAction() { 61 | let backgroundChildContext = stack.childContext(concurrencyType: .privateQueueConcurrencyType) 62 | backgroundChildContext.performAndWait { 63 | do { 64 | let objects = try backgroundChildContext.fetch(Company.fetchRequest) 65 | for each in objects { 66 | backgroundChildContext.delete(each) 67 | } 68 | backgroundChildContext.saveSync() 69 | } catch { 70 | print("Error deleting objects: \(error)") 71 | } 72 | } 73 | } 74 | 75 | // MARK: Helpers 76 | 77 | func setupCoordinator() { 78 | let supplementaryViews = [ 79 | AnyFetchedResultsSupplementaryConfiguration(CompanyHeaderConfig()), 80 | AnyFetchedResultsSupplementaryConfiguration(CompanyFooterConfig()) 81 | ] 82 | 83 | self.coordinator = FetchedResultsCoordinator( 84 | fetchRequest: Company.fetchRequest, 85 | context: self.stack.mainContext, 86 | sectionNameKeyPath: nil, 87 | cacheName: nil, 88 | collectionView: self.collectionView, 89 | cellConfiguration: CompanyCellConfig(), 90 | supplementaryConfigurations: supplementaryViews 91 | ) 92 | } 93 | 94 | // MARK: Collection view delegate 95 | 96 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 97 | let company = self.coordinator![indexPath] 98 | let employeeVC = EmployeeViewController(stack: self.stack, company: company) 99 | self.navigationController?.pushViewController(employeeVC, animated: true) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Example/ExampleApp/EmployeeCellConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | import JSQCoreDataKit 22 | import UIKit 23 | 24 | struct EmployeeCellConfig: FetchedResultsCellConfiguration { 25 | 26 | func configure(cell: CollectionViewCell, with object: Employee) { 27 | cell.configure(primaryText: object.name, secondaryText: "$\(object.salary).00") 28 | } 29 | } 30 | 31 | struct EmployeeHeaderConfig: FetchedResultsSupplementaryConfiguration { 32 | let kind = UICollectionView.elementKindSectionHeader 33 | 34 | func configure(view: UICollectionViewCell, with object: Employee?, in section: NSFetchedResultsSectionInfo) { 35 | var contentConfiguration = UIListContentConfiguration.groupedHeader() 36 | contentConfiguration.text = "All Employees" 37 | view.contentConfiguration = contentConfiguration 38 | view.isHidden = (section.numberOfObjects == 0) 39 | } 40 | } 41 | 42 | struct EmployeeFooterConfig: FetchedResultsSupplementaryConfiguration { 43 | let kind = UICollectionView.elementKindSectionFooter 44 | 45 | func configure(view: UICollectionViewCell, with object: Employee?, in section: NSFetchedResultsSectionInfo) { 46 | var contentConfiguration = UIListContentConfiguration.groupedFooter() 47 | contentConfiguration.text = "There are \(section.numberOfObjects) employees." 48 | view.contentConfiguration = contentConfiguration 49 | view.isHidden = (section.numberOfObjects == 0) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Example/ExampleApp/EmployeeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | import JSQCoreDataKit 22 | import UIKit 23 | 24 | final class EmployeeViewController: CollectionViewController { 25 | 26 | let stack: CoreDataStack 27 | let company: Company 28 | 29 | lazy var coordinator: FetchedResultsCoordinator = { 30 | let supplementaryViews = [ 31 | AnyFetchedResultsSupplementaryConfiguration(EmployeeHeaderConfig()), 32 | AnyFetchedResultsSupplementaryConfiguration(EmployeeFooterConfig()) 33 | ] 34 | return FetchedResultsCoordinator( 35 | fetchRequest: Employee.fetchRequest(for: self.company), 36 | context: self.stack.mainContext, 37 | sectionNameKeyPath: nil, 38 | cacheName: nil, 39 | collectionView: self.collectionView, 40 | cellConfiguration: EmployeeCellConfig(), 41 | supplementaryConfigurations: supplementaryViews 42 | ) 43 | }() 44 | 45 | // MARK: Init 46 | 47 | init(stack: CoreDataStack, company: Company) { 48 | self.stack = stack 49 | self.company = company 50 | super.init() 51 | } 52 | 53 | // MARK: View lifecycle 54 | 55 | override func viewDidLoad() { 56 | super.viewDidLoad() 57 | self.title = self.company.name 58 | self.collectionView.allowsSelection = false 59 | } 60 | 61 | override func viewWillAppear(_ animated: Bool) { 62 | super.viewWillAppear(animated) 63 | self.coordinator.performFetch() 64 | } 65 | 66 | // MARK: Actions 67 | 68 | override func addAction() { 69 | self.stack.mainContext.performAndWait { 70 | _ = Employee.newEmployee(self.stack.mainContext, company: self.company) 71 | self.stack.mainContext.saveSync() 72 | } 73 | } 74 | 75 | override func deleteAction() { 76 | let backgroundChildContext = self.stack.childContext() 77 | backgroundChildContext.performAndWait { 78 | do { 79 | let objects = try backgroundChildContext.fetch(Employee.fetchRequest(for: self.company)) 80 | for each in objects { 81 | backgroundChildContext.delete(each) 82 | } 83 | backgroundChildContext.saveSync() 84 | } catch { 85 | print("Error deleting objects: \(error)") 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Example/ExampleApp/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import Foundation 20 | import UIKit 21 | 22 | extension UICollectionViewCompositionalLayout { 23 | 24 | static func list() -> UICollectionViewCompositionalLayout { 25 | UICollectionViewCompositionalLayout { _, layoutEnvironment in 26 | var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) 27 | configuration.headerMode = .supplementary 28 | configuration.footerMode = .supplementary 29 | return NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) 30 | } 31 | } 32 | } 33 | 34 | extension UIBarButtonItem { 35 | 36 | static func add(target: Any, selector: Selector) -> UIBarButtonItem { 37 | UIBarButtonItem(barButtonSystemItem: .add, target: target, action: selector) 38 | } 39 | 40 | static func trash(target: Any, selector: Selector) -> UIBarButtonItem { 41 | UIBarButtonItem(barButtonSystemItem: .trash, target: target, action: selector) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Example/ExampleAppUITests/ExampleAppUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import XCTest 20 | 21 | final class ExampleAppUITests: XCTestCase { 22 | 23 | override func setUpWithError() throws { 24 | continueAfterFailure = false 25 | try super.setUpWithError() 26 | } 27 | 28 | func testExample() throws { 29 | let app = XCUIApplication() 30 | app.launch() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Created by Jesse Squires 8 | // https://www.jessesquires.com 9 | // 10 | // 11 | // Documentation 12 | // https://jessesquires.github.io/JSQCoreDataKit 13 | // 14 | // 15 | // GitHub 16 | // https://github.com/jessesquires/JSQCoreDataKit 17 | // 18 | // 19 | // License 20 | // Copyright © 2015-present Jesse Squires 21 | // Released under an MIT license: https://opensource.org/licenses/MIT 22 | // 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/Company.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import Foundation 21 | import JSQCoreDataKit 22 | 23 | public final class Company: NSManagedObject, CoreDataEntityProtocol { 24 | 25 | // MARK: CoreDataEntityProtocol 26 | 27 | public static let defaultSortDescriptors = [NSSortDescriptor(key: "profits", ascending: true), 28 | NSSortDescriptor(key: "name", ascending: true) ] 29 | 30 | // MARK: Properties 31 | 32 | @NSManaged public var name: String 33 | @NSManaged public var dateFounded: Date 34 | @NSManaged public var profits: NSDecimalNumber 35 | @NSManaged public var employees: NSSet 36 | 37 | // MARK: Init 38 | 39 | public init(context: NSManagedObjectContext, 40 | name: String, 41 | dateFounded: Date, 42 | profits: NSDecimalNumber) { 43 | super.init(entity: Self.entity(context: context), insertInto: context) 44 | self.name = name 45 | self.dateFounded = dateFounded 46 | self.profits = profits 47 | } 48 | 49 | @objc 50 | override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { 51 | super.init(entity: entity, insertInto: context) 52 | } 53 | 54 | public static func newCompany(_ context: NSManagedObjectContext) -> Company { 55 | let name = "Company " + String(UUID().uuidString.split { $0 == "-" }.first!) 56 | return Self(context: context, 57 | name: name, 58 | dateFounded: Date.distantPast, 59 | profits: NSDecimalNumber(value: Int.random(in: 0...1_000_000))) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/Employee.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import Foundation 21 | import JSQCoreDataKit 22 | 23 | public final class Employee: NSManagedObject, CoreDataEntityProtocol { 24 | 25 | // MARK: CoreDataEntityProtocol 26 | 27 | public static let defaultSortDescriptors = [ NSSortDescriptor(key: "name", ascending: true) ] 28 | 29 | // MARK: Properties 30 | 31 | @NSManaged public var name: String 32 | @NSManaged public var birthDate: Date 33 | @NSManaged public var salary: NSDecimalNumber 34 | @NSManaged public var company: Company? 35 | 36 | // MARK: Init 37 | 38 | public init(context: NSManagedObjectContext, 39 | name: String, 40 | birthDate: Date, 41 | salary: NSDecimalNumber, 42 | company: Company? = nil) { 43 | super.init(entity: Self.entity(context: context), insertInto: context) 44 | self.name = name 45 | self.birthDate = birthDate 46 | self.salary = salary 47 | self.company = company 48 | } 49 | 50 | @objc 51 | override private init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { 52 | super.init(entity: entity, insertInto: context) 53 | } 54 | 55 | public static func newEmployee(_ context: NSManagedObjectContext, company: Company? = nil) -> Employee { 56 | let name = "Employee " + String(UUID().uuidString.split { $0 == "-" }.first!) 57 | return Self(context: context, 58 | name: name, 59 | birthDate: Date.distantPast, 60 | salary: NSDecimalNumber(value: Int.random(in: 0...100_000)), 61 | company: company) 62 | } 63 | 64 | public static func fetchRequest(for company: Company) -> NSFetchRequest { 65 | let fetch = self.fetchRequest 66 | fetch.predicate = NSPredicate(format: "company == %@", company) 67 | return fetch 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/ExampleModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | #import 20 | 21 | FOUNDATION_EXPORT double ExampleModelVersionNumber; 22 | 23 | FOUNDATION_EXPORT const unsigned char ExampleModelVersionString[]; 24 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/ExampleModel.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Version 3.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/ExampleModel.xcdatamodeld/Version 1.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/ExampleModel.xcdatamodeld/Version 2.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/ExampleModel.xcdatamodeld/Version 3.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import Foundation 20 | 21 | public let modelName = "ExampleModel" 22 | 23 | public let modelBundle = Bundle(identifier: "com.hexedbits.ExampleModel")! 24 | -------------------------------------------------------------------------------- /Example/ExampleModel/ExampleModel/Version1_to_Version2.xcdatamodeld/Version1_to_Version2.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # https://github.com/CocoaPods/CocoaPods 4 | gem 'cocoapods', '~> 1.16' 5 | 6 | # https://github.com/realm/jazzy 7 | gem 'jazzy' 8 | 9 | # Danger 10 | # https://danger.systems/ruby/ 11 | gem 'danger', '~> 9.5' 12 | gem 'danger-swiftlint' 13 | gem 'danger-jazzy' 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.2.2.1) 9 | base64 10 | benchmark (>= 0.3) 11 | bigdecimal 12 | concurrent-ruby (~> 1.0, >= 1.3.1) 13 | connection_pool (>= 2.2.5) 14 | drb 15 | i18n (>= 1.6, < 2) 16 | logger (>= 1.4.2) 17 | minitest (>= 5.1) 18 | securerandom (>= 0.3) 19 | tzinfo (~> 2.0, >= 2.0.5) 20 | addressable (2.8.7) 21 | public_suffix (>= 2.0.2, < 7.0) 22 | algoliasearch (1.27.5) 23 | httpclient (~> 2.8, >= 2.8.3) 24 | json (>= 1.5.1) 25 | atomos (0.1.3) 26 | base64 (0.2.0) 27 | benchmark (0.4.0) 28 | bigdecimal (3.1.9) 29 | claide (1.1.0) 30 | claide-plugins (0.9.2) 31 | cork 32 | nap 33 | open4 (~> 1.3) 34 | cocoapods (1.16.2) 35 | addressable (~> 2.8) 36 | claide (>= 1.0.2, < 2.0) 37 | cocoapods-core (= 1.16.2) 38 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 39 | cocoapods-downloader (>= 2.1, < 3.0) 40 | cocoapods-plugins (>= 1.0.0, < 2.0) 41 | cocoapods-search (>= 1.0.0, < 2.0) 42 | cocoapods-trunk (>= 1.6.0, < 2.0) 43 | cocoapods-try (>= 1.1.0, < 2.0) 44 | colored2 (~> 3.1) 45 | escape (~> 0.0.4) 46 | fourflusher (>= 2.3.0, < 3.0) 47 | gh_inspector (~> 1.0) 48 | molinillo (~> 0.8.0) 49 | nap (~> 1.0) 50 | ruby-macho (>= 2.3.0, < 3.0) 51 | xcodeproj (>= 1.27.0, < 2.0) 52 | cocoapods-core (1.16.2) 53 | activesupport (>= 5.0, < 8) 54 | addressable (~> 2.8) 55 | algoliasearch (~> 1.0) 56 | concurrent-ruby (~> 1.1) 57 | fuzzy_match (~> 2.0.4) 58 | nap (~> 1.0) 59 | netrc (~> 0.11) 60 | public_suffix (~> 4.0) 61 | typhoeus (~> 1.0) 62 | cocoapods-deintegrate (1.0.5) 63 | cocoapods-downloader (2.1) 64 | cocoapods-plugins (1.0.0) 65 | nap 66 | cocoapods-search (1.0.1) 67 | cocoapods-trunk (1.6.0) 68 | nap (>= 0.8, < 2.0) 69 | netrc (~> 0.11) 70 | cocoapods-try (1.2.0) 71 | colored2 (3.1.2) 72 | concurrent-ruby (1.3.5) 73 | connection_pool (2.5.0) 74 | cork (0.3.0) 75 | colored2 (~> 3.1) 76 | danger (9.5.1) 77 | base64 (~> 0.2) 78 | claide (~> 1.0) 79 | claide-plugins (>= 0.9.2) 80 | colored2 (~> 3.1) 81 | cork (~> 0.1) 82 | faraday (>= 0.9.0, < 3.0) 83 | faraday-http-cache (~> 2.0) 84 | git (~> 1.13) 85 | kramdown (~> 2.3) 86 | kramdown-parser-gfm (~> 1.0) 87 | octokit (>= 4.0) 88 | pstore (~> 0.1) 89 | terminal-table (>= 1, < 4) 90 | danger-jazzy (1.1.0) 91 | danger-plugin-api (~> 1.0) 92 | json (~> 2.1.0) 93 | danger-plugin-api (1.0.0) 94 | danger (> 2.0) 95 | danger-swiftlint (0.37.1) 96 | danger 97 | rake (> 10) 98 | thor (~> 1.0.0) 99 | drb (2.2.1) 100 | escape (0.0.4) 101 | ethon (0.16.0) 102 | ffi (>= 1.15.0) 103 | faraday (2.12.2) 104 | faraday-net_http (>= 2.0, < 3.5) 105 | json 106 | logger 107 | faraday-http-cache (2.5.1) 108 | faraday (>= 0.8) 109 | faraday-net_http (3.4.0) 110 | net-http (>= 0.5.0) 111 | ffi (1.17.1) 112 | ffi (1.17.1-x86_64-linux-gnu) 113 | fourflusher (2.3.1) 114 | fuzzy_match (2.0.4) 115 | gh_inspector (1.1.3) 116 | git (1.19.1) 117 | addressable (~> 2.8) 118 | rchardet (~> 1.8) 119 | httpclient (2.9.0) 120 | mutex_m 121 | i18n (1.14.7) 122 | concurrent-ruby (~> 1.0) 123 | jazzy (0.15.3) 124 | cocoapods (~> 1.5) 125 | mustache (~> 1.1) 126 | open4 (~> 1.3) 127 | redcarpet (~> 3.4) 128 | rexml (>= 3.2.7, < 4.0) 129 | rouge (>= 2.0.6, < 5.0) 130 | sassc (~> 2.1) 131 | sqlite3 (~> 1.3) 132 | xcinvoke (~> 0.3.0) 133 | json (2.1.0) 134 | kramdown (2.5.1) 135 | rexml (>= 3.3.9) 136 | kramdown-parser-gfm (1.1.0) 137 | kramdown (~> 2.0) 138 | liferaft (0.0.6) 139 | logger (1.6.6) 140 | mini_portile2 (2.8.8) 141 | minitest (5.25.4) 142 | molinillo (0.8.0) 143 | mustache (1.1.1) 144 | mutex_m (0.3.0) 145 | nanaimo (0.4.0) 146 | nap (1.1.0) 147 | net-http (0.6.0) 148 | uri 149 | netrc (0.11.0) 150 | nkf (0.2.0) 151 | octokit (9.2.0) 152 | faraday (>= 1, < 3) 153 | sawyer (~> 0.9) 154 | open4 (1.3.4) 155 | pstore (0.1.4) 156 | public_suffix (4.0.7) 157 | rake (13.2.1) 158 | rchardet (1.9.0) 159 | redcarpet (3.6.1) 160 | rexml (3.4.1) 161 | rouge (4.5.1) 162 | ruby-macho (2.5.1) 163 | sassc (2.4.0) 164 | ffi (~> 1.9) 165 | sawyer (0.9.2) 166 | addressable (>= 2.3.5) 167 | faraday (>= 0.17.3, < 3) 168 | securerandom (0.4.1) 169 | sqlite3 (1.7.3) 170 | mini_portile2 (~> 2.8.0) 171 | sqlite3 (1.7.3-x86_64-linux) 172 | terminal-table (3.0.2) 173 | unicode-display_width (>= 1.1.1, < 3) 174 | thor (1.0.1) 175 | typhoeus (1.4.1) 176 | ethon (>= 0.9.0) 177 | tzinfo (2.0.6) 178 | concurrent-ruby (~> 1.0) 179 | unicode-display_width (2.6.0) 180 | uri (1.0.3) 181 | xcinvoke (0.3.0) 182 | liferaft (~> 0.0.6) 183 | xcodeproj (1.27.0) 184 | CFPropertyList (>= 2.3.3, < 4.0) 185 | atomos (~> 0.1.3) 186 | claide (>= 1.0.2, < 2.0) 187 | colored2 (~> 3.1) 188 | nanaimo (~> 0.4.0) 189 | rexml (>= 3.3.6, < 4.0) 190 | 191 | PLATFORMS 192 | ruby 193 | x86_64-linux 194 | 195 | DEPENDENCIES 196 | cocoapods (~> 1.16) 197 | danger (~> 9.5) 198 | danger-jazzy 199 | danger-swiftlint 200 | jazzy 201 | 202 | BUNDLED WITH 203 | 2.5.18 204 | -------------------------------------------------------------------------------- /Guides/Getting Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide provides a brief overview for how to get started using `JSQCoreDataKit`. 4 | 5 | ````swift 6 | import JSQCoreDataKit 7 | ```` 8 | 9 | ## Standing up the stack 10 | 11 | ````swift 12 | // Initialize the Core Data model, this class encapsulates the notion of a .xcdatamodeld file 13 | // The name passed here should be the name of an .xcdatamodeld file 14 | let bundle = Bundle(identifier: "com.MyApp.MyModelFramework")! 15 | let model = CoreDataModel(name: "MyModel", bundle: bundle) 16 | 17 | // Initialize a stack with a provider 18 | let provider = CoreDataStackProvider(model: model) 19 | 20 | let stack: CoreDataStack? 21 | provider.createStack { result in 22 | switch result { 23 | case .success(let s): 24 | stack = s 25 | 26 | case .failure(let e): 27 | print("Error: \(e)") 28 | } 29 | } 30 | ```` 31 | 32 | ## In-memory stacks for testing 33 | 34 | ````swift 35 | let inMemoryModel = CoreDataModel(name: myName, bundle: myBundle, storeType: .inMemory) 36 | let provider = CoreDataStackProvider(model: inMemoryModel) 37 | 38 | let stack: CoreDataStack? 39 | provider.createStack { result in 40 | switch result { 41 | case .success(let s): 42 | stack = s 43 | 44 | case .failure(let e): 45 | print("Error: \(e)") 46 | } 47 | } 48 | ```` 49 | 50 | ## Saving a managed object context 51 | 52 | ````swift 53 | stack.mainContext.saveSync { result in 54 | switch result { 55 | case .success: 56 | print("save succeeded") 57 | 58 | case .failure(let error): 59 | print("save failed: \(error)") 60 | } 61 | } 62 | ```` 63 | 64 | ## Deleting the store 65 | 66 | ````swift 67 | let bundle = Bundle(identifier: "com.MyApp.MyModelFramework")! 68 | let model = CoreDataModel(name: "MyModel", bundle: bundle) 69 | do { 70 | try model.removeExistingStore() 71 | } catch { 72 | print("Error: \(error)") 73 | } 74 | ```` 75 | 76 | ## Performing migrations 77 | 78 | ````swift 79 | let bundle = Bundle(identifier: "com.MyApp.MyModelFramework")! 80 | let model = CoreDataModel(name: "MyModel", bundle: bundle) 81 | if model.needsMigration { 82 | do { 83 | try model.migrate() 84 | } catch { 85 | print("Failed to migrate model: \(error)") 86 | } 87 | } 88 | ```` 89 | 90 | ## Using child contexts 91 | 92 | ````swift 93 | // Create a main queue child context from the main context 94 | let childContext = stack.childContext(concurrencyType: .mainQueueConcurrencyType) 95 | 96 | // Create a background queue child context from the background context 97 | let childContext = stack.childContext(concurrencyType: .privateQueueConcurrencyType) 98 | ```` 99 | 100 | ## Example app 101 | 102 | There's an example app in the `Example/` directory. Open the `ExampleApp.xcodeproj` to run it. The project exercises all basic functionality of the library. 103 | -------------------------------------------------------------------------------- /JSQCoreDataKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'JSQCoreDataKit' 3 | s.version = '9.0.3' 4 | s.license = 'MIT' 5 | 6 | s.summary = 'A swifter Core Data stack' 7 | s.homepage = 'https://github.com/jessesquires/JSQCoreDataKit' 8 | s.documentation_url = 'https://jessesquires.github.io/JSQCoreDataKit' 9 | s.social_media_url = 'https://www.jessesquires.com' 10 | s.author = 'Jesse Squires' 11 | 12 | s.source = { :git => 'https://github.com/jessesquires/JSQCoreDataKit.git', :tag => s.version } 13 | s.source_files = 'Sources/*.swift' 14 | 15 | s.swift_version = '5.5' 16 | 17 | s.ios.deployment_target = '14.0' 18 | s.tvos.deployment_target = '14.0' 19 | s.watchos.deployment_target = '6.0' 20 | s.osx.deployment_target = '10.14' 21 | 22 | s.frameworks = 'CoreData' 23 | s.requires_arc = true 24 | end 25 | -------------------------------------------------------------------------------- /JSQCoreDataKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JSQCoreDataKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JSQCoreDataKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JSQCoreDataKit.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Created by Jesse Squires 8 | // https://www.jessesquires.com 9 | // 10 | // 11 | // Documentation 12 | // https://jessesquires.github.io/JSQCoreDataKit 13 | // 14 | // 15 | // GitHub 16 | // https://github.com/jessesquires/JSQCoreDataKit 17 | // 18 | // 19 | // License 20 | // Copyright © 2015-present Jesse Squires 21 | // Released under an MIT license: https://opensource.org/licenses/MIT 22 | // 23 | 24 | 25 | -------------------------------------------------------------------------------- /JSQCoreDataKit.xcodeproj/xcshareddata/xcschemes/JSQCoreDataKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 46 | 47 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 65 | 68 | 74 | 75 | 76 | 77 | 78 | 88 | 89 | 95 | 96 | 97 | 98 | 104 | 105 | 111 | 112 | 113 | 114 | 116 | 117 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jesse Squires 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | // The swift-tools-version declares the minimum version 3 | // of Swift required to build this package. 4 | // ---------------------------------------------------- 5 | // 6 | // Created by Jesse Squires 7 | // https://www.jessesquires.com 8 | // 9 | // Documentation 10 | // https://jessesquires.github.io/JSQCoreDataKit 11 | // 12 | // GitHub 13 | // https://github.com/jessesquires/JSQCoreDataKit 14 | // 15 | // Copyright © 2020-present Jesse Squires 16 | // 17 | 18 | import PackageDescription 19 | 20 | let package = Package( 21 | name: "JSQCoreDataKit", 22 | platforms: [ 23 | .iOS(.v14), 24 | .tvOS(.v14), 25 | .watchOS(.v6), 26 | .macOS(.v10_14) 27 | ], 28 | products: [ 29 | .library(name: "JSQCoreDataKit", targets: ["JSQCoreDataKit"]) 30 | ], 31 | targets: [ 32 | .target(name: "JSQCoreDataKit", path: "Sources") 33 | 34 | // Unfortunately, we cannot include tests right now. 35 | // The test target depends on a fixture project, `ExampleModel`. 36 | // There is not a great way to model that in SwiftPM. 37 | // We do not want to include `ExampleModel` in our package. 38 | // However, this is not that bad. 39 | // We still run tests via the main Xcode project. 40 | 41 | // .testTarget(name: "JSQCoreDataKitTests", dependencies: ["JSQCoreDataKit"], path: "Tests") 42 | ], 43 | swiftLanguageVersions: [.v5] 44 | ) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSQCoreDataKit [![CI](https://github.com/jessesquires/JSQCoreDataKit/workflows/CI/badge.svg)](https://github.com/jessesquires/JSQCoreDataKit/actions) 2 | 3 | *A swifter Core Data stack* 4 | 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fjessesquires%2FJSQCoreDataKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/jessesquires/JSQCoreDataKit)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fjessesquires%2FJSQCoreDataKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/jessesquires/JSQCoreDataKit) 6 | 7 | ## About 8 | 9 | This library aims to do the following: 10 | 11 | * Encode Core Data best practices, so you don't have to think "is this correct?" or "is this the right way to do this?" 12 | * Provide better interoperability with Swift 13 | * Harness Swift features and enforce Swift paradigms 14 | * Bring functional paradigms to Core Data 15 | * Make Core Data more *Swifty* 16 | * Simplify the processes of standing up the Core Data stack 17 | * Aid in testing your Core Data models 18 | * Reduce the boilerplate involved with Core Data 19 | 20 | ## Requirements 21 | 22 | #### Tooling 23 | 24 | * Xcode 15.0+ 25 | * Swift 5.9+ 26 | * [SwiftLint](https://github.com/realm/SwiftLint) 27 | 28 | #### Platforms 29 | 30 | * iOS 11.0+ 31 | * macOS 10.12+ 32 | * tvOS 11.0+ 33 | * watchOS 4.0+ 34 | 35 | ## Installation 36 | 37 | ### [CocoaPods](http://cocoapods.org) 38 | 39 | ````ruby 40 | pod 'JSQCoreDataKit', '~> 9.0.0' 41 | ```` 42 | 43 | ### [Swift Package Manager](https://swift.org/package-manager/) 44 | 45 | Add `JSQCoreDataKit` to the `dependencies` value of your `Package.swift`. 46 | 47 | ```swift 48 | dependencies: [ 49 | .package(url: "https://github.com/jessesquires/JSQCoreDataKit.git", from: "9.0.0") 50 | ] 51 | ``` 52 | 53 | Alternatively, you can add the package [directly via Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app). 54 | 55 | ## Documentation 56 | 57 | You can read the [documentation here](https://jessesquires.github.io/JSQCoreDataKit). Generated with [jazzy](https://github.com/realm/jazzy). Hosted by [GitHub Pages](https://pages.github.com). 58 | 59 | ## Additional Resources 60 | 61 | * [Core Data Programming Guide](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/cdProgrammingGuide.html) 62 | * [Core Data Core Competencies Guide](https://developer.apple.com/library/ios/documentation/DataManagement/Devpedia-CoreData/coreDataStack.html#//apple_ref/doc/uid/TP40010398-CH25-SW1) 63 | * [objc.io issue #4 on Core Data](https://www.objc.io/issue-4/) 64 | * [Concurrent Core Data Stacks – Performance Shootout](http://floriankugler.com/2013/04/29/concurrent-core-data-stack-performance-shootout/) 65 | * [Backstage with Nested Managed Object Contexts](http://floriankugler.com/2013/05/13/backstage-with-nested-managed-object-contexts/) 66 | 67 | ## Contributing 68 | 69 | Interested in making contributions to this project? Please review the guides below. 70 | 71 | - [Contributing Guidelines](https://github.com/jessesquires/.github/blob/master/CONTRIBUTING.md) 72 | - [Code of Conduct](https://github.com/jessesquires/.github/blob/master/CODE_OF_CONDUCT.md) 73 | - [Support and Help](https://github.com/jessesquires/.github/blob/master/SUPPORT.md) 74 | - [Security Policy](https://github.com/jessesquires/.github/blob/master/SECURITY.md) 75 | 76 | Also, consider [sponsoring this project](https://www.jessesquires.com/sponsor/) or [buying my apps](https://www.hexedbits.com)! ✌️ 77 | 78 | ## Credits 79 | 80 | Created and maintained by [**@jesse_squires**](https://twitter.com/jesse_squires). 81 | 82 | ## License 83 | 84 | Released under the MIT License. See `LICENSE` for details. 85 | 86 | > **Copyright © 2015-present Jesse Squires.** 87 | -------------------------------------------------------------------------------- /Sources/CoreDataEntityProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import Foundation 21 | 22 | /// Describes an entity in Core Data. 23 | public protocol CoreDataEntityProtocol: AnyObject { 24 | 25 | /// The name of the entity. 26 | static var entityName: String { get } 27 | 28 | /// The default sort descriptors for a fetch request. 29 | static var defaultSortDescriptors: [NSSortDescriptor] { get } 30 | } 31 | 32 | extension CoreDataEntityProtocol where Self: NSManagedObject { 33 | 34 | /// Returns a default entity name for this managed object based on its class name. 35 | public static var entityName: String { 36 | "\(Self.self)" 37 | } 38 | 39 | /// Returns a new fetch request with `defaultSortDescriptors`. 40 | public static var fetchRequest: NSFetchRequest { 41 | let request = NSFetchRequest(entityName: entityName) 42 | request.sortDescriptors = defaultSortDescriptors 43 | return request as! NSFetchRequest 44 | } 45 | 46 | /// Returns the entity with the specified name from the managed object model associated 47 | /// with the specified managed object context’s persistent store coordinator. 48 | /// 49 | /// - parameter context: The managed object context to use. 50 | /// 51 | /// - returns: Returns the entity description for this managed object. 52 | public static func entity(context: NSManagedObjectContext) -> NSEntityDescription { 53 | NSEntityDescription.entity(forEntityName: entityName, in: context)! 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/CoreDataModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import Foundation 21 | 22 | /** 23 | An instance of `CoreDataModel` represents a Core Data model — a `.xcdatamodeld` file package. 24 | It provides the model and store URLs as well as methods for interacting with the store. 25 | */ 26 | public struct CoreDataModel: Equatable { 27 | 28 | // MARK: Properties 29 | 30 | /// The name of the Core Data model resource. 31 | public let name: String 32 | 33 | /// The bundle in which the model is located. 34 | /// The default is the main bundle. 35 | public let bundle: Bundle 36 | 37 | /// The type of the Core Data persistent store for the model. 38 | /// The default is `.sqlite` with the user's documents directory. 39 | public let storeType: StoreType 40 | 41 | /** 42 | The file URL specifying the full path to the store. 43 | 44 | - note: If the store is in-memory, then this value will be `nil`. 45 | */ 46 | public var storeURL: URL? { 47 | self.storeType.storeDirectory()?.appendingPathComponent(self.databaseFileName) 48 | } 49 | 50 | /// The file URL specifying the model file in the bundle specified by `bundle`. 51 | public var modelURL: URL { 52 | guard let url = self.bundle.url(forResource: self.name, withExtension: ModelFileExtension.bundle.rawValue) else { 53 | fatalError("*** Error loading model URL for model named \(self.name) in bundle: \(self.bundle)") 54 | } 55 | return url 56 | 57 | } 58 | 59 | /// The database file name for the store. 60 | public var databaseFileName: String { 61 | switch self.storeType { 62 | case .sqlite: return self.name + "." + ModelFileExtension.sqlite.rawValue 63 | default: return self.name 64 | } 65 | 66 | } 67 | 68 | /// The managed object model for the model specified by `name`. 69 | public var managedObjectModel: NSManagedObjectModel { 70 | guard let model = NSManagedObjectModel(contentsOf: self.modelURL) else { 71 | fatalError("*** Error loading managed object model at url: \(self.modelURL)") 72 | } 73 | return model 74 | 75 | } 76 | 77 | /** 78 | Queries the meta data for the persistent store specified by the receiver 79 | and returns whether or not a migration is needed. 80 | 81 | - returns: `true` if the store requires a migration, `false` otherwise. 82 | */ 83 | public var needsMigration: Bool { 84 | guard let storeURL = self.storeURL else { return false } 85 | do { 86 | let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: self.storeType.type, 87 | at: storeURL, 88 | options: nil) 89 | return !self.managedObjectModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) 90 | } catch { 91 | debugPrint("*** Error checking persistent store coordinator meta data: \(error)") 92 | return false 93 | } 94 | 95 | } 96 | 97 | // MARK: Initialization 98 | 99 | /** 100 | Constructs a new `CoreDataModel` instance with the specified name and bundle. 101 | 102 | - parameter name: The name of the Core Data model. 103 | - parameter bundle: The bundle in which the model is located. The default is the main bundle. 104 | - parameter storeType: The store type for the Core Data model. The default is `.sqlite` with the user's documents directory. 105 | 106 | - returns: A new `CoreDataModel` instance. 107 | */ 108 | public init(name: String, bundle: Bundle = .main, storeType: StoreType = .sqlite(defaultDirectoryURL())) { 109 | self.name = name 110 | self.bundle = bundle 111 | self.storeType = storeType 112 | } 113 | 114 | // MARK: Methods 115 | 116 | /// The default directory used to initialize a `CoreDataModel`. 117 | /// On tvOS, this is the caches directory. All other platforms use the document directory. 118 | public static func defaultDirectoryURL() -> URL { 119 | do { 120 | #if os(tvOS) 121 | let searchPathDirectory = FileManager.SearchPathDirectory.cachesDirectory 122 | #else 123 | let searchPathDirectory = FileManager.SearchPathDirectory.documentDirectory 124 | #endif 125 | 126 | return try FileManager.default.url(for: searchPathDirectory, 127 | in: .userDomainMask, 128 | appropriateFor: nil, 129 | create: true) 130 | } catch { 131 | fatalError("*** Error finding default directory: \(error)") 132 | } 133 | } 134 | 135 | /** 136 | Removes the existing model store specfied by the receiver. 137 | 138 | - throws: If removing the store fails or errors, then this function throws an `NSError`. 139 | */ 140 | public func removeExistingStore() throws { 141 | let fileManager = FileManager.default 142 | if let storePath = self.storeURL?.path, 143 | fileManager.fileExists(atPath: storePath) { 144 | try fileManager.removeItem(atPath: storePath) 145 | 146 | let writeAheadLog = storePath + "-wal" 147 | _ = try? fileManager.removeItem(atPath: writeAheadLog) 148 | 149 | let sharedMemoryfile = storePath + "-shm" 150 | _ = try? fileManager.removeItem(atPath: sharedMemoryfile) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/CoreDataStackProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | 21 | /** 22 | An instance of `CoreDataStackProvider` is responsible for creating instances of `CoreDataStack`. 23 | 24 | Because the adding of the persistent store to the persistent store coordinator during initialization 25 | of a `CoreDataStack` can take an unknown amount of time, you should not perform this operation on the main queue. 26 | 27 | See this [guide](https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Conceptual/CoreData/IntegratingCoreData.html#//apple_ref/doc/uid/TP40001075-CH9-SW1) for more details. 28 | 29 | - warning: You should not create instances of `CoreDataStack` directly. Use a `CoreDataStackProvider` instead. 30 | */ 31 | public struct CoreDataStackProvider { 32 | 33 | // MARK: Typealiases 34 | 35 | /// Describes the initialization options for a persistent store. 36 | public typealias PersistentStoreOptions = [AnyHashable: Any] 37 | 38 | // MARK: Properties 39 | 40 | /// Describes default persistent store options. 41 | public static let defaultStoreOptions: PersistentStoreOptions = [ 42 | NSMigratePersistentStoresAutomaticallyOption: true, 43 | NSInferMappingModelAutomaticallyOption: true 44 | ] 45 | 46 | /// The model for the stack that the factory produces. 47 | public let model: CoreDataModel 48 | 49 | /** 50 | A dictionary that specifies options for the store that the factory produces. 51 | The default value is `DefaultStoreOptions`. 52 | */ 53 | public let options: PersistentStoreOptions? 54 | 55 | // MARK: Initialization 56 | 57 | /** 58 | Constructs a new `CoreDataStackProvider` instance with the specified `model` and `options`. 59 | 60 | - parameter model: The model describing the stack. 61 | - parameter options: Options for the persistent store. 62 | 63 | - returns: A new `CoreDataStackProvider` instance. 64 | */ 65 | public init(model: CoreDataModel, options: PersistentStoreOptions? = defaultStoreOptions) { 66 | self.model = model 67 | self.options = options 68 | } 69 | 70 | // MARK: Creating a stack 71 | 72 | /** 73 | Initializes a new `CoreDataStack` instance using the factory's `model` and `options`. 74 | 75 | - warning: If a queue is provided, this operation is performed asynchronously on the specified queue, 76 | and the completion closure is executed asynchronously on the main queue. 77 | If `queue` is `nil`, then this method and the completion closure execute synchronously on the current queue. 78 | 79 | - parameter queue: The queue on which to initialize the stack. 80 | The default is a background queue with a "user initiated" quality of service class. 81 | If passing `nil`, this method is executed synchronously on the queue from which the method was called. 82 | 83 | - parameter completion: The closure to be called once initialization is complete. 84 | If a queue is provided, this is called asynchronously on the main queue. 85 | Otherwise, this is executed on the thread from which the method was originally called. 86 | */ 87 | public func createStack(onQueue queue: DispatchQueue? = .global(qos: .userInitiated), 88 | completion: @escaping (CoreDataStack.StackResult) -> Void) { 89 | let isAsync = (queue != nil) 90 | 91 | let creationClosure = { 92 | let storeCoordinator: NSPersistentStoreCoordinator 93 | do { 94 | storeCoordinator = try self._createStoreCoordinator() 95 | } catch { 96 | if isAsync { 97 | DispatchQueue.main.async { 98 | completion(.failure(error)) 99 | } 100 | } else { 101 | completion(.failure(error)) 102 | } 103 | return 104 | } 105 | 106 | let backgroundContext = self._createContext(.privateQueueConcurrencyType, name: "background") 107 | backgroundContext.persistentStoreCoordinator = storeCoordinator 108 | 109 | let mainContext = self._createContext(.mainQueueConcurrencyType, name: "main") 110 | mainContext.persistentStoreCoordinator = storeCoordinator 111 | 112 | let stack = CoreDataStack(model: self.model, 113 | mainContext: mainContext, 114 | backgroundContext: backgroundContext, 115 | storeCoordinator: storeCoordinator) 116 | 117 | if isAsync { 118 | DispatchQueue.main.async { 119 | completion(.success(stack)) 120 | } 121 | } else { 122 | completion(.success(stack)) 123 | } 124 | } 125 | 126 | if let queue { 127 | queue.async(execute: creationClosure) 128 | } else { 129 | creationClosure() 130 | } 131 | } 132 | 133 | // MARK: Private 134 | 135 | private func _createStoreCoordinator() throws -> NSPersistentStoreCoordinator { 136 | let storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.model.managedObjectModel) 137 | try storeCoordinator.addPersistentStore(ofType: self.model.storeType.type, 138 | configurationName: nil, 139 | at: self.model.storeURL, 140 | options: self.options) 141 | return storeCoordinator 142 | } 143 | 144 | private func _createContext(_ concurrencyType: NSManagedObjectContextConcurrencyType, 145 | name: String) -> NSManagedObjectContext { 146 | let context = NSManagedObjectContext(concurrencyType: concurrencyType) 147 | context.mergePolicy = NSMergePolicy(merge: .mergeByPropertyStoreTrumpMergePolicyType) 148 | 149 | let contextName = "JSQCoreDataKit.CoreDataStack.context." 150 | context.name = contextName + name 151 | 152 | return context 153 | } 154 | } 155 | extension CoreDataStackProvider: Equatable { 156 | /// :nodoc: 157 | public static func == (lhs: CoreDataStackProvider, rhs: CoreDataStackProvider) -> Bool { 158 | lhs.model == rhs.model 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/FetchedResultsCellConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | #if os(iOS) || os(tvOS) 20 | 21 | import CoreData 22 | import UIKit 23 | 24 | public protocol FetchedResultsCellConfiguration { 25 | 26 | associatedtype CellType: UICollectionViewCell 27 | 28 | associatedtype Object: NSManagedObject 29 | 30 | typealias Registration = UICollectionView.CellRegistration 31 | 32 | func configure(cell: CellType, with object: Object) 33 | } 34 | 35 | extension FetchedResultsCellConfiguration { 36 | 37 | public var registration: Registration { 38 | Registration { cell, _, object in 39 | self.configure(cell: cell, with: object) 40 | } 41 | } 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/FetchedResultsController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import Foundation 21 | 22 | #if os(macOS) 23 | import AppKit 24 | #endif 25 | 26 | #if os(iOS) || os(tvOS) || os(watchOS) 27 | import UIKit 28 | #endif 29 | 30 | /// A generic `NSFetchedResultsController`. 31 | public final class FetchedResultsController: NSFetchedResultsController { 32 | 33 | // MARK: Init 34 | 35 | /// Returns a fetch request controller initialized using the given arguments. 36 | /// 37 | /// - Parameters: 38 | /// - fetchRequest: The fetch request used to get the objects. 39 | /// - context: The managed object against which `fetchRequest` is executed. 40 | /// - sectionNameKeyPath: A key path on result objects that returns the section name. 41 | /// - cacheName: The name of the cache file the receiver should use. 42 | /// 43 | /// - Returns: An initialized fetch request controller. 44 | public init(fetchRequest: NSFetchRequest, 45 | context: NSManagedObjectContext, 46 | sectionNameKeyPath: String?, 47 | cacheName: String?) { 48 | super.init( 49 | fetchRequest: fetchRequest as! NSFetchRequest, 50 | managedObjectContext: context, 51 | sectionNameKeyPath: sectionNameKeyPath, 52 | cacheName: cacheName 53 | ) 54 | } 55 | 56 | // MARK: Subscripts 57 | 58 | /// - Parameter indexPath: An index path of an object. 59 | /// - Returns: The object at `indexPath`. 60 | public subscript (indexPath: IndexPath) -> ObjectType { 61 | self.object(at: indexPath) 62 | } 63 | 64 | public subscript (section index: Int) -> NSFetchedResultsSectionInfo { 65 | self.section(at: index) 66 | } 67 | 68 | // MARK: Methods 69 | 70 | public func deleteCache() { 71 | Self.deleteCache(withName: self.cacheName) 72 | } 73 | 74 | public func numberOfSections() -> Int { 75 | self.sections().count 76 | } 77 | 78 | public func numberOfItems(in section: Int) -> Int { 79 | self.sections?[section].numberOfObjects ?? 0 80 | } 81 | 82 | public func sections() -> [NSFetchedResultsSectionInfo] { 83 | self.sections ?? [] 84 | } 85 | 86 | public func section(at index: Int) -> NSFetchedResultsSectionInfo { 87 | self.sections()[index] 88 | } 89 | 90 | public func fetchedObjects() -> [ObjectType] { 91 | (self.fetchedObjects ?? []) as! [ObjectType] 92 | } 93 | 94 | public func hasObject(at indexPath: IndexPath) -> Bool { 95 | if self.fetchedObjects().isEmpty { 96 | return false 97 | } 98 | let numberOfItems = self.numberOfItems(in: indexPath.section) 99 | return numberOfItems > 0 && indexPath.item < numberOfItems 100 | } 101 | 102 | public func object(at indexPath: IndexPath) -> ObjectType { 103 | super.object(at: indexPath) as! ObjectType 104 | } 105 | 106 | public func indexPath(for object: ObjectType) -> IndexPath? { 107 | self.indexPath(forObject: object) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/FetchedResultsCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | #if os(iOS) || os(tvOS) 20 | 21 | import CoreData 22 | import Foundation 23 | import UIKit 24 | 25 | public final class FetchedResultsCoordinator< 26 | Object, 27 | CellConfig: FetchedResultsCellConfiguration 28 | >: NSObject, NSFetchedResultsControllerDelegate where CellConfig.Object == Object { 29 | 30 | public let controller: FetchedResultsController 31 | 32 | public let cellConfiguration: CellConfig 33 | 34 | public let supplementaryConfigurations: [AnyFetchedResultsSupplementaryConfiguration] 35 | 36 | public var animateUpdates = true 37 | 38 | private let _dataSource: _FetchedResultsDiffableDataSource 39 | 40 | private unowned var _collectionView: UICollectionView 41 | 42 | public init(fetchRequest: NSFetchRequest, 43 | context: NSManagedObjectContext, 44 | sectionNameKeyPath: String?, 45 | cacheName: String?, 46 | collectionView: UICollectionView, 47 | cellConfiguration: CellConfig, 48 | supplementaryConfigurations: [AnyFetchedResultsSupplementaryConfiguration] = []) { 49 | let controller = FetchedResultsController( 50 | fetchRequest: fetchRequest, 51 | context: context, 52 | sectionNameKeyPath: sectionNameKeyPath, 53 | cacheName: cacheName 54 | ) 55 | 56 | self.controller = controller 57 | self.cellConfiguration = cellConfiguration 58 | self.supplementaryConfigurations = supplementaryConfigurations 59 | self._dataSource = _FetchedResultsDiffableDataSource( 60 | collectionView: collectionView, 61 | controller: controller, 62 | cellConfig: cellConfiguration, 63 | supplementaryConfigs: supplementaryConfigurations 64 | ) 65 | self._collectionView = collectionView 66 | super.init() 67 | controller.delegate = self 68 | collectionView.dataSource = self._dataSource 69 | } 70 | 71 | // MARK: Subscripts 72 | 73 | public subscript (indexPath: IndexPath) -> Object { 74 | self.controller[indexPath] 75 | } 76 | 77 | // MARK: Methods 78 | 79 | public func performFetch() { 80 | do { 81 | try self.controller.performFetch() 82 | } catch { 83 | assertionFailure("FetchedResultsController failed to perform fetch: \(error)") 84 | } 85 | } 86 | 87 | public func object(at indexPath: IndexPath) -> Object { 88 | self.controller.object(at: indexPath) 89 | } 90 | 91 | // MARK: NSFetchedResultsControllerDelegate 92 | 93 | /// :nodoc: 94 | public func controller(_ controller: NSFetchedResultsController, 95 | didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { 96 | var fetchedSnapshot = snapshot as _FetchedResultsDiffableSnapshot 97 | let currentSnapshot = self._dataSource.snapshot() 98 | 99 | // Ensure updated managed objects get reloaded 100 | // Taken from: https://www.avanderlee.com/swift/diffable-data-sources-core-data/ 101 | let itemsToReload: [NSManagedObjectID] = fetchedSnapshot.itemIdentifiers.compactMap { itemId in 102 | // the item is at the same index 103 | guard let currentIndex = currentSnapshot.indexOfItem(itemId), 104 | let fetchedIndex = fetchedSnapshot.indexOfItem(itemId), 105 | fetchedIndex == currentIndex else { 106 | return nil 107 | } 108 | // the item has been updated 109 | guard let existingObject = try? controller.managedObjectContext.existingObject(with: itemId), 110 | existingObject.isUpdated else { 111 | return nil 112 | } 113 | return itemId 114 | } 115 | 116 | if #available(iOS 15.0, tvOS 15.0, *) { 117 | fetchedSnapshot.reconfigureItems(itemsToReload) 118 | } else { 119 | fetchedSnapshot.reloadItems(itemsToReload) 120 | } 121 | 122 | // Fix issue with "empty sections" appearing. 123 | // If a section has no items, delete it. 124 | let sections = fetchedSnapshot.sectionIdentifiers.filter { 125 | fetchedSnapshot.numberOfItems(inSection: $0) == 0 126 | } 127 | fetchedSnapshot.deleteSections(sections) 128 | 129 | // If current snapshot is empty, this is our first load. 130 | // Don't animate a diff, just reload. 131 | if currentSnapshot.isEmpty { 132 | if #available(iOS 15.0, tvOS 15.0, *) { 133 | self._dataSource.applySnapshotUsingReloadData(fetchedSnapshot, completion: nil) 134 | } else { 135 | // prior to iOS 15, passing false means reload data 136 | // https://jessesquires.github.io/wwdc-notes/2021/10252_blazing_fast_collection_views.html 137 | self._dataSource.apply(fetchedSnapshot, animatingDifferences: false) 138 | } 139 | } else { 140 | self._dataSource.apply(fetchedSnapshot, animatingDifferences: self.animateUpdates) 141 | } 142 | 143 | self.supplementaryConfigurations.forEach { config in 144 | let kind = config.kind 145 | let visibleIndexPaths = self._collectionView.indexPathsForVisibleSupplementaryElements(ofKind: kind) 146 | 147 | visibleIndexPaths.forEach { indexPath in 148 | guard let view = self._collectionView.supplementaryView(forElementKind: kind, at: indexPath) else { 149 | return 150 | } 151 | 152 | var object: Object? 153 | if self.controller.hasObject(at: indexPath) { 154 | object = self.controller.object(at: indexPath) 155 | } 156 | let sectionInfo = self.controller.section(at: indexPath.section) 157 | config.configure(view: view, with: object, in: sectionInfo) 158 | } 159 | } 160 | } 161 | } 162 | 163 | #endif 164 | -------------------------------------------------------------------------------- /Sources/FetchedResultsDiffableDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | #if os(iOS) || os(tvOS) 20 | 21 | import CoreData 22 | import UIKit 23 | 24 | typealias _FetchedResultsDiffableDataSource = UICollectionViewDiffableDataSource 25 | 26 | typealias _FetchedResultsDiffableSnapshot = NSDiffableDataSourceSnapshot 27 | 28 | extension _FetchedResultsDiffableDataSource { 29 | 30 | convenience init( 31 | collectionView: UICollectionView, 32 | controller: FetchedResultsController, 33 | cellConfig: CellConfig, 34 | supplementaryConfigs: [AnyFetchedResultsSupplementaryConfiguration] 35 | ) where CellConfig.Object == Object { 36 | let cellRegistration = cellConfig.registration 37 | 38 | self.init(collectionView: collectionView) { collectionView, indexPath, objectID in 39 | let object = controller.object(at: indexPath) 40 | precondition(object.objectID == objectID) 41 | return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: object) 42 | } 43 | 44 | if supplementaryConfigs.isEmpty { 45 | return 46 | } 47 | 48 | supplementaryConfigs.forEach { 49 | $0.registerWith(collectionView: collectionView) 50 | } 51 | 52 | let allKinds = supplementaryConfigs.map { $0.kind } 53 | let configMap = Dictionary(uniqueKeysWithValues: zip(allKinds, supplementaryConfigs)) 54 | 55 | self.supplementaryViewProvider = { collectionView, kind, indexPath in 56 | precondition(configMap[kind] != nil, "A SupplementaryConfiguration should exist for kind: \(kind)") 57 | 58 | var object: Object? 59 | if controller.hasObject(at: indexPath) { 60 | object = controller.object(at: indexPath) 61 | } 62 | let sectionInfo = controller.section(at: indexPath.section) 63 | let config = configMap[kind]! 64 | return config._dequeueAndConfigureViewFor( 65 | collectionView: collectionView, 66 | at: indexPath, 67 | with: object, 68 | in: sectionInfo 69 | ) 70 | } 71 | } 72 | } 73 | 74 | extension NSDiffableDataSourceSnapshot { 75 | var isEmpty: Bool { 76 | self.numberOfItems == 0 77 | } 78 | } 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /Sources/FetchedResultsSupplementaryConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | #if os(iOS) || os(tvOS) 20 | 21 | import CoreData 22 | import UIKit 23 | 24 | public protocol FetchedResultsSupplementaryConfiguration { 25 | 26 | associatedtype ViewType: UICollectionReusableView 27 | 28 | associatedtype Object: NSManagedObject 29 | 30 | var reuseIdentifier: String { get } 31 | 32 | var kind: String { get } 33 | 34 | func registerWith(collectionView: UICollectionView) 35 | 36 | func configure(view: ViewType, with object: Object?, in section: NSFetchedResultsSectionInfo) 37 | } 38 | 39 | extension FetchedResultsSupplementaryConfiguration { 40 | public var viewClass: AnyClass { ViewType.self } 41 | 42 | public var reuseIdentifier: String { "\(Self.self)" } 43 | 44 | public func registerWith(collectionView: UICollectionView) { 45 | collectionView.register( 46 | self.viewClass, 47 | forSupplementaryViewOfKind: self.kind, 48 | withReuseIdentifier: self.reuseIdentifier 49 | ) 50 | } 51 | 52 | func _dequeueAndConfigureViewFor( 53 | collectionView: UICollectionView, 54 | at indexPath: IndexPath, 55 | with object: Object?, 56 | in section: NSFetchedResultsSectionInfo) -> ViewType { 57 | let view = collectionView.dequeueReusableSupplementaryView( 58 | ofKind: self.kind, 59 | withReuseIdentifier: self.reuseIdentifier, 60 | for: indexPath) as! ViewType 61 | self.configure(view: view, with: object, in: section) 62 | return view 63 | } 64 | } 65 | 66 | public struct AnyFetchedResultsSupplementaryConfiguration: FetchedResultsSupplementaryConfiguration { 67 | 68 | // MARK: FetchedResultsSupplementaryConfiguration 69 | 70 | public typealias ViewType = UICollectionReusableView 71 | 72 | public typealias Object = T 73 | 74 | public var reuseIdentifier: String { self._reuseIdentifier } 75 | 76 | public var kind: String { self._kind } 77 | 78 | public func registerWith(collectionView: UICollectionView) { 79 | self._register(collectionView) 80 | } 81 | 82 | public func configure(view: ViewType, with object: Object?, in section: NSFetchedResultsSectionInfo) { 83 | self._configure(view, object, section) 84 | } 85 | 86 | // MARK: Private 87 | 88 | private let _reuseIdentifier: String 89 | private let _kind: String 90 | private let _register: (UICollectionView) -> Void 91 | private let _configure: (ViewType, Object?, NSFetchedResultsSectionInfo) -> Void 92 | 93 | // MARK: Init 94 | 95 | public init(_ config: Config) where Config.Object == T { 96 | self._reuseIdentifier = config.reuseIdentifier 97 | self._kind = config.kind 98 | self._register = { collectionView in 99 | config.registerWith(collectionView: collectionView) 100 | } 101 | self._configure = { view, object, section in 102 | precondition( 103 | view is Config.ViewType, 104 | "View must be of type \(Config.ViewType.self). Found \(view.self)" 105 | ) 106 | config.configure(view: view as! Config.ViewType, with: object, in: section) 107 | } 108 | } 109 | } 110 | 111 | #endif 112 | -------------------------------------------------------------------------------- /Sources/Migrate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import Foundation 21 | 22 | /** 23 | An error type that specifies possible errors that are thrown by calling `CoreDataModel.migrate() throws`. 24 | */ 25 | public enum MigrationError: Error { 26 | 27 | /** 28 | Specifies that the `NSManagedObjectModel` corresponding to the existing persistent store was not found in the model's bundle. 29 | 30 | - parameter model: The model that failed to be migrated. 31 | */ 32 | case sourceModelNotFound(model: CoreDataModel) 33 | 34 | /** 35 | Specifies that an `NSMappingModel` was not found in the model's bundle in the progressive migration 'path'. 36 | 37 | - parameter sourceModel: The destination managed object model for which a mapping model was not found. 38 | */ 39 | case mappingModelNotFound(destinationModel: NSManagedObjectModel) 40 | } 41 | 42 | extension CoreDataModel { 43 | 44 | /** 45 | Progressively migrates the persistent store of the `CoreDataModel` based on mapping models found in the model's bundle. 46 | If the model returns false from `needsMigration`, then this function does nothing. 47 | 48 | - throws: If an error occurs, either an `NSError` or a `MigrationError` is thrown. If an `NSError` is thrown, it could 49 | specify any of the following: an error checking persistent store metadata, an error from `NSMigrationManager`, or 50 | an error from `NSFileManager`. 51 | 52 | - warning: Migration is only supported for on-disk persistent stores. 53 | A complete 'path' of mapping models must exist between the peristent store's version and the model's version. 54 | */ 55 | public func migrate() throws { 56 | guard self.needsMigration else { return } 57 | 58 | guard let storeURL = self.storeURL, let storeDirectory = self.storeType.storeDirectory() else { 59 | preconditionFailure("*** Error: migration is only available for on-disk persistent stores. Invalid model: \(self)") 60 | } 61 | 62 | // could also throw NSError from NSPersistentStoreCoordinator 63 | guard let sourceModel = try findCompatibleModel(withBundle: self.bundle, storeType: self.storeType.type, storeURL: storeURL) else { 64 | throw MigrationError.sourceModelNotFound(model: self) 65 | } 66 | 67 | let migrationSteps = try buildMigrationMappingSteps(bundle: self.bundle, 68 | sourceModel: sourceModel, 69 | destinationModel: self.managedObjectModel) 70 | 71 | for step in migrationSteps { 72 | let tempURL = storeDirectory.appendingPathComponent("migration." + ModelFileExtension.sqlite.rawValue) 73 | 74 | // could throw error from `migrateStoreFromURL` 75 | let manager = NSMigrationManager(sourceModel: step.source, destinationModel: step.destination) 76 | try manager.migrateStore(from: storeURL, 77 | sourceType: self.storeType.type, 78 | options: nil, 79 | with: step.mapping, 80 | toDestinationURL: tempURL, 81 | destinationType: self.storeType.type, 82 | destinationOptions: nil) 83 | 84 | // could throw file system errors 85 | try self.removeExistingStore() 86 | try FileManager.default.moveItem(at: tempURL, to: storeURL) 87 | } 88 | } 89 | } 90 | 91 | // MARK: Internal 92 | 93 | struct MigrationMappingStep { 94 | let source: NSManagedObjectModel 95 | let mapping: NSMappingModel 96 | let destination: NSManagedObjectModel 97 | } 98 | 99 | func findCompatibleModel(withBundle bundle: Bundle, 100 | storeType: String, 101 | storeURL: URL) throws -> NSManagedObjectModel? { 102 | let storeMetadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: storeType, at: storeURL, options: nil) 103 | let modelsInBundle = findModelsInBundle(bundle) 104 | for model in modelsInBundle where model.isConfiguration(withName: nil, compatibleWithStoreMetadata: storeMetadata) { 105 | return model 106 | } 107 | return nil 108 | } 109 | 110 | func findModelsInBundle(_ bundle: Bundle) -> [NSManagedObjectModel] { 111 | guard let modelBundleDirectoryURLs = bundle.urls(forResourcesWithExtension: ModelFileExtension.bundle.rawValue, subdirectory: nil) else { 112 | return [] 113 | } 114 | 115 | let modelBundleDirectoryNames = modelBundleDirectoryURLs.compactMap { url -> String? in 116 | url.lastPathComponent 117 | } 118 | 119 | let modelVersionFileURLs = modelBundleDirectoryNames.compactMap { name -> [URL]? in 120 | bundle.urls(forResourcesWithExtension: ModelFileExtension.versionedFile.rawValue, subdirectory: name) 121 | } 122 | .joined() 123 | .sorted { $0.absoluteString < $1.absoluteString } 124 | 125 | return modelVersionFileURLs.compactMap { url -> NSManagedObjectModel? in 126 | NSManagedObjectModel(contentsOf: url) 127 | } 128 | } 129 | 130 | func buildMigrationMappingSteps(bundle: Bundle, 131 | sourceModel: NSManagedObjectModel, 132 | destinationModel: NSManagedObjectModel) throws -> [MigrationMappingStep] { 133 | var migrationSteps = [MigrationMappingStep]() 134 | var nextModel = sourceModel 135 | repeat { 136 | guard let nextStep = nextMigrationMappingStep(fromSourceModel: nextModel, bundle: bundle) else { 137 | throw MigrationError.mappingModelNotFound(destinationModel: nextModel) 138 | } 139 | migrationSteps.append(nextStep) 140 | nextModel = nextStep.destination 141 | 142 | } while nextModel.entityVersionHashesByName != destinationModel.entityVersionHashesByName 143 | 144 | return migrationSteps 145 | } 146 | 147 | func nextMigrationMappingStep(fromSourceModel sourceModel: NSManagedObjectModel, 148 | bundle: Bundle) -> MigrationMappingStep? { 149 | let modelsInBundle = findModelsInBundle(bundle) 150 | 151 | for nextDestinationModel in modelsInBundle where nextDestinationModel.entityVersionHashesByName != sourceModel.entityVersionHashesByName { 152 | if let mappingModel = NSMappingModel(from: [bundle], 153 | forSourceModel: sourceModel, 154 | destinationModel: nextDestinationModel) { 155 | return MigrationMappingStep(source: sourceModel, mapping: mappingModel, destination: nextDestinationModel) 156 | } 157 | } 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /Sources/ModelFileExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | /** 20 | Describes a Core Data model file exention type based on the 21 | [Model File Format and Versions](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmModelFormat.html) 22 | documentation. 23 | */ 24 | public enum ModelFileExtension: String { 25 | /// The extension for a model bundle, or a `.xcdatamodeld` file package. 26 | case bundle = "momd" 27 | 28 | /// The extension for a versioned model file, or a `.xcdatamodel` file. 29 | case versionedFile = "mom" 30 | 31 | /// The extension for a mapping model file, or a `.xcmappingmodel` file. 32 | case mapping = "cdm" 33 | 34 | /// The extension for a sqlite store. 35 | case sqlite = "sqlite" 36 | } 37 | -------------------------------------------------------------------------------- /Sources/NSManagedObjectContext+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | 21 | extension NSManagedObjectContext { 22 | 23 | /// Describes a child managed object context. 24 | public typealias ChildContext = NSManagedObjectContext 25 | 26 | /// Describes the result type for saving a managed object context. 27 | public typealias SaveResult = Result 28 | 29 | /// Attempts to **asynchronously** commit unsaved changes to registered objects in the context. 30 | /// This function is performed in a block on the context's queue. If the context has no changes, 31 | /// then this function returns immediately and the completion block is not called. 32 | /// 33 | /// - Parameter completion: The closure to be executed when the save operation completes. 34 | public func saveAsync(completion: ((SaveResult) -> Void)? = nil) { 35 | self._save(wait: false, completion: completion) 36 | } 37 | 38 | /// Attempts to **synchronously** commit unsaved changes to registered objects in the context. 39 | /// This function is performed in a block on the context's queue. If the context has no changes, 40 | /// then this function returns immediately and the completion block is not called. 41 | /// 42 | /// - Parameter completion: The closure to be executed when the save operation completes. 43 | public func saveSync(completion: ((SaveResult) -> Void)? = nil) { 44 | self._save(wait: true, completion: completion) 45 | } 46 | 47 | /// Attempts to commit unsaved changes to registered objects in the context. 48 | /// 49 | /// - Parameter wait: If `true`, saves synchronously. If `false`, saves asynchronously. 50 | /// - Parameter completion: The closure to be executed when the save operation completes. 51 | private func _save(wait: Bool, completion: ((SaveResult) -> Void)? = nil) { 52 | 53 | let block = { 54 | guard self.hasChanges else { return } 55 | do { 56 | try self.save() 57 | completion?(.success(self)) 58 | } catch { 59 | completion?(.failure(error)) 60 | } 61 | } 62 | 63 | // swiftlint:disable:next void_function_in_ternary 64 | wait ? self.performAndWait(block) : self.perform(block) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | 12 | NSPrivacyCollectedDataType 13 | 14 | NSPrivacyCollectedDataTypeLinked 15 | 16 | NSPrivacyCollectedDataTypeTracking 17 | 18 | NSPrivacyCollectedDataTypePurposes 19 | 20 | 21 | 22 | 23 | 24 | NSPrivacyAccessedAPITypes 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Sources/StoreType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import Foundation 21 | 22 | /// Describes a Core Data persistent store type. 23 | public enum StoreType: Equatable { 24 | 25 | /// The SQLite database store type. The associated file URL specifies the directory for the store. 26 | case sqlite(URL) 27 | 28 | /// The binary store type. The associated file URL specifies the directory for the store. 29 | case binary(URL) 30 | 31 | /// The in-memory store type. 32 | case inMemory 33 | 34 | // MARK: Properties 35 | 36 | /// Returns the type string description for the store type. 37 | public var type: String { 38 | switch self { 39 | case .sqlite: return NSSQLiteStoreType 40 | case .binary: return NSBinaryStoreType 41 | case .inMemory: return NSInMemoryStoreType 42 | } 43 | } 44 | 45 | // MARK: Methods 46 | 47 | /** 48 | - note: If the store is in-memory, then this value will be `nil`. 49 | - returns: The file URL specifying the directory in which the store is located. 50 | */ 51 | public func storeDirectory() -> URL? { 52 | switch self { 53 | case let .sqlite(url): return url 54 | case let .binary(url): return url 55 | case .inMemory: return nil 56 | } 57 | } 58 | } 59 | 60 | extension StoreType: CustomStringConvertible { 61 | /// :nodoc: 62 | public var description: String { 63 | self.type 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/ContextSyncTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | @testable import JSQCoreDataKit 22 | import XCTest 23 | 24 | // swiftlint:disable force_try 25 | 26 | final class ContextSyncTests: TestCase { 27 | 28 | func test_ThatUnsavedChangesFromChildContext_DoNotPropagate() { 29 | // GIVEN: objects in a child context 30 | let childContext = self.inMemoryStack.childContext() 31 | childContext.performAndWait { 32 | self.generateDataInContext(childContext, companiesCount: 3, employeesCount: 3) 33 | } 34 | 35 | // WHEN: we do not save the child context 36 | 37 | // WHEN: we fetch the objects from the main context 38 | let request = Company.fetchRequest 39 | let results = try? self.inMemoryStack.mainContext.fetch(request) 40 | 41 | // THEN: the main context does not return any objects 42 | XCTAssertEqual(results?.count, 0, "Main context should return nothing") 43 | } 44 | 45 | func test_ThatChangesPropagate_FromMainContext_ToBackgroundContext() { 46 | // GIVEN: objects in the main context 47 | let context = self.inMemoryStack.mainContext 48 | var companies = [Company]() 49 | context.performAndWait { 50 | companies = self.generateDataInContext(context, companiesCount: 3, employeesCount: 3) 51 | } 52 | let companyNames = companies.map { $0.name } 53 | 54 | // WHEN: we save the main context 55 | self.expectation(forNotification: .NSManagedObjectContextDidSave, object: self.inMemoryStack.mainContext, handler: nil) 56 | 57 | self.inMemoryStack.mainContext.saveAsync { result in 58 | XCTAssertNotNil(try? result.get(), "Save should not error") 59 | } 60 | 61 | self.waitForExpectations(timeout: defaultTimeout) { error in 62 | XCTAssertNil(error, "Expectation should not error") 63 | } 64 | 65 | // WHEN: we fetch the objects in the background context 66 | let request = Company.fetchRequest 67 | let bgContext = self.inMemoryStack.backgroundContext 68 | var results = [Company]() 69 | bgContext.performAndWait { 70 | results = try! bgContext.fetch(request) 71 | } 72 | 73 | // THEN: the background context returns the objects 74 | XCTAssertEqual(results.count, companies.count, "Background context should return same objects") 75 | results.forEach { (company: Company) in 76 | XCTAssertTrue(companyNames.contains(company.name), "Background context should return same objects") 77 | } 78 | } 79 | 80 | func test_ThatChangesPropagate_FromBackgroundContext_ToMainContext() { 81 | // GIVEN: objects in the background context 82 | let context = self.inMemoryStack.backgroundContext 83 | var companies = [Company]() 84 | context.performAndWait { 85 | companies = self.generateDataInContext(context, companiesCount: 3, employeesCount: 3) 86 | } 87 | let companyNames = companies.map { $0.name } 88 | 89 | // WHEN: we save the background context 90 | self.expectation(forNotification: .NSManagedObjectContextDidSave, object: self.inMemoryStack.backgroundContext, handler: nil) 91 | 92 | self.inMemoryStack.backgroundContext.saveSync { result in 93 | XCTAssertNotNil(try? result.get(), "Save should not error") 94 | } 95 | 96 | self.waitForExpectations(timeout: defaultTimeout) { error in 97 | XCTAssertNil(error, "Expectation should not error") 98 | } 99 | 100 | // WHEN: we fetch the objects from the main context 101 | let request = NSFetchRequest(entityName: Company.entityName) 102 | let results = try! self.inMemoryStack.mainContext.fetch(request) 103 | 104 | // THEN: the main context returns the objects 105 | XCTAssertEqual(results.count, companies.count, "Main context should return the same objects") 106 | results.forEach { (company: Company) in 107 | XCTAssertTrue(companyNames.contains(company.name), "Main context should return same objects") 108 | } 109 | } 110 | 111 | func test_ThatChangesPropagate_FromChildContext_ToMainContext() { 112 | // GIVEN: objects in a child context 113 | let childContext = self.inMemoryStack.childContext(concurrencyType: .mainQueueConcurrencyType) 114 | var companies = [Company]() 115 | childContext.performAndWait { 116 | companies = self.generateDataInContext(childContext, companiesCount: 3, employeesCount: 3) 117 | } 118 | let companyNames = companies.map { $0.name } 119 | 120 | // WHEN: we save the child context 121 | self.expectation(forNotification: .NSManagedObjectContextDidSave, object: childContext, handler: nil) 122 | childContext.saveAsync { result in 123 | XCTAssertNotNil(try? result.get(), "Save should not error") 124 | } 125 | 126 | self.waitForExpectations(timeout: defaultTimeout) { error in 127 | XCTAssertNil(error, "Expectation should not error") 128 | } 129 | 130 | // WHEN: we fetch the objects from the main context 131 | let request = Company.fetchRequest 132 | let context = self.inMemoryStack.mainContext 133 | var results = [Company]() 134 | context.performAndWait { 135 | results = try! context.fetch(request) 136 | } 137 | 138 | // THEN: the main context returns the objects 139 | XCTAssertEqual(results.count, companies.count, "Main context should return the same objects") 140 | results.forEach { (company: Company) in 141 | XCTAssertTrue(companyNames.contains(company.name), "Main context should return same objects") 142 | } 143 | } 144 | 145 | func test_ThatChangesPropagate_FromChildContext_ToBackgroundContext() { 146 | // GIVEN: objects in a child context 147 | let childContext = self.inMemoryStack.childContext(concurrencyType: .privateQueueConcurrencyType) 148 | var companies = [Company]() 149 | childContext.performAndWait { 150 | companies = self.generateDataInContext(childContext, companiesCount: 3, employeesCount: 3) 151 | } 152 | let companyNames = companies.map { $0.name } 153 | 154 | // WHEN: we save the child context 155 | self.expectation(forNotification: .NSManagedObjectContextDidSave, object: childContext, handler: nil) 156 | childContext.saveSync { result in 157 | XCTAssertNotNil(try? result.get(), "Save should not error") 158 | } 159 | 160 | self.waitForExpectations(timeout: defaultTimeout) { error in 161 | XCTAssertNil(error, "Expectation should not error") 162 | } 163 | 164 | // WHEN: we fetch the objects from the background context 165 | let request = Company.fetchRequest 166 | let context = self.inMemoryStack.backgroundContext 167 | var results = [Company]() 168 | context.performAndWait { 169 | results = try! context.fetch(request) 170 | } 171 | 172 | // THEN: the background context returns the objects 173 | XCTAssertEqual(results.count, companies.count, "Background context should return the same objects") 174 | results.forEach { (company: Company) in 175 | XCTAssertTrue(companyNames.contains(company.name), "Background context should return same objects") 176 | } 177 | } 178 | } 179 | 180 | // swiftlint:enable force_try 181 | -------------------------------------------------------------------------------- /Tests/CoreDataEntityProtocolTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | @testable import JSQCoreDataKit 22 | import XCTest 23 | 24 | final class CoreDataEntityProtocolTests: TestCase { 25 | 26 | func test_thatManagedObjects_returnCorrect_entityName() { 27 | // GIVEN: managed objects 28 | // WHEN: we ask for entity name 29 | // THEN: we receive expected values 30 | 31 | XCTAssertEqual(Company.entityName, "Company") 32 | XCTAssertEqual(Employee.entityName, "Employee") 33 | } 34 | 35 | func test_thatManagedObjects_returnCorrect_entityDescription() { 36 | // GIVEN: managed objects 37 | // WHEN: we ask for entity descriptions 38 | // THEN: we receive expected values 39 | 40 | let companyEntity = Company.entity(context: self.inMemoryStack.mainContext) 41 | XCTAssertEqual(companyEntity.name, "Company") 42 | XCTAssertEqual(companyEntity.managedObjectModel, self.inMemoryModel.managedObjectModel) 43 | 44 | let employeeEntity = Employee.entity(context: self.inMemoryStack.mainContext) 45 | XCTAssertEqual(employeeEntity.name, "Employee") 46 | XCTAssertEqual(employeeEntity.managedObjectModel, self.inMemoryModel.managedObjectModel) 47 | } 48 | 49 | func test_thatManagedObjects_returnCorrect_fetchRequest_andFetchesFromContext() { 50 | // GIVEN: managed objects 51 | let context = self.inMemoryStack.mainContext 52 | context.performAndWait { 53 | self.generateDataInContext(context, companiesCount: 2, employeesCount: 4) 54 | } 55 | 56 | // WHEN: we generate and execute fetch requests 57 | let companyFetch = Company.fetchRequest 58 | let companyResults = try? context.fetch(companyFetch) 59 | 60 | let employeeFetch = Employee.fetchRequest 61 | let employeeResults = try? context.fetch(employeeFetch) 62 | 63 | // THEN: we receive expected values 64 | XCTAssertNotNil(companyResults) 65 | XCTAssertEqual(companyResults?.count, 2) 66 | 67 | XCTAssertNotNil(employeeResults) 68 | XCTAssertEqual(employeeResults?.count, 8) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/ExampleModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import ExampleModel 20 | @testable import JSQCoreDataKit 21 | import XCTest 22 | 23 | final class ExampleModelTests: TestCase { 24 | 25 | func test_ThatEmployeeInsertsSuccessfully() { 26 | let employee = Employee.newEmployee(self.inMemoryStack.mainContext) 27 | XCTAssertNotNil(employee) 28 | } 29 | 30 | func test_ThatCompanyInsertsSuccessfully() { 31 | let company = Company.newCompany(self.inMemoryStack.mainContext) 32 | XCTAssertNotNil(company) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/ModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import ExampleModel 20 | @testable import JSQCoreDataKit 21 | import XCTest 22 | 23 | // swiftlint:disable force_try 24 | 25 | final class ModelTests: XCTestCase { 26 | 27 | override func setUp() { 28 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 29 | _ = try? model.removeExistingStore() 30 | super.setUp() 31 | } 32 | 33 | func test_ThatSQLiteModel_InitializesSuccessfully() { 34 | // GIVEN: a model name and bundle 35 | 36 | // WHEN: we create a model 37 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 38 | 39 | // THEN: the model has the correct name, bundle, and type 40 | XCTAssertEqual(model.name, modelName) 41 | XCTAssertEqual(model.bundle, modelBundle) 42 | XCTAssertEqual(model.storeType, StoreType.sqlite(CoreDataModel.defaultDirectoryURL())) 43 | 44 | // THEN: the model returns the correct database filename 45 | XCTAssertEqual(model.databaseFileName, model.name + "." + ModelFileExtension.sqlite.rawValue) 46 | 47 | // THEN: the store file is in the correct directory 48 | #if os(iOS) || os(macOS) || os(watchOS) 49 | let dir = "Documents" 50 | #elseif os(tvOS) 51 | let dir = "Caches" 52 | #endif 53 | 54 | let storeURLComponents = model.storeURL!.pathComponents 55 | XCTAssertEqual(String(storeURLComponents.last!), model.databaseFileName) 56 | XCTAssertEqual(String(storeURLComponents[storeURLComponents.count - 2]), dir) 57 | XCTAssertTrue(model.storeURL!.isFileURL) 58 | 59 | // THEN: the model is in its specified bundle 60 | let modelURLComponents = model.modelURL.pathComponents 61 | XCTAssertEqual(String(modelURLComponents.last!), model.name + ".momd") 62 | 63 | #if os(iOS) || os(tvOS) || os(watchOS) 64 | let count = modelURLComponents.count - 2 65 | #elseif os(macOS) 66 | let count = modelURLComponents.count - 3 67 | #endif 68 | 69 | XCTAssertEqual(String(modelURLComponents[count]), NSString(string: model.bundle.bundlePath).lastPathComponent) 70 | 71 | // THEN: the managed object model does not assert 72 | XCTAssertNotNil(model.managedObjectModel) 73 | 74 | // THEN: the store doesn't need migration 75 | XCTAssertFalse(model.needsMigration) 76 | } 77 | 78 | func test_ThatBinaryModel_InitializesSuccessfully() { 79 | // GIVEN: a model name and bundle 80 | 81 | // WHEN: we create a model 82 | let model = CoreDataModel(name: modelName, bundle: modelBundle, storeType: .binary(URL(fileURLWithPath: NSTemporaryDirectory()))) 83 | 84 | // THEN: the model has the correct name, bundle, and type 85 | XCTAssertEqual(model.name, modelName) 86 | XCTAssertEqual(model.bundle, modelBundle) 87 | XCTAssertEqual(model.storeType, StoreType.binary(URL(fileURLWithPath: NSTemporaryDirectory()))) 88 | 89 | // THEN: the model returns the correct database filename 90 | XCTAssertEqual(model.databaseFileName, model.name) 91 | 92 | // THEN: the store file is in the tmp directory 93 | let storeURLComponents = model.storeURL!.pathComponents 94 | XCTAssertEqual(String(storeURLComponents.last!), model.databaseFileName) 95 | 96 | #if os(iOS) || os(tvOS) || os(watchOS) 97 | let temp = "tmp" 98 | #elseif os(macOS) 99 | let temp = "T" 100 | #endif 101 | 102 | XCTAssertEqual(String(storeURLComponents[storeURLComponents.count - 2]), temp) 103 | XCTAssertTrue(model.storeURL!.isFileURL) 104 | 105 | // THEN: the model is in its specified bundle 106 | let modelURLComponents = model.modelURL.pathComponents 107 | XCTAssertEqual(String(modelURLComponents.last!), model.name + ".momd") 108 | 109 | #if os(iOS) || os(tvOS) || os(watchOS) 110 | let count = modelURLComponents.count - 2 111 | #elseif os(macOS) 112 | let count = modelURLComponents.count - 3 113 | #endif 114 | 115 | XCTAssertEqual(String(modelURLComponents[count]), NSString(string: model.bundle.bundlePath).lastPathComponent) 116 | 117 | // THEN: the managed object model does not assert 118 | XCTAssertNotNil(model.managedObjectModel) 119 | 120 | // THEN: the store doesn't need migration 121 | XCTAssertFalse(model.needsMigration) 122 | } 123 | 124 | func test_ThatInMemoryModel_InitializesSuccessfully() { 125 | // GIVEN: a model name and bundle 126 | 127 | // WHEN: we create a model 128 | let model = CoreDataModel(name: modelName, bundle: modelBundle, storeType: .inMemory) 129 | 130 | // THEN: the model has the correct name, bundle, and type 131 | XCTAssertEqual(model.name, modelName) 132 | XCTAssertEqual(model.bundle, modelBundle) 133 | XCTAssertEqual(model.storeType, StoreType.inMemory) 134 | 135 | // THEN: the model returns the correct database filename 136 | XCTAssertEqual(model.databaseFileName, model.name) 137 | 138 | // THEN: the store URL is nil 139 | XCTAssertNil(model.storeURL) 140 | 141 | // THEN: the model is in its specified bundle 142 | let modelURLComponents = model.modelURL.pathComponents 143 | XCTAssertEqual(String(modelURLComponents.last!), model.name + ".momd") 144 | 145 | #if os(iOS) || os(tvOS) || os(watchOS) 146 | let count = modelURLComponents.count - 2 147 | #elseif os(macOS) 148 | let count = modelURLComponents.count - 3 149 | #endif 150 | 151 | XCTAssertEqual(String(modelURLComponents[count]), NSString(string: model.bundle.bundlePath).lastPathComponent) 152 | 153 | // THEN: the managed object model does not assert 154 | XCTAssertNotNil(model.managedObjectModel) 155 | 156 | // THEN: the store doesn't need migration 157 | XCTAssertFalse(model.needsMigration) 158 | } 159 | 160 | func test_ThatSQLiteModel_RemoveExistingStore_Succeeds() { 161 | // GIVEN: a core data model and stack 162 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 163 | let factory = CoreDataStackProvider(model: model) 164 | let result = factory.createStack() 165 | let stack = try! result.get() 166 | stack.mainContext.saveSync() 167 | 168 | let fileManager = FileManager.default 169 | 170 | XCTAssertTrue(fileManager.fileExists(atPath: model.storeURL!.path), "Model store should exist on disk") 171 | XCTAssertTrue(fileManager.fileExists(atPath: model.storeURL!.path + "-wal"), "Model write ahead log should exist on disk") 172 | XCTAssertTrue(fileManager.fileExists(atPath: model.storeURL!.path + "-shm"), "Model shared memory file should exist on disk") 173 | 174 | // WHEN: we remove the existing model store 175 | do { 176 | try model.removeExistingStore() 177 | } catch { 178 | XCTFail("Removing existing model store should not error.") 179 | } 180 | 181 | // THEN: the model store is successfully removed 182 | XCTAssertFalse(fileManager.fileExists(atPath: model.storeURL!.path), "Model store should NOT exist on disk") 183 | XCTAssertFalse(fileManager.fileExists(atPath: model.storeURL!.path + "-wal"), "Model write ahead log should NOT exist on disk") 184 | XCTAssertFalse(fileManager.fileExists(atPath: model.storeURL!.path + "-shm"), "Model shared memory file should NOT exist on disk") 185 | } 186 | 187 | func test_ThatSQLiteModel_RemoveExistingStore_Fails() { 188 | // GIVEN: a core data model 189 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 190 | 191 | // WHEN: we do not create a core data stack 192 | 193 | // THEN: the model store does not exist on disk 194 | XCTAssertFalse(FileManager.default.fileExists(atPath: model.storeURL!.path), "Model store should not exist on disk") 195 | 196 | // WHEN: we attempt to remove the existing model store 197 | var success = true 198 | do { 199 | try model.removeExistingStore() 200 | } catch { 201 | success = false 202 | } 203 | 204 | // THEN: then removal is ignored 205 | XCTAssertTrue(success, "Removing store should be ignored") 206 | } 207 | 208 | func test_ThatInMemoryModel_RemoveExistingStore_Fails() { 209 | // GIVEN: a core data model in-memory 210 | let model = CoreDataModel(name: modelName, bundle: modelBundle, storeType: .inMemory) 211 | 212 | // THEN: the store URL is nil 213 | XCTAssertNil(model.storeURL) 214 | 215 | // WHEN: we attempt to remove the existing model store 216 | var success = true 217 | do { 218 | try model.removeExistingStore() 219 | } catch { 220 | success = false 221 | } 222 | 223 | // THEN: then removal is ignored 224 | XCTAssertTrue(success, "Removing store should be ignored") 225 | } 226 | } 227 | 228 | // swiftlint:enable force_try 229 | -------------------------------------------------------------------------------- /Tests/ResetStackTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | @testable import JSQCoreDataKit 22 | import XCTest 23 | 24 | // swiftlint:disable force_try 25 | 26 | final class ResetStackTests: TestCase { 27 | 28 | func test_ThatMainContext_WithChanges_DoesNotHaveObjects_AfterReset() { 29 | // GIVEN: a stack and context with changes 30 | let context = self.inMemoryStack.mainContext 31 | context.performAndWait { 32 | self.generateCompaniesInContext(context, count: 3) 33 | } 34 | 35 | let expectation = self.expectation(description: #function) 36 | 37 | // WHEN: we attempt to reset the stack 38 | self.inMemoryStack.reset { result in 39 | if case .failure(let error) = result { 40 | XCTFail("Error while resetting the stack: \(error)") 41 | } 42 | expectation.fulfill() 43 | } 44 | 45 | // THEN: the reset succeeds and the contexts contain no objects 46 | self.waitForExpectations(timeout: defaultTimeout) { error -> Void in 47 | XCTAssertNil(error, "Expectation should not error") 48 | } 49 | 50 | XCTAssertEqual(self.inMemoryStack.mainContext.registeredObjects.count, 0) 51 | XCTAssertEqual(self.inMemoryStack.backgroundContext.registeredObjects.count, 0) 52 | } 53 | 54 | func test_ThatBackgroundContext_WithChanges_DoesNotHaveObjects_AfterReset() { 55 | // GIVEN: a stack and context with changes 56 | let context = self.inMemoryStack.backgroundContext 57 | context.performAndWait { 58 | self.generateCompaniesInContext(context, count: 3) 59 | } 60 | 61 | let expectation = self.expectation(description: #function) 62 | 63 | // WHEN: we attempt to reset the stack 64 | self.inMemoryStack.reset { result in 65 | if case .failure(let error) = result { 66 | XCTFail("Error while resetting the stack: \(error)") 67 | } 68 | expectation.fulfill() 69 | } 70 | 71 | // THEN: the reset succeeds and the contexts contain no objects 72 | self.waitForExpectations(timeout: defaultTimeout) { error -> Void in 73 | XCTAssertNil(error, "Expectation should not error") 74 | } 75 | 76 | XCTAssertEqual(self.inMemoryStack.mainContext.registeredObjects.count, 0) 77 | XCTAssertEqual(self.inMemoryStack.backgroundContext.registeredObjects.count, 0) 78 | } 79 | 80 | func test_ThatPersistentStore_WithChanges_DoesNotHaveObjects_AfterReset() { 81 | // GIVEN: a stack and persistent store with data 82 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 83 | let factory = CoreDataStackProvider(model: model) 84 | let stack = try! factory.createStack().get() 85 | let context = stack.mainContext 86 | 87 | context.performAndWait { 88 | self.generateCompaniesInContext(context, count: 3) 89 | } 90 | context.saveSync() 91 | 92 | let request = Company.fetchRequest 93 | let objectsBefore = try? context.count(for: request) 94 | XCTAssertEqual(objectsBefore, 3) 95 | 96 | let expectation = self.expectation(description: #function) 97 | 98 | // WHEN: we attempt to reset the stack 99 | stack.reset { result in 100 | if case .failure(let error) = result { 101 | XCTFail("Error while resetting the stack: \(error)") 102 | } 103 | expectation.fulfill() 104 | } 105 | 106 | // THEN: the reset succeeds and the stack contains no managed objects 107 | self.waitForExpectations(timeout: defaultTimeout) { error -> Void in 108 | XCTAssertNil(error, "Expectation should not error") 109 | } 110 | 111 | let objectsAfter = try? stack.mainContext.count(for: request) 112 | XCTAssertEqual(objectsAfter, 0) 113 | } 114 | } 115 | 116 | // swiftlint:enable force_try 117 | -------------------------------------------------------------------------------- /Tests/StackFactoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | @testable import JSQCoreDataKit 22 | import XCTest 23 | 24 | final class StackFactoryTests: TestCase { 25 | 26 | override func setUp() { 27 | self.cleanUp() 28 | super.setUp() 29 | } 30 | 31 | override func tearDown() { 32 | self.cleanUp() 33 | super.tearDown() 34 | } 35 | 36 | func test_ThatStackFactory_InitializesSuccessFully() { 37 | let factory = CoreDataStackProvider(model: self.inMemoryModel) 38 | XCTAssertEqual(factory.model, self.inMemoryModel) 39 | XCTAssertTrue(factory.options?[NSMigratePersistentStoresAutomaticallyOption] as! Bool) 40 | XCTAssertTrue(factory.options?[NSInferMappingModelAutomaticallyOption] as! Bool) 41 | } 42 | 43 | func test_ThatStackFactory_CreatesStackInBackground_Successfully() { 44 | // GIVEN: a core data model 45 | let sqliteModel = CoreDataModel(name: modelName, bundle: modelBundle) 46 | 47 | // GIVEN: a factory 48 | let factory = CoreDataStackProvider(model: sqliteModel) 49 | 50 | var stack: CoreDataStack? 51 | let expectation = self.expectation(description: #function) 52 | 53 | // WHEN: we create a stack in the background 54 | factory.createStack { result in 55 | XCTAssertTrue(Thread.isMainThread, "Factory completion handler should return on main thread") 56 | 57 | switch result { 58 | case .success(let success): 59 | stack = success 60 | XCTAssertNotNil(stack) 61 | 62 | case .failure(let error): 63 | XCTFail("Error: \(error)") 64 | } 65 | 66 | expectation.fulfill() 67 | } 68 | 69 | // THEN: creating a stack succeeds 70 | self.waitForExpectations(timeout: defaultTimeout) { error -> Void in 71 | XCTAssertNil(error, "Expectation should not error") 72 | } 73 | 74 | XCTAssertNotNil(stack) 75 | 76 | self.validateStack(stack!, fromFactory: factory) 77 | } 78 | 79 | func test_ThatStackFactory_CreatesStackSynchronously_Successfully() { 80 | // GIVEN: a core data model 81 | let sqliteModel = CoreDataModel(name: modelName, bundle: modelBundle) 82 | 83 | // GIVEN: a factory 84 | let factory = CoreDataStackProvider(model: sqliteModel) 85 | 86 | // WHEN: we create a stack 87 | let result = factory.createStack() 88 | let stack = try? result.get() 89 | 90 | XCTAssertNotNil(stack) 91 | 92 | self.validateStack(stack!, fromFactory: factory) 93 | } 94 | 95 | func test_StackFactory_Equality() { 96 | let factory1 = CoreDataStackProvider(model: self.inMemoryModel) 97 | let factory2 = CoreDataStackProvider(model: self.inMemoryModel) 98 | XCTAssertEqual(factory1, factory2) 99 | 100 | let factory3 = CoreDataStackProvider(model: self.inMemoryModel, options: nil) 101 | XCTAssertEqual(factory1, factory3) 102 | 103 | let factory4 = CoreDataStackProvider(model: self.inMemoryModel, options: nil) 104 | XCTAssertEqual(factory3, factory4) 105 | 106 | let sqliteModel = CoreDataModel(name: modelName, bundle: modelBundle) 107 | let sqliteFactory = CoreDataStackProvider(model: sqliteModel) 108 | XCTAssertNotEqual(factory1, sqliteFactory) 109 | } 110 | 111 | // MARK: Helpers 112 | 113 | func validateStack(_ stack: CoreDataStack, fromFactory factory: CoreDataStackProvider) { 114 | XCTAssertEqual(stack.model, factory.model) 115 | 116 | XCTAssertNotNil(stack.storeCoordinator) 117 | XCTAssertEqual(stack.mainContext.persistentStoreCoordinator, stack.backgroundContext.persistentStoreCoordinator) 118 | 119 | XCTAssertEqual(stack.mainContext.name, "JSQCoreDataKit.CoreDataStack.context.main") 120 | XCTAssertEqual(stack.mainContext.concurrencyType, NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType) 121 | XCTAssertNil(stack.mainContext.parent) 122 | XCTAssertNotNil(stack.mainContext.persistentStoreCoordinator) 123 | 124 | XCTAssertEqual(stack.backgroundContext.name, "JSQCoreDataKit.CoreDataStack.context.background") 125 | XCTAssertEqual(stack.backgroundContext.concurrencyType, NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType) 126 | XCTAssertNil(stack.backgroundContext.parent) 127 | XCTAssertNotNil(stack.backgroundContext.persistentStoreCoordinator) 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /Tests/StackTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | import ExampleModel 21 | @testable import JSQCoreDataKit 22 | import XCTest 23 | 24 | // swiftlint:disable force_try 25 | 26 | final class StackTests: XCTestCase { 27 | 28 | override func tearDown() { 29 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 30 | _ = try? model.removeExistingStore() 31 | super.tearDown() 32 | } 33 | 34 | func test_ThatSQLiteStack_InitializesSuccessfully() { 35 | // GIVEN: a SQLite model 36 | let sqliteModel = CoreDataModel(name: modelName, bundle: modelBundle) 37 | 38 | // WHEN: we create a stack 39 | let factory = CoreDataStackProvider(model: sqliteModel) 40 | let result = factory.createStack() 41 | let stack = try! result.get() 42 | 43 | // THEN: it is setup as expected 44 | XCTAssertTrue(FileManager.default.fileExists(atPath: sqliteModel.storeURL!.path), "Model store should exist on disk") 45 | XCTAssertEqual(stack.mainContext.concurrencyType, NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType) 46 | XCTAssertEqual(stack.backgroundContext.concurrencyType, NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType) 47 | } 48 | 49 | func test_ThatBinaryStack_InitializesSuccessfully() { 50 | // GIVEN: a binary model 51 | let binaryModel = CoreDataModel(name: modelName, bundle: modelBundle, storeType: .binary(URL(fileURLWithPath: NSTemporaryDirectory()))) 52 | 53 | // WHEN: we create a stack 54 | let factory = CoreDataStackProvider(model: binaryModel) 55 | let result = factory.createStack() 56 | let stack = try! result.get() 57 | 58 | // THEN: it is setup as expected 59 | XCTAssertTrue(FileManager.default.fileExists(atPath: binaryModel.storeURL!.path), "Model store should exist on disk") 60 | XCTAssertEqual(stack.mainContext.concurrencyType, NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType) 61 | XCTAssertEqual(stack.backgroundContext.concurrencyType, NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType) 62 | } 63 | 64 | func test_ThatInMemoryStack_InitializesSuccessfully() { 65 | // GIVEN: a in-memory model 66 | let inMemoryModel = CoreDataModel(name: modelName, bundle: modelBundle, storeType: .inMemory) 67 | 68 | // WHEN: we create a stack 69 | let factory = CoreDataStackProvider(model: inMemoryModel, options: nil) 70 | let result = factory.createStack() 71 | let stack = try! result.get() 72 | 73 | // THEN: it is setup as expected 74 | XCTAssertNil(inMemoryModel.storeURL, "Model store should not exist on disk") 75 | XCTAssertEqual(stack.mainContext.concurrencyType, NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType) 76 | XCTAssertEqual(stack.backgroundContext.concurrencyType, NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType) 77 | } 78 | 79 | func test_ThatChildContext_IsCreatedSuccessfully_WithDefaultParameters() { 80 | // GIVEN: a model and stack 81 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 82 | let factory = CoreDataStackProvider(model: model) 83 | let result = factory.createStack() 84 | let stack = try! result.get() 85 | 86 | // WHEN: we create a child context 87 | let childContext = stack.childContext() 88 | 89 | // THEN: it is initialized as expected 90 | XCTAssertEqual(childContext.name, "JSQCoreDataKit.CoreDataStack.context.main.child") 91 | XCTAssertEqual(childContext.parent!, stack.mainContext) 92 | XCTAssertEqual(childContext.concurrencyType, NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType) 93 | XCTAssertEqual((childContext.mergePolicy as! NSMergePolicy).mergeType, NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) 94 | } 95 | 96 | func test_ThatChildContext_IsCreatedSuccessfully_WithCustomParameters() { 97 | // GIVEN: a model and stack 98 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 99 | let factory = CoreDataStackProvider(model: model) 100 | let result = factory.createStack() 101 | let stack = try! result.get() 102 | 103 | // WHEN: we create a child context 104 | let childContext = stack.childContext(concurrencyType: .privateQueueConcurrencyType, mergePolicyType: .errorMergePolicyType) 105 | 106 | // THEN: it is initialized as expected 107 | XCTAssertEqual(childContext.name, "JSQCoreDataKit.CoreDataStack.context.background.child") 108 | XCTAssertEqual(childContext.parent!, stack.backgroundContext) 109 | XCTAssertEqual(childContext.concurrencyType, NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType) 110 | XCTAssertEqual((childContext.mergePolicy as! NSMergePolicy).mergeType, NSMergePolicyType.errorMergePolicyType) 111 | } 112 | 113 | func test_Stack_Description() { 114 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 115 | let factory = CoreDataStackProvider(model: model) 116 | let result = factory.createStack() 117 | let stack = try! result.get() 118 | print(stack) 119 | } 120 | } 121 | 122 | // swiftlint:enable force_try 123 | -------------------------------------------------------------------------------- /Tests/StoreTypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | import CoreData 20 | @testable import JSQCoreDataKit 21 | import XCTest 22 | 23 | final class StoreTypeTests: XCTestCase { 24 | 25 | let url = CoreDataModel.defaultDirectoryURL() 26 | 27 | func test_StoreType_SQLite() { 28 | let store = StoreType.sqlite(self.url) 29 | XCTAssertEqual(store.type, NSSQLiteStoreType) 30 | XCTAssertEqual(store.storeDirectory(), self.url) 31 | } 32 | 33 | func test_StoreType_Binary() { 34 | let store = StoreType.binary(self.url) 35 | XCTAssertEqual(store.type, NSBinaryStoreType) 36 | XCTAssertEqual(store.storeDirectory(), self.url) 37 | } 38 | 39 | func test_StoreType_InMemory() { 40 | let store = StoreType.inMemory 41 | XCTAssertEqual(store.type, NSInMemoryStoreType) 42 | XCTAssertNil(store.storeDirectory()) 43 | } 44 | 45 | func test_StoreType_Equality() { 46 | let sqlite = StoreType.sqlite(self.url) 47 | let binary = StoreType.binary(self.url) 48 | let memory = StoreType.inMemory 49 | 50 | XCTAssertNotEqual(sqlite, binary) 51 | XCTAssertNotEqual(sqlite, memory) 52 | XCTAssertNotEqual(binary, memory) 53 | } 54 | 55 | func test_StoreType_Equality_SQLite() { 56 | let sqlite1 = StoreType.sqlite(self.url) 57 | let sqlite2 = StoreType.sqlite(self.url) 58 | XCTAssertEqual(sqlite1, sqlite2) 59 | 60 | let sqlite3 = StoreType.sqlite(URL(fileURLWithPath: NSTemporaryDirectory())) 61 | XCTAssertNotEqual(sqlite1, sqlite3) 62 | } 63 | 64 | func test_StoreType_Equality_Binary() { 65 | let binary1 = StoreType.binary(self.url) 66 | let binary2 = StoreType.binary(self.url) 67 | XCTAssertEqual(binary1, binary2) 68 | 69 | let binary3 = StoreType.binary(URL(fileURLWithPath: NSTemporaryDirectory())) 70 | XCTAssertNotEqual(binary1, binary3) 71 | } 72 | 73 | func test_StoreType_Equality_InMemory() { 74 | let memory1 = StoreType.inMemory 75 | let memory2 = StoreType.inMemory 76 | XCTAssertEqual(memory1, memory2) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/TestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jesse Squires 3 | // https://www.jessesquires.com 4 | // 5 | // 6 | // Documentation 7 | // https://jessesquires.github.io/JSQCoreDataKit 8 | // 9 | // 10 | // GitHub 11 | // https://github.com/jessesquires/JSQCoreDataKit 12 | // 13 | // 14 | // License 15 | // Copyright © 2015-present Jesse Squires 16 | // Released under an MIT license: https://opensource.org/licenses/MIT 17 | // 18 | 19 | // swiftlint:disable force_try 20 | 21 | import CoreData 22 | import ExampleModel 23 | @testable import JSQCoreDataKit 24 | import XCTest 25 | 26 | let defaultTimeout = TimeInterval(20) 27 | 28 | extension CoreDataStackProvider { 29 | 30 | func createStack() -> CoreDataStack.StackResult { 31 | var result: CoreDataStack.StackResult! 32 | self.createStack(onQueue: nil) { result = $0 } 33 | return result 34 | } 35 | } 36 | 37 | extension XCTestCase { 38 | func cleanUp() { 39 | let model = CoreDataModel(name: modelName, bundle: modelBundle) 40 | _ = try? model.removeExistingStore() 41 | } 42 | } 43 | 44 | class TestCase: XCTestCase { 45 | 46 | let inMemoryModel = CoreDataModel(name: modelName, bundle: modelBundle, storeType: .inMemory) 47 | 48 | var inMemoryStack: CoreDataStack! 49 | 50 | override func setUp() { 51 | super.setUp() 52 | 53 | let factory = CoreDataStackProvider(model: self.inMemoryModel) 54 | let result = factory.createStack() 55 | self.inMemoryStack = try! result.get() 56 | } 57 | 58 | override func tearDown() { 59 | self.inMemoryStack = nil 60 | super.tearDown() 61 | } 62 | 63 | // MARK: Helpers 64 | 65 | @discardableResult 66 | func generateDataInContext(_ context: NSManagedObjectContext, 67 | companiesCount: Int = Int.random(in: 0...10), 68 | employeesCount: Int = Int.random(in: 0...1_000)) -> [Company] { 69 | let companies = self.generateCompaniesInContext(context, count: companiesCount) 70 | 71 | companies.forEach { company in 72 | self.generateEmployeesInContext(context, count: employeesCount, company: company) 73 | } 74 | 75 | return companies 76 | } 77 | 78 | @discardableResult 79 | func generateCompaniesInContext(_ context: NSManagedObjectContext, count: Int) -> [Company] { 80 | var companies = [Company]() 81 | 82 | for _ in 0.. [Employee] { 92 | var employees = [Employee]() 93 | 94 | for _ in 0.. 2 | 3 | 4 | Classes Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

JSQCoreDataKit 9.0.3 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 98 |
99 |
100 |
101 |

Classes

102 |

The following classes are available globally.

103 | 104 |
105 |
106 |
107 |
    108 |
  • 109 |
    110 | 111 | 112 | 113 | CoreDataStack 114 | 115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 |

    An instance of CoreDataStack encapsulates the entire Core Data stack. 122 | It manages the managed object model, the persistent store coordinator, and managed object contexts.

    123 | 124 |

    It is composed of a main context and a background context. 125 | These two contexts operate on the main queue and a private background queue, respectively. 126 | Both are connected to the persistent store coordinator and data between them is perpetually kept in sync.

    127 | 128 |

    Changes to a child context are propagated to its parent context and eventually the persistent store when saving.

    129 |
    130 |

    Warning

    131 | You cannot create a CoreDataStack instance directly. Instead, use a CoreDataStackProvider for initialization. 132 | 133 |
    134 | 135 | See more 136 |
    137 |
    138 |

    Declaration

    139 |
    140 |

    Swift

    141 |
    public final class CoreDataStack
    142 |
    extension CoreDataStack: Equatable
    143 |
    extension CoreDataStack: CustomStringConvertible
    144 | 145 |
    146 |
    147 |
    148 |
    149 |
  • 150 |
151 |
152 |
153 |
154 | 158 |
159 |
160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /docs/Extensions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Extensions Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

JSQCoreDataKit 9.0.3 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 98 |
99 |
100 |
101 |

Extensions

102 |

The following extensions are available globally.

103 | 104 |
105 |
106 |
107 |
    108 |
  • 109 |
    110 | 111 | 112 | 113 | NSManagedObjectContext 114 | 115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 | 122 | See more 123 |
    124 |
    125 |

    Declaration

    126 |
    127 |

    Swift

    128 |
    extension NSManagedObjectContext
    129 | 130 |
    131 |
    132 |
    133 |
    134 |
  • 135 |
136 |
137 |
138 |
139 | 143 |
144 |
145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/Guides.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Guides Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

JSQCoreDataKit 9.0.3 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 98 |
99 |
100 |
101 |

Guides

102 |

The following guides are available globally.

103 | 104 |
105 |
106 |
107 |
    108 |
  • 109 |
    110 | 111 | 112 | 113 | Getting Started 114 | 115 |
    116 |
  • 117 |
118 |
119 |
120 |
121 | 125 |
126 |
127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /docs/Protocols.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Protocols Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

JSQCoreDataKit 9.0.3 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 98 |
99 |
100 |
101 |

Protocols

102 |

The following protocols are available globally.

103 | 104 |
105 |
106 |
107 |
    108 |
  • 109 |
    110 | 111 | 112 | 113 | CoreDataEntityProtocol 114 | 115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 |

    Describes an entity in Core Data.

    122 | 123 | See more 124 |
    125 |
    126 |

    Declaration

    127 |
    128 |

    Swift

    129 |
    public protocol CoreDataEntityProtocol : AnyObject
    130 | 131 |
    132 |
    133 |
    134 |
    135 |
  • 136 |
137 |
138 |
139 |
140 | 144 |
145 |
146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /docs/Structs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Structures Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

JSQCoreDataKit 9.0.3 Docs (100% documented)

21 |

View on GitHub

22 |

23 |

24 | 25 |
26 |

27 |
28 |
29 |
30 | 35 |
36 |
37 | 98 |
99 |
100 |
101 |

Structures

102 |

The following structures are available globally.

103 | 104 |
105 |
106 |
107 |
    108 |
  • 109 |
    110 | 111 | 112 | 113 | CoreDataModel 114 | 115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 |

    An instance of CoreDataModel represents a Core Data model — a .xcdatamodeld file package. 122 | It provides the model and store URLs as well as methods for interacting with the store.

    123 | 124 | See more 125 |
    126 |
    127 |

    Declaration

    128 |
    129 |

    Swift

    130 |
    public struct CoreDataModel
    131 |
    extension CoreDataModel: Equatable
    132 | 133 |
    134 |
    135 |
    136 |
    137 |
  • 138 |
  • 139 |
    140 | 141 | 142 | 143 | CoreDataStackProvider 144 | 145 |
    146 |
    147 |
    148 |
    149 |
    150 |
    151 |

    An instance of CoreDataStackProvider is responsible for creating instances of CoreDataStack.

    152 | 153 |

    Because the adding of the persistent store to the persistent store coordinator during initialization 154 | of a CoreDataStack can take an unknown amount of time, you should not perform this operation on the main queue.

    155 | 156 |

    See this guide for more details.

    157 |
    158 |

    Warning

    159 | You should not create instances of CoreDataStack directly. Use a CoreDataStackProvider instead. 160 | 161 |
    162 | 163 | See more 164 |
    165 |
    166 |

    Declaration

    167 |
    168 |

    Swift

    169 |
    public struct CoreDataStackProvider
    170 |
    extension CoreDataStackProvider: Equatable
    171 | 172 |
    173 |
    174 |
    175 |
    176 |
  • 177 |
178 |
179 |
180 |
181 | 185 |
186 |
187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 100% 23 | 24 | 25 | 100% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jessesquires/JSQCoreDataKit/84f382e959c2b238e996a7c29e5254069485467f/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jessesquires/JSQCoreDataKit/84f382e959c2b238e996a7c29e5254069485467f/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jessesquires/JSQCoreDataKit/84f382e959c2b238e996a7c29e5254069485467f/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jessesquires/JSQCoreDataKit/84f382e959c2b238e996a7c29e5254069485467f/docs/img/spinner.gif -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | 4 | ], 5 | "source_directory": "/Users/jsq/GitHub/JSQCoreDataKit" 6 | } -------------------------------------------------------------------------------- /scripts/build_docs.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Created by Jesse Squires 4 | # https://www.jessesquires.com 5 | # 6 | # Copyright © 2020-present Jesse Squires 7 | # 8 | # Jazzy: https://github.com/realm/jazzy/releases/latest 9 | # Generates documentation using jazzy and checks for installation. 10 | 11 | VERSION="0.14.4" 12 | 13 | FOUND=$(jazzy --version) 14 | LINK="https://github.com/realm/jazzy" 15 | INSTALL="gem install jazzy" 16 | 17 | PROJECT="JSQCoreDataKit" 18 | 19 | if which jazzy >/dev/null; then 20 | jazzy \ 21 | --clean \ 22 | --author "Jesse Squires" \ 23 | --author_url "https://jessesquires.com" \ 24 | --github_url "https://github.com/jessesquires/$PROJECT" \ 25 | --module "$PROJECT" \ 26 | --source-directory . \ 27 | --readme "README.md" \ 28 | --documentation "Guides/*.md" \ 29 | --output docs/ 30 | else 31 | echo " 32 | Error: Jazzy not installed! 33 | 34 | Download: $LINK 35 | Install: $INSTALL 36 | " 37 | exit 1 38 | fi 39 | 40 | if [ "$FOUND" != "jazzy version: $VERSION" ]; then 41 | echo " 42 | Warning: incorrect Jazzy installed! Please upgrade. 43 | Expected: $VERSION 44 | Found: $FOUND 45 | 46 | Download: $LINK 47 | Install: $INSTALL 48 | " 49 | fi 50 | 51 | exit 52 | -------------------------------------------------------------------------------- /scripts/lint.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Created by Jesse Squires 4 | # https://www.jessesquires.com 5 | # 6 | # Copyright © 2020-present Jesse Squires 7 | # 8 | # SwiftLint: https://github.com/realm/SwiftLint/releases/latest 9 | # 10 | # Runs SwiftLint and checks for installation of correct version. 11 | 12 | set -e 13 | export PATH="$PATH:/opt/homebrew/bin" 14 | 15 | PROJECT="JSQCoreDataKit.xcodeproj" 16 | SCHEME="JSQCoreDataKit" 17 | 18 | VERSION="0.54.0" 19 | 20 | FOUND=$(swiftlint version) 21 | LINK="https://github.com/realm/SwiftLint" 22 | INSTALL="brew install swiftlint" 23 | 24 | CONFIG="./.swiftlint.yml" 25 | 26 | if which swiftlint >/dev/null; then 27 | echo "Running swiftlint..." 28 | echo "" 29 | 30 | # no arguments, just lint without fixing 31 | if [[ $# -eq 0 ]]; then 32 | swiftlint --config $CONFIG 33 | echo "" 34 | fi 35 | 36 | for argval in "$@" 37 | do 38 | # run --fix 39 | if [[ "$argval" == "fix" ]]; then 40 | echo "Auto-correcting lint errors..." 41 | echo "" 42 | swiftlint --fix --progress --config $CONFIG && swiftlint --config $CONFIG 43 | echo "" 44 | # run analyze 45 | elif [[ "$argval" == "analyze" ]]; then 46 | LOG="xcodebuild.log" 47 | echo "Running anaylze..." 48 | echo "" 49 | xcodebuild -scheme $SCHEME -project $PROJECT clean build-for-testing > $LOG 50 | swiftlint analyze --fix --progress --format --strict --config $CONFIG --compiler-log-path $LOG 51 | rm $LOG 52 | echo "" 53 | else 54 | echo "Error: invalid arguments." 55 | echo "Usage: $0 [fix] [analyze]" 56 | echo "" 57 | fi 58 | done 59 | else 60 | echo " 61 | Error: SwiftLint not installed! 62 | 63 | Download: $LINK 64 | Install: $INSTALL 65 | " 66 | fi 67 | 68 | if [ $FOUND != $VERSION ]; then 69 | echo " 70 | Warning: incorrect SwiftLint installed! Please upgrade. 71 | Expected: $VERSION 72 | Found: $FOUND 73 | 74 | Download: $LINK 75 | Install: $INSTALL 76 | " 77 | fi 78 | 79 | exit 80 | --------------------------------------------------------------------------------