├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .markdownlint.json ├── .periphery.yml ├── .prettierignore ├── .prettierrc.json ├── .ruby-version ├── Brewfile ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── Package.swift ├── README.md ├── SECURITY.md ├── Sources └── AsyncCloudKit │ ├── ACKSequence.swift │ ├── Extensions │ └── AsyncSequence.swift │ ├── OperationFactory.swift │ ├── Progress.swift │ └── Protocols │ ├── ACKContainer.swift │ ├── ACKDatabase+CKRecord.swift │ ├── ACKDatabase+CKRecordZone.swift │ ├── ACKDatabase+CKSubscription.swift │ ├── ACKDatabase.swift │ ├── ACKDatabaseOperation.swift │ ├── ACKFetchRecordZonesOperation.swift │ ├── ACKFetchRecordsOperation.swift │ ├── ACKFetchSubscriptionsOperation.swift │ ├── ACKModifyRecordZonesOperation.swift │ ├── ACKModifyRecordsOperation.swift │ ├── ACKModifySubscriptionsOperation.swift │ ├── ACKOperation.swift │ └── ACKQueryOperation.swift ├── Tests └── AsyncCloudKitTests │ ├── AsyncCloudKitTests.swift │ ├── CKContainerTests.swift │ ├── CKDatabaseTests.swift │ ├── ErrorInjectionTests.swift │ ├── Mocks │ ├── MockContainer.swift │ ├── MockDatabase.swift │ ├── MockDatabaseOperation.swift │ ├── MockError.swift │ ├── MockFetchOperation.swift │ ├── MockFetchRecordZonesOperation.swift │ ├── MockFetchRecordsOperation.swift │ ├── MockFetchSubscriptionsOperation.swift │ ├── MockModifyOperation.swift │ ├── MockModifyRecordZonesOperation.swift │ ├── MockModifyRecordsOperation.swift │ ├── MockModifySubscriptionsOperation.swift │ ├── MockOperation.swift │ ├── MockOperationFactory.swift │ └── MockQueryOperation.swift │ ├── ProgressTests.swift │ └── Simulation │ └── DecisionSpace.swift ├── docs ├── CNAME ├── Enums.html ├── Enums │ └── Progress.html ├── Extensions.html ├── Protocols.html ├── Protocols │ ├── ACKContainer.html │ └── ACKDatabase.html ├── Structs.html ├── Structs │ ├── ACKSequence.html │ └── ACKSequence │ │ └── Iterator.html ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── 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 └── script ├── build_docs └── lint /.gitattributes: -------------------------------------------------------------------------------- 1 | # Trailing spaces may be intentional in markdown documents, so these should not 2 | # be removed. 3 | # https://gist.github.com/shaunlebron/746476e6e7a4d698b373 4 | **/*.md whitespace=-blank-at-eol 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve AsyncCloudKit 4 | title: ':bug: ' 5 | labels: bug 6 | assignees: chris-araman 7 | --- 8 | 9 | ## Describe the bug 10 | 11 | A clear and concise description of the problem. 12 | 13 | ## How to reproduce 14 | 15 | Steps to follow to reproduce the problem: 16 | 17 | 1. Do a thing. 18 | 1. Then do this. 19 | 1. Then this. 20 | 21 | Please include sample code that reproduces the problem. 22 | 23 | ```swift 24 | let records = try await database 25 | .performQuery(ofType: "ToDoItem", where: NSPredicate(true)) 26 | .collect() 27 | XCTAssertGreaterThan(records.count, 0) 28 | ``` 29 | 30 | ## Unexpected behavior 31 | 32 | A clear and concise description of what happened that you did not expect to happen. 33 | 34 | > AsyncCloudKit does not return any `CKRecord` results. The assertion fails. 35 | 36 | ## Expected behavior 37 | 38 | A clear and concise description of what you expected to happen. 39 | 40 | > I expect to receive at least one `CKRecord` in `sink`. 41 | 42 | ## Environment 43 | 44 | - AsyncCloudKit version 45 | - macOS, iOS, watchOS, or tvOS version 46 | - Swift version 47 | - Xcode version 48 | 49 | > AsyncCloudKit 1.0.0, macOS Catalyst 12.0, Swift 5.5.2, Xcode 13.2 50 | 51 | ## Additional context 52 | 53 | Any other context about the problem. 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or enhancement for AsyncCloudKit 4 | title: ':sparkles: ' 5 | labels: enhancement 6 | assignees: chris-araman 7 | --- 8 | 9 | ## Problem statement 10 | 11 | A clear and concise description of what the problem is. 12 | 13 | > AsyncCloudKit doesn't yet support X. 14 | 15 | ## Proposed solution 16 | 17 | A clear and concise description of what you want to happen. 18 | 19 | > Implement support for X using Y and Z. 20 | 21 | ## Alternatives considered 22 | 23 | A clear and concise description of any alternative solutions you've considered. 24 | 25 | ## Additional context 26 | 27 | Add any other context about the feature request here. 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: 'bundler' 5 | directory: '/' 6 | schedule: 7 | interval: 'daily' 8 | - package-ecosystem: 'github-actions' 9 | directory: '/' 10 | schedule: 11 | interval: 'daily' 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Change Description 2 | 3 | Fixes #. 4 | 5 | Changes Proposed: 6 | 7 | - foo 8 | - bar 9 | - baz 10 | 11 | Alternatives Considered: 12 | 13 | - this 14 | - that 15 | - the other 16 | 17 | I have: 18 | 19 | - [ ] Reviewed and followed the [contribution guidelines](https://github.com/chris-araman/AsyncCloudKit/blob/main/CONTRIBUTING.md) 20 | - [ ] Built my changes with `swift build` 21 | - [ ] Tested my changes with `swift test` 22 | - [ ] Linted my changes with `script/lint` 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | workflow_dispatch: 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: macos-latest 15 | steps: 16 | - name: Clone 17 | uses: actions/checkout@v2 18 | - name: Select Swift for swift-format 19 | uses: mxcl/xcodebuild@v1 20 | with: 21 | action: none 22 | # Keep in sync with Homebrew's swift-format formula. 23 | swift: ~5.5 24 | - name: Lint 25 | run: script/lint 26 | build-test: 27 | name: Build & Test (Swift ${{ matrix.swift }}, ${{ matrix.platform-name || matrix.platform }}) 28 | runs-on: macos-latest 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | swift: ['5.5'] 33 | platform: [macOS, mac-catalyst, iOS, tvOS, watchOS] 34 | include: 35 | - platform: mac-catalyst 36 | platform-name: Mac Catalyst 37 | steps: 38 | - name: Clone 39 | uses: actions/checkout@v2 40 | - name: Build & Test 41 | uses: mxcl/xcodebuild@v1 42 | with: 43 | platform: ${{ matrix.platform }} 44 | swift: ~${{ matrix.swift }} 45 | code-coverage: true 46 | warnings-as-errors: true 47 | - name: Prepare for Code Coverage 48 | uses: sersoft-gmbh/xcode-coverage-action@v2 49 | with: 50 | fail-on-empty-output: true 51 | output: .xcodecov 52 | target-name-filter: AsyncCloudKit 53 | - name: Coverage 54 | uses: codecov/codecov-action@v2 55 | with: 56 | flags: ${{ matrix.platform }},swift${{ matrix.swift }} 57 | token: ${{ secrets.CODECOV_TOKEN }} 58 | fail_ci_if_error: true 59 | directory: .xcodecov 60 | sanitize: 61 | name: ${{ matrix.name }} Sanitizer 62 | runs-on: macos-latest 63 | strategy: 64 | fail-fast: false 65 | matrix: 66 | # Something is amiss with async generics and thread sanitizer. 67 | # https://bugs.swift.org/browse/SR-15475 68 | # TODO: Add 'scudo' once it is supported by Swift. 69 | sanitizer: [address, undefined] 70 | include: 71 | - sanitizer: address 72 | name: Address 73 | # - sanitizer: thread 74 | # name: Thread 75 | - sanitizer: undefined 76 | name: Undefined Behavior 77 | steps: 78 | - name: Clone 79 | uses: actions/checkout@v2 80 | - name: Select Latest Swift 81 | uses: mxcl/xcodebuild@v1 82 | with: 83 | action: none 84 | swift: ^5 85 | - name: Sanitize 86 | run: swift test --sanitize=${{ matrix.sanitizer }} 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Swift Package Manager 5 | /.build/ 6 | /.swiftpm/ 7 | /Packages 8 | /*.xcodeproj 9 | xcuserdata/ 10 | 11 | # Xcode 12 | /build/ 13 | 14 | # Homebrew 15 | Brewfile.lock.json 16 | 17 | # Jazzy 18 | /docs/docsets/ 19 | 20 | # Visual Studio Code 21 | /.vscode/ 22 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": { 3 | "code_block_line_length": 160, 4 | "line_length": 120 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | retain_public: true 2 | strict: true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .build 2 | .swiftpm 3 | docs 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.0 2 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "markdownlint-cli" 2 | brew "prettier" 3 | brew "shellcheck" 4 | brew "shfmt" 5 | brew "swift-format" 6 | tap "peripheryapp/periphery" 7 | cask "periphery" 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | community@hiddenplace.dev. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | . Translations are available at 128 | . 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to AsyncCloudKit 2 | 3 | There are three ways in which you can contribute to the project. 4 | 5 | ## ❤️ Sponsorship 6 | 7 | [![Sponsor](https://img.shields.io/badge/Sponsor-chris--araman-slateblue?logo=github&style=flat-square)](https://github.com/sponsors/chris-araman) 8 | 9 | Your sponsorship will enable me to spend more time contributing to open source projects. Thanks for your support! 10 | 11 | ## 🐛 Issues 12 | 13 | Submit [bug reports](https://github.com/chris-araman/AsyncCloudKit/issues/new?template=bug_report.md) and 14 | [feature requests](https://github.com/chris-araman/AsyncCloudKit/issues/new?template=feature_request.md) using the 15 | provided templates. I can't guarantee I can resolve everything reported, but I'd like the opportunity to try. 16 | Sponsorship can be quite motivating. 😊 17 | 18 | ## 🧑🏽‍💻👩🏿‍💻👨🏻‍💻 Pull Requests 19 | 20 | I welcome high quality pull requests from everyone! 🦄 21 | 22 | Pull requests are preferred over bug reports and feature requests. ✨ 23 | 24 | Submit [pull requests](https://github.com/chris-araman/AsyncCloudKit/compare) from your fork of the repository. I may 25 | suggest some changes or improvements or alternatives. To increase the chance that your pull request is accepted: 26 | 27 | - Ensure your changes build, test, and lint successfully. 28 | - Document any new public types or functions and include the generated documentation with your changes. 29 | - Write tests for any new functionality and backfill tests to improve code coverage. 30 | - Write clear, concise commit messages. 31 | - Follow the surrounding code style. 32 | 33 | ### 🛠 Building AsyncCloudKit 34 | 35 | Use the Swift Package Manager to build: 36 | 37 | ```bash 38 | swift build 39 | ``` 40 | 41 | ### ✅ Testing AsyncCloudKit 42 | 43 | Unit testing is accomplished using mock CloudKit types. 44 | 45 | [![Coverage](https://img.shields.io/codecov/c/github/chris-araman/AsyncCloudKit/main?style=flat-square&color=informational)](https://app.codecov.io/gh/chris-araman/AsyncCloudKit/) 46 | 47 | > 🚧 Integration testing against the CloudKit API and service will require app entitlements. Some work 48 | > remains to wire this up automatically to `swift test`. 49 | 50 | Use the Swift Package Manager to test: 51 | 52 | ```bash 53 | swift test 54 | ``` 55 | 56 | ### 🧹 Linting AsyncCloudKit 57 | 58 | We check for lint using several tools, many of which are installed using [Homebrew](https://brew.sh). Please fix any 59 | issues reported here before submitting a pull request. 60 | 61 | Check for lint: 62 | 63 | ```bash 64 | script/lint 65 | ``` 66 | 67 | ### 📘 Documentation 68 | 69 | AsyncCloudKit is 💯% [documented](https://asynccloudkit.hiddenplace.dev). Please include documentation for any new 70 | public types or functions using 71 | [markup](https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_markup_formatting_ref/) with 72 | appropriate [syntax](https://github.com/apple/swift/blob/main/docs/DocumentationComments.md). 73 | 74 | Generate new documentation pages: 75 | 76 | ```bash 77 | script/build_docs 78 | ``` 79 | 80 | Commit the new documentation pages with your changes. 81 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "jazzy" 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | activesupport (6.1.4.4) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | zeitwerk (~> 2.3) 12 | addressable (2.8.0) 13 | public_suffix (>= 2.0.2, < 5.0) 14 | algoliasearch (1.27.5) 15 | httpclient (~> 2.8, >= 2.8.3) 16 | json (>= 1.5.1) 17 | atomos (0.1.3) 18 | claide (1.0.3) 19 | cocoapods (1.11.2) 20 | addressable (~> 2.8) 21 | claide (>= 1.0.2, < 2.0) 22 | cocoapods-core (= 1.11.2) 23 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 24 | cocoapods-downloader (>= 1.4.0, < 2.0) 25 | cocoapods-plugins (>= 1.0.0, < 2.0) 26 | cocoapods-search (>= 1.0.0, < 2.0) 27 | cocoapods-trunk (>= 1.4.0, < 2.0) 28 | cocoapods-try (>= 1.1.0, < 2.0) 29 | colored2 (~> 3.1) 30 | escape (~> 0.0.4) 31 | fourflusher (>= 2.3.0, < 3.0) 32 | gh_inspector (~> 1.0) 33 | molinillo (~> 0.8.0) 34 | nap (~> 1.0) 35 | ruby-macho (>= 1.0, < 3.0) 36 | xcodeproj (>= 1.21.0, < 2.0) 37 | cocoapods-core (1.11.2) 38 | activesupport (>= 5.0, < 7) 39 | addressable (~> 2.8) 40 | algoliasearch (~> 1.0) 41 | concurrent-ruby (~> 1.1) 42 | fuzzy_match (~> 2.0.4) 43 | nap (~> 1.0) 44 | netrc (~> 0.11) 45 | public_suffix (~> 4.0) 46 | typhoeus (~> 1.0) 47 | cocoapods-deintegrate (1.0.5) 48 | cocoapods-downloader (1.5.1) 49 | cocoapods-plugins (1.0.0) 50 | nap 51 | cocoapods-search (1.0.1) 52 | cocoapods-trunk (1.6.0) 53 | nap (>= 0.8, < 2.0) 54 | netrc (~> 0.11) 55 | cocoapods-try (1.2.0) 56 | colored2 (3.1.2) 57 | concurrent-ruby (1.1.9) 58 | escape (0.0.4) 59 | ethon (0.15.0) 60 | ffi (>= 1.15.0) 61 | ffi (1.15.4) 62 | fourflusher (2.3.1) 63 | fuzzy_match (2.0.4) 64 | gh_inspector (1.1.3) 65 | httpclient (2.8.3) 66 | i18n (1.8.11) 67 | concurrent-ruby (~> 1.0) 68 | jazzy (0.14.1) 69 | cocoapods (~> 1.5) 70 | mustache (~> 1.1) 71 | open4 (~> 1.3) 72 | redcarpet (~> 3.4) 73 | rexml (~> 3.2) 74 | rouge (>= 2.0.6, < 4.0) 75 | sassc (~> 2.1) 76 | sqlite3 (~> 1.3) 77 | xcinvoke (~> 0.3.0) 78 | json (2.6.1) 79 | liferaft (0.0.6) 80 | minitest (5.15.0) 81 | molinillo (0.8.0) 82 | mustache (1.1.1) 83 | nanaimo (0.3.0) 84 | nap (1.1.0) 85 | netrc (0.11.0) 86 | open4 (1.3.4) 87 | public_suffix (4.0.6) 88 | redcarpet (3.5.1) 89 | rexml (3.2.5) 90 | rouge (3.27.0) 91 | ruby-macho (2.5.1) 92 | sassc (2.4.0) 93 | ffi (~> 1.9) 94 | sqlite3 (1.4.2) 95 | typhoeus (1.4.0) 96 | ethon (>= 0.9.0) 97 | tzinfo (2.0.4) 98 | concurrent-ruby (~> 1.0) 99 | xcinvoke (0.3.0) 100 | liferaft (~> 0.0.6) 101 | xcodeproj (1.21.0) 102 | CFPropertyList (>= 2.3.3, < 4.0) 103 | atomos (~> 0.1.3) 104 | claide (>= 1.0.2, < 2.0) 105 | colored2 (~> 3.1) 106 | nanaimo (~> 0.3.0) 107 | rexml (~> 3.2.4) 108 | zeitwerk (2.5.1) 109 | 110 | PLATFORMS 111 | arm64-darwin-21 112 | 113 | DEPENDENCIES 114 | jazzy 115 | 116 | BUNDLED WITH 117 | 2.2.33 118 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright © 2021 [Chris Araman](https://github.com/chris-araman) 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.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AsyncCloudKit", 7 | // Concurrency for macOS 12, MacCatalyst 15, iOS 15, tvOS 15, or watchOS 8 requires Swift 5.5 or Xcode 13. 8 | // Concurrency for macOS 10.15, Mac Catalyst 13, iOS 13, tvOS 13, or watchOS 6 requires Swift 5.5.2 or Xcode 13.2.1. 9 | // XCTest requires watchOS 7.4. 10 | platforms: [ 11 | .macCatalyst(.v13), 12 | .macOS(.v10_15), 13 | .iOS(.v13), 14 | .tvOS(.v13), 15 | .watchOS("7.4"), 16 | ], 17 | products: [ 18 | .library( 19 | name: "AsyncCloudKit", 20 | targets: ["AsyncCloudKit"] 21 | ) 22 | ], 23 | targets: [ 24 | .target(name: "AsyncCloudKit"), 25 | .testTarget( 26 | name: "AsyncCloudKitTests", 27 | dependencies: ["AsyncCloudKit"] 28 | ), 29 | ], 30 | swiftLanguageVersions: [.v5] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⛅️ AsyncCloudKit 2 | 3 | Swift extensions for asynchronous CloudKit record processing. Designed for simplicity. 4 | 5 | [![Swift](https://img.shields.io/endpoint?label=swift&logo=swift&style=flat-square&url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fchris-araman%2FAsyncCloudKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/chris-araman/AsyncCloudKit) 6 | [![Platforms](https://img.shields.io/endpoint?label=platforms&logo=apple&style=flat-square&url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fchris-araman%2FAsyncCloudKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/chris-araman/AsyncCloudKit) 7 | [![License](https://img.shields.io/github/license/chris-araman/AsyncCloudKit?style=flat-square&color=informational)](https://github.com/chris-araman/AsyncCloudKit/blob/main/LICENSE.md) 8 | [![Release](https://img.shields.io/github/v/tag/chris-araman/AsyncCloudKit?style=flat-square&color=informational&label=release&sort=semver)](https://github.com/chris-araman/AsyncCloudKit/releases) 9 | 10 | [![Lint | Build | Test](https://img.shields.io/github/workflow/status/chris-araman/AsyncCloudKit/Continuous%20Integration/main?style=flat-square&logo=github&label=lint%20%7C%20build%20%7C%20test)](https://github.com/chris-araman/AsyncCloudKit/actions/workflows/ci.yml?query=branch%3Amain) 11 | [![Coverage](https://img.shields.io/codecov/c/github/chris-araman/AsyncCloudKit/main?style=flat-square&color=informational)](https://app.codecov.io/gh/chris-araman/AsyncCloudKit/) 12 | 13 | AsyncCloudKit exposes [CloudKit](https://developer.apple.com/documentation/cloudkit) operations as 14 | [async functions](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#ID639) and 15 | [AsyncSequences](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#ID640). 16 | 17 | ## ⚠️ Deprecated 18 | 19 | I am unfortunately unable to maintain this project moving forward. If you would like to fork and maintain it, please reach out. 20 | 21 | Please consider making use of the new async functionality in CloudKit and SwiftData. 22 | 23 | ## 📦 Adding AsyncCloudKit to Your Project 24 | 25 | AsyncCloudKit is a [Swift Package](https://developer.apple.com/documentation/swift_packages). 26 | Add a dependency on AsyncCloudKit to your 27 | [`Package.swift`](https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html) using 28 | [Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) or the 29 | [Swift Package Manager](https://swift.org/package-manager/). Optionally, specify a 30 | [version requirement](https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#package-dependency-requirement). 31 | 32 | ```swift 33 | dependencies: [ 34 | .package(url: "https://github.com/chris-araman/AsyncCloudKit.git", from: "1.0.0") 35 | ] 36 | ``` 37 | 38 | Then resolve the dependency: 39 | 40 | ```bash 41 | swift package resolve 42 | ``` 43 | 44 | To update to the latest AsyncCloudKit version compatible with your version requirement: 45 | 46 | ```bash 47 | swift package update AsyncCloudKit 48 | ``` 49 | 50 | ## 🌤 Using AsyncCloudKit in Your Project 51 | 52 | Swift concurrency allows you to process CloudKit records asynchronously, without writing a lot of boilerplate code involving 53 | [`CKOperation`](https://developer.apple.com/documentation/cloudkit/ckoperation/)s and completion blocks. 54 | Here, we perform a query on our 55 | [`CKDatabase`](https://developer.apple.com/documentation/cloudkit/ckdatabase), then process the results 56 | asynchronously. As each [`CKRecord`](https://developer.apple.com/documentation/cloudkit/ckrecord) is read from the 57 | database, we print its `name` field. 58 | 59 | ```swift 60 | import CloudKit 61 | import AsyncCloudKit 62 | 63 | func queryDueItems(database: CKDatabase, due: Date) async throws { 64 | for try await record in database 65 | .performQuery(ofType: "ToDoItem", where: NSPredicate(format: "due >= %@", due)) { (record: CKRecord) in 66 | print("\(record.name)") 67 | }) 68 | } 69 | ``` 70 | 71 | ### Cancellation 72 | 73 | AsyncCloudKit functions that return an `ACKSequence` queue an operation immediately. Iterating 74 | the sequence allows you to inspect the results of the operation. If you stop iterating the sequence early, 75 | the operation may be cancelled. 76 | 77 | Note that because the `atBackgroundPriority` functions are built on `CKDatabase` methods that do not provide means of 78 | cancellation, they cannot be canceled. If you need operations to respond to requests for cooperative cancellation, 79 | please use the publishers that do not have `atBackgroundPriority` in their names. You can still specify 80 | [`QualityOfService.background`](https://developer.apple.com/documentation/foundation/qualityofservice/background) 81 | by passing in a 82 | [`CKOperation.Configuration`](https://developer.apple.com/documentation/cloudkit/ckoperation/configuration). 83 | 84 | ## 📘 Documentation 85 | 86 | 💯% [documented](https://asynccloudkit.hiddenplace.dev) using [Jazzy](https://github.com/realm/jazzy). 87 | Hosted by [GitHub Pages](https://pages.github.com). 88 | 89 | ## ❤️ Contributing 90 | 91 | [Contributions](https://github.com/chris-araman/AsyncCloudKit/blob/main/CONTRIBUTING.md) are welcome! 92 | 93 | ## 📚 Further Reading 94 | 95 | To learn more about Swift Concurrency and CloudKit, watch these videos from WWDC: 96 | 97 | - [Explore structured concurrency in Swift](https://developer.apple.com/videos/play/wwdc2021/10134/) 98 | - [Discover concurrency in SwiftUI](https://developer.apple.com/videos/play/wwdc2021/10019/) 99 | - [Swift concurrency: Behind the scenes](https://developer.apple.com/videos/play/wwdc2021/10254/) 100 | 101 | ...or review Apple's documentation: 102 | 103 | - [CloudKit Overview](https://developer.apple.com/icloud/cloudkit/) 104 | - [CloudKit Documentation](https://developer.apple.com/documentation/cloudkit) 105 | - [Swift Language Features for Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html) 106 | - [Apple Concurrency Module](https://developer.apple.com/documentation/swift/swift_standard_library/concurrency) 107 | 108 | If you're looking for Swift Combine extensions for CloudKit, take a look at 109 | [CombineCloudKit](https://github.com/chris-araman/CombineCloudKit)! 110 | 111 | ## 📜 License 112 | 113 | AsyncCloudKit was created by [Chris Araman](https://github.com/chris-araman). It is published under the 114 | [MIT license](https://github.com/chris-araman/AsyncCloudKit/blob/main/LICENSE.md). 115 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report any security vulnerabilities in AsyncCloudKit, please contact security@hiddenplace.dev. 6 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/ACKSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKSequence.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/18/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | /// An [`AsyncSequence`](https://developer.apple.com/documentation/swift/asyncsequence) 10 | /// of results from an AsyncCloudKit operation. If this sequence is iterated twice, 11 | /// no second operation is queued, and no results are returned. 12 | public struct ACKSequence: AsyncSequence { 13 | /// An [`AsyncIteratorProtocol`](https://developer.apple.com/documentation/swift/asynciteratorprotocol) 14 | /// of results from an AsyncCloudKit operation. 15 | /// - SeeAlso: [`AsyncSequence`](https://developer.apple.com/documentation/swift/asyncsequence) 16 | public struct Iterator: AsyncIteratorProtocol { 17 | private let upstreamNext: () async throws -> Element? 18 | 19 | init(_ iterator: Upstream) 20 | where Upstream.Element == Element { 21 | var iterator = iterator 22 | self.upstreamNext = { 23 | try await iterator.next() 24 | } 25 | } 26 | 27 | /// Asynchronously advances to the next result and returns it, or ends the 28 | /// sequence if there is no next result. 29 | /// - Returns: The next result, if it exists, or `nil` to signal the end of 30 | /// the sequence. 31 | public mutating func next() async throws -> Element? { 32 | try await upstreamNext() 33 | } 34 | } 35 | 36 | let makeIterator: () -> Iterator 37 | 38 | init(_ upstream: Upstream) 39 | where Upstream.Element == Element { 40 | makeIterator = { 41 | Iterator(upstream.makeAsyncIterator()) 42 | } 43 | } 44 | 45 | /// Creates an ``Iterator`` of record zones from an AsyncCloudKit operation. 46 | /// - Returns: The ``Iterator``. 47 | /// - SeeAlso: [`AsyncSequence`](https://developer.apple.com/documentation/swift/asyncsequence) 48 | public func makeAsyncIterator() -> Iterator { 49 | makeIterator() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Extensions/AsyncSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSequence.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | extension AsyncSequence { 10 | func erase() -> ACKSequence { 11 | ACKSequence(self) 12 | } 13 | 14 | /// Collects all of the results from the sequence and returns them as a single array. 15 | /// - Returns: An array of the results. 16 | func collect() async throws -> [Element] { 17 | try await reduce(into: [Element]()) { $0.append($1) } 18 | } 19 | 20 | func single() async throws -> Element { 21 | var first: Element? 22 | for try await element in self { 23 | precondition(first == nil, "The sequence yielded more than one element.") 24 | first = element 25 | } 26 | 27 | precondition(first != nil, "The sequence yielded no elements.") 28 | return first! 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/OperationFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationFactory.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | /// Allows dependency injection for testing. 12 | var operationFactory: OperationFactory = CKOperationFactory() 13 | 14 | protocol OperationFactory { 15 | func createFetchAllRecordZonesOperation() -> ACKFetchRecordZonesOperation 16 | 17 | func createFetchAllSubscriptionsOperation() -> ACKFetchSubscriptionsOperation 18 | 19 | func createFetchCurrentUserRecordOperation() -> ACKFetchRecordsOperation 20 | 21 | func createFetchRecordsOperation( 22 | recordIDs: [CKRecord.ID] 23 | ) -> ACKFetchRecordsOperation 24 | 25 | func createFetchRecordZonesOperation( 26 | recordZoneIDs: [CKRecordZone.ID] 27 | ) -> ACKFetchRecordZonesOperation 28 | 29 | func createFetchSubscriptionsOperation( 30 | subscriptionIDs: [CKSubscription.ID] 31 | ) -> ACKFetchSubscriptionsOperation 32 | 33 | func createModifyRecordsOperation( 34 | recordsToSave: [CKRecord]?, 35 | recordIDsToDelete: [CKRecord.ID]? 36 | ) -> ACKModifyRecordsOperation 37 | 38 | func createModifyRecordZonesOperation( 39 | recordZonesToSave: [CKRecordZone]?, 40 | recordZoneIDsToDelete: [CKRecordZone.ID]? 41 | ) -> ACKModifyRecordZonesOperation 42 | 43 | func createModifySubscriptionsOperation( 44 | subscriptionsToSave: [CKSubscription]?, 45 | subscriptionIDsToDelete: [CKSubscription.ID]? 46 | ) -> ACKModifySubscriptionsOperation 47 | 48 | func createQueryOperation() -> ACKQueryOperation 49 | } 50 | 51 | class CKOperationFactory: OperationFactory { 52 | func createFetchAllRecordZonesOperation() -> ACKFetchRecordZonesOperation { 53 | CKFetchRecordZonesOperation.fetchAllRecordZonesOperation() 54 | } 55 | 56 | func createFetchAllSubscriptionsOperation() -> ACKFetchSubscriptionsOperation { 57 | CKFetchSubscriptionsOperation.fetchAllSubscriptionsOperation() 58 | } 59 | 60 | func createFetchCurrentUserRecordOperation() -> ACKFetchRecordsOperation { 61 | CKFetchRecordsOperation.fetchCurrentUserRecordOperation() 62 | } 63 | 64 | func createFetchRecordsOperation( 65 | recordIDs: [CKRecord.ID] 66 | ) -> ACKFetchRecordsOperation { 67 | CKFetchRecordsOperation(recordIDs: recordIDs) 68 | } 69 | 70 | func createFetchRecordZonesOperation( 71 | recordZoneIDs: [CKRecordZone.ID] 72 | ) -> ACKFetchRecordZonesOperation { 73 | CKFetchRecordZonesOperation(recordZoneIDs: recordZoneIDs) 74 | } 75 | 76 | func createFetchSubscriptionsOperation( 77 | subscriptionIDs: [CKSubscription.ID] 78 | ) -> ACKFetchSubscriptionsOperation { 79 | CKFetchSubscriptionsOperation(subscriptionIDs: subscriptionIDs) 80 | } 81 | 82 | func createModifyRecordsOperation( 83 | recordsToSave: [CKRecord]? = nil, 84 | recordIDsToDelete: [CKRecord.ID]? = nil 85 | ) -> ACKModifyRecordsOperation { 86 | CKModifyRecordsOperation( 87 | recordsToSave: recordsToSave, 88 | recordIDsToDelete: recordIDsToDelete 89 | ) 90 | } 91 | 92 | func createModifyRecordZonesOperation( 93 | recordZonesToSave: [CKRecordZone]? = nil, 94 | recordZoneIDsToDelete: [CKRecordZone.ID]? = nil 95 | ) -> ACKModifyRecordZonesOperation { 96 | CKModifyRecordZonesOperation( 97 | recordZonesToSave: recordZonesToSave, 98 | recordZoneIDsToDelete: recordZoneIDsToDelete 99 | ) 100 | } 101 | 102 | func createModifySubscriptionsOperation( 103 | subscriptionsToSave: [CKSubscription]? = nil, 104 | subscriptionIDsToDelete: [CKSubscription.ID]? = nil 105 | ) -> ACKModifySubscriptionsOperation { 106 | CKModifySubscriptionsOperation( 107 | subscriptionsToSave: subscriptionsToSave, 108 | subscriptionIDsToDelete: subscriptionIDsToDelete 109 | ) 110 | } 111 | 112 | func createQueryOperation() -> ACKQueryOperation { 113 | CKQueryOperation() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Progress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Progress.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | /// Represents the completion progress of a `CKRecord` save or fetch operation. 10 | public enum Progress { 11 | /// Initializes a `Progress` instance from a percentage expressed as a `Double` in the range [0.0, 100.0]. 12 | /// 13 | /// - Parameters: 14 | /// - percent: A `Double` in the range [0.0, 100.0]. Values are clamped to this range. 15 | /// - Returns: The `Progress`. 16 | public init(percent: Double) { 17 | self.init(rawValue: percent / 100.0) 18 | } 19 | 20 | /// The save or fetch operation is complete. 21 | case complete 22 | 23 | /// The save or fetch operation is incomplete. `percent` is a value indicating progress in the range \[0.0, 100.0\). 24 | case incomplete(percent: Double) 25 | } 26 | 27 | extension Progress: RawRepresentable { 28 | public var rawValue: Double { 29 | switch self { 30 | case .complete: 31 | return 1.0 32 | case .incomplete(let percent): 33 | return percent / 100.0 34 | } 35 | } 36 | 37 | public typealias RawValue = Double 38 | 39 | /// Initializes a `Progress` instance from a percentage expressed as a `Double` in the range [0.0, 1.0]. 40 | /// 41 | /// - Parameters: 42 | /// - percent: A `Double` in the range [0.0, 1.0]. Values are clamped to this range. 43 | /// - Returns: The `Progress`. 44 | public init(rawValue: Double) { 45 | if rawValue >= 1.0 { 46 | self = .complete 47 | } else if rawValue >= 0.0 { 48 | self = .incomplete(percent: rawValue * 100.0) 49 | } else { 50 | self = .incomplete(percent: 0.0) 51 | } 52 | } 53 | } 54 | 55 | extension Progress: Comparable { 56 | public static func < (left: Progress, right: Progress) -> Bool { 57 | left.rawValue < right.rawValue 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKContainer.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | /// An extension that declares [`CKContainer`](https://developer.apple.com/documentation/cloudkit/ckcontainer) 12 | /// conforms to the ``ACKContainer`` protocol provided by AsyncCloudKit. 13 | /// 14 | /// - SeeAlso:[`CloudKit`](https://developer.apple.com/documentation/cloudkit) 15 | extension CKContainer: ACKContainer { 16 | } 17 | 18 | /// A protocol used to abstract a [`CKContainer`](https://developer.apple.com/documentation/cloudkit/ckcontainer). 19 | /// 20 | /// Invoke the async extension method on your [`CKContainer`](https://developer.apple.com/documentation/cloudkit/ckcontainer) 21 | /// instances to fetch the account status asynchronously. 22 | /// 23 | /// - SeeAlso: [`CloudKit`](https://developer.apple.com/documentation/cloudkit) 24 | public protocol ACKContainer { 25 | /// Implemented by `CKContainer`. 26 | /// 27 | /// - SeeAlso: [`accountStatus`](https://developer.apple.com/documentation/cloudkit/ckcontainer/1399180-accountstatus) 28 | func accountStatus(completionHandler: @escaping (CKAccountStatus, Error?) -> Void) 29 | } 30 | 31 | extension ACKContainer { 32 | private func asyncFrom( 33 | _ method: @escaping (@escaping (Output, Error?) -> Void) -> Void 34 | ) async throws -> Output { 35 | try await withCheckedThrowingContinuation { continuation in 36 | method { item, error in 37 | guard error == nil else { 38 | continuation.resume(throwing: error!) 39 | return 40 | } 41 | 42 | continuation.resume(returning: item) 43 | } 44 | } 45 | } 46 | 47 | /// Determines whether the system can access the user’s iCloud account. 48 | /// 49 | /// - Returns: The [`CKAccountStatus`](https://developer.apple.com/documentation/cloudkit/ckaccountstatus). 50 | /// - SeeAlso: [`accountStatus`](https://developer.apple.com/documentation/cloudkit/ckcontainer/1399180-accountstatus) 51 | public func accountStatus() async throws -> CKAccountStatus { 52 | try await asyncFrom(accountStatus) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKDatabase+CKRecordZone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKDatabase+CKRecordZone.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension ACKDatabase { 12 | /// Saves a single record zone. 13 | /// 14 | /// - Parameters: 15 | /// - recordZone: The record zone to save. 16 | /// - Note: AsyncCloudKit executes the save with a low priority. Use this method when you don’t require the save to 17 | /// happen immediately. 18 | /// - Returns: The saved [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone). 19 | /// - SeeAlso: [`save`](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449108-save) 20 | public func saveAtBackgroundPriority(recordZone: CKRecordZone) async throws -> CKRecordZone { 21 | try await asyncAtBackgroundPriorityFrom(save, with: recordZone) 22 | } 23 | 24 | /// Saves a single record zone. 25 | /// 26 | /// - Parameters: 27 | /// - recordZone: The record zone to save. 28 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 29 | /// the operation will use a default configuration. 30 | /// - Returns: The saved [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone). 31 | /// - SeeAlso: [`CKModifyRecordZonesOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifyrecordzonesoperation) 32 | public func save( 33 | recordZone: CKRecordZone, 34 | withConfiguration configuration: CKOperation.Configuration? = nil 35 | ) async throws -> CKRecordZone { 36 | try await save( 37 | recordZones: [recordZone], 38 | withConfiguration: configuration 39 | ).single() 40 | } 41 | 42 | /// Saves multiple record zones. 43 | /// 44 | /// - Parameters: 45 | /// - recordZones: The record zones to save. 46 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 47 | /// the operation will use a default configuration. 48 | /// - Returns: An ``ACKSequence`` that emits the saved 49 | /// [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone)s. 50 | /// - SeeAlso: [`CKModifyRecordZonesOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifyrecordzonesoperation) 51 | public func save( 52 | recordZones: [CKRecordZone], 53 | withConfiguration configuration: CKOperation.Configuration? = nil 54 | ) -> ACKSequence { 55 | modify(recordZonesToSave: recordZones, withConfiguration: configuration).compactMap { 56 | saved, _ in 57 | saved 58 | }.erase() 59 | } 60 | 61 | /// Deletes a single record zone. 62 | /// 63 | /// - Parameters: 64 | /// - recordZoneID: The ID of the record zone to delete. 65 | /// - Note: AsyncCloudKit executes the delete with a low priority. Use this method when you don’t require the delete 66 | /// to happen immediately. 67 | /// - Returns: The deleted [`CKRecordZone.ID`](https://developer.apple.com/documentation/cloudkit/ckrecordzone/id). 68 | /// - SeeAlso: [`delete`](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449118-delete) 69 | public func deleteAtBackgroundPriority(recordZoneID: CKRecordZone.ID) async throws 70 | -> CKRecordZone.ID 71 | { 72 | try await asyncAtBackgroundPriorityFrom(delete, with: recordZoneID) 73 | } 74 | 75 | /// Deletes a single record zone. 76 | /// 77 | /// - Parameters: 78 | /// - recordZoneID: The ID of the record zone to delete. 79 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 80 | /// the operation will use a default configuration. 81 | /// - Returns: The deleted [`CKRecordZone.ID`](https://developer.apple.com/documentation/cloudkit/ckrecordzone/id). 82 | /// - SeeAlso: [`CKModifyRecordZonesOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifyrecordzonesoperation) 83 | public func delete( 84 | recordZoneID: CKRecordZone.ID, 85 | withConfiguration configuration: CKOperation.Configuration? = nil 86 | ) async throws -> CKRecordZone.ID { 87 | try await delete( 88 | recordZoneIDs: [recordZoneID], 89 | withConfiguration: configuration 90 | ).single() 91 | } 92 | 93 | /// Deletes multiple record zones. 94 | /// 95 | /// - Parameters: 96 | /// - recordZoneIDs: The IDs of the record zones to delete. 97 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 98 | /// the operation will use a default configuration. 99 | /// - Returns: An ``ACKSequence`` that emits the deleted 100 | /// [`CKRecordZone.ID`](https://developer.apple.com/documentation/cloudkit/ckrecordzone/id)s. 101 | /// - SeeAlso: [`CKModifyRecordZonesOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifyrecordzonesoperation) 102 | public func delete( 103 | recordZoneIDs: [CKRecordZone.ID], 104 | withConfiguration configuration: CKOperation.Configuration? = nil 105 | ) -> ACKSequence { 106 | modify(recordZoneIDsToDelete: recordZoneIDs, withConfiguration: configuration).compactMap { 107 | _, deleted in 108 | deleted 109 | }.erase() 110 | } 111 | 112 | /// Modifies one or more record zones. 113 | /// 114 | /// - Parameters: 115 | /// - recordZonesToSave: The record zones to save. 116 | /// - recordZonesToDelete: The IDs of the record zones to delete. 117 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 118 | /// the operation will use a default configuration. 119 | /// - Returns: An ``ACKSequence`` that emits the saved 120 | /// [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone)s and the deleted 121 | /// [`CKRecordZone.ID`](https://developer.apple.com/documentation/cloudkit/ckrecordzone/id)s. 122 | /// - SeeAlso: [`CKModifyRecordZonesOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifyrecordzonesoperation) 123 | public func modify( 124 | recordZonesToSave: [CKRecordZone]? = nil, 125 | recordZoneIDsToDelete: [CKRecordZone.ID]? = nil, 126 | withConfiguration configuration: CKOperation.Configuration? = nil 127 | ) -> ACKSequence<(CKRecordZone?, CKRecordZone.ID?)> { 128 | let operation = operationFactory.createModifyRecordZonesOperation( 129 | recordZonesToSave: recordZonesToSave, 130 | recordZoneIDsToDelete: recordZoneIDsToDelete 131 | ) 132 | return asyncFromModify(operation, configuration) { completion in 133 | operation.modifyRecordZonesCompletionBlock = completion 134 | } 135 | } 136 | 137 | /// Fetches the record zone with the specified ID. 138 | /// 139 | /// - Parameters: 140 | /// - recordZoneID: The ID of the record zone to fetch. 141 | /// - Note: AsyncCloudKit executes the fetch with a low priority. Use this method when you don’t require the record 142 | /// zone immediately. 143 | /// - Returns: The [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone). 144 | /// - SeeAlso: [fetch](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449104-fetch) 145 | public func fetchAtBackgroundPriority(withRecordZoneID recordZoneID: CKRecordZone.ID) async throws 146 | -> CKRecordZone 147 | { 148 | try await asyncAtBackgroundPriorityFrom(fetch, with: recordZoneID) 149 | } 150 | 151 | /// Fetches the record zone with the specified ID. 152 | /// 153 | /// - Parameters: 154 | /// - recordZoneID: The ID of the record zone to fetch. 155 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 156 | /// the operation will use a default configuration. 157 | /// - Returns: The [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone). 158 | /// - SeeAlso: [CKFetchRecordZonesOperation](https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonesoperation) 159 | public func fetch( 160 | recordZoneID: CKRecordZone.ID, 161 | withConfiguration configuration: CKOperation.Configuration? = nil 162 | ) async throws -> CKRecordZone { 163 | try await fetch( 164 | recordZoneIDs: [recordZoneID], 165 | withConfiguration: configuration 166 | ).single() 167 | } 168 | 169 | /// Fetches multiple record zones. 170 | /// 171 | /// - Parameters: 172 | /// - recordZoneIDs: The IDs of the record zones to fetch. 173 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 174 | /// the operation will use a default configuration. 175 | /// - Returns: An ``ACKSequence`` that emits the 176 | /// [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone)s. 177 | /// - SeeAlso: [CKFetchRecordZonesOperation](https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonesoperation) 178 | public func fetch( 179 | recordZoneIDs: [CKRecordZone.ID], 180 | withConfiguration configuration: CKOperation.Configuration? = nil 181 | ) -> ACKSequence { 182 | let operation = operationFactory.createFetchRecordZonesOperation(recordZoneIDs: recordZoneIDs) 183 | return asyncFromFetch(operation, configuration) { completion in 184 | operation.fetchRecordZonesCompletionBlock = completion 185 | } 186 | } 187 | 188 | /// Fetches the database's record zones. 189 | /// 190 | /// - Note: AsyncCloudKit executes the fetch with a low priority. Use this method when you don’t require the record 191 | /// zones immediately. 192 | /// - Returns: An ``ACKSequence`` that emits the 193 | /// [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone)s. 194 | /// - SeeAlso: [fetchAllRecordZones](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449112-fetchallrecordzones) 195 | public func fetchAllRecordZonesAtBackgroundPriority() -> ACKSequence { 196 | asyncFromFetchAll(fetchAllRecordZones) 197 | } 198 | 199 | /// Fetches the database's record zones. 200 | /// 201 | /// - Parameters: 202 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 203 | /// the operation will use a default configuration. 204 | /// - Returns: An ``ACKSequence`` that emits the 205 | /// [`CKRecordZone`](https://developer.apple.com/documentation/cloudkit/ckrecordzone)s. 206 | /// - SeeAlso: 207 | /// [fetchAllRecordZonesOperation] 208 | /// (https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonesoperation/1514890-fetchallrecordzonesoperation) 209 | public func fetchAllRecordZones( 210 | withConfiguration configuration: CKOperation.Configuration? = nil 211 | ) -> ACKSequence { 212 | let operation = operationFactory.createFetchAllRecordZonesOperation() 213 | return asyncFromFetch(operation, configuration) { completion in 214 | operation.fetchRecordZonesCompletionBlock = completion 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKDatabase+CKSubscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKDatabase+CKSubscription.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension ACKDatabase { 12 | /// Saves a single subscription. 13 | /// 14 | /// - Parameters: 15 | /// - subscription: The subscription to save. 16 | /// - Note: AsyncCloudKit executes the save with a low priority. Use this method when you don’t require the save to 17 | /// happen immediately. 18 | /// - Returns: The saved [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription). 19 | /// - SeeAlso: [`save`](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449102-save) 20 | public func saveAtBackgroundPriority(subscription: CKSubscription) async throws -> CKSubscription 21 | { 22 | try await asyncAtBackgroundPriorityFrom(save, with: subscription) 23 | } 24 | 25 | /// Saves a single subscription. 26 | /// 27 | /// - Parameters: 28 | /// - subscription: The subscription to save. 29 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 30 | /// the operation will use a default configuration. 31 | /// - Returns: The saved [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription). 32 | /// - SeeAlso: [`CKModifySubscriptionsOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifysubscriptionsoperation) 33 | public func save( 34 | subscription: CKSubscription, 35 | withConfiguration configuration: CKOperation.Configuration? = nil 36 | ) async throws -> CKSubscription { 37 | try await save( 38 | subscriptions: [subscription], 39 | withConfiguration: configuration 40 | ).single() 41 | } 42 | 43 | /// Saves multiple subscriptions. 44 | /// 45 | /// - Parameters: 46 | /// - subscriptions: The subscriptions to save. 47 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 48 | /// the operation will use a default configuration. 49 | /// - Returns: An ``ACKSequence`` that emits the saved 50 | /// [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription)s. 51 | /// - SeeAlso: [`CKModifySubscriptionsOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifysubscriptionsoperation) 52 | public func save( 53 | subscriptions: [CKSubscription], 54 | withConfiguration configuration: CKOperation.Configuration? = nil 55 | ) -> ACKSequence { 56 | modify(subscriptionsToSave: subscriptions, withConfiguration: configuration).compactMap { 57 | saved, _ in 58 | saved 59 | }.erase() 60 | } 61 | 62 | /// Deletes a single subscription. 63 | /// 64 | /// - Parameters: 65 | /// - subscriptionID: The ID of the subscription to delete. 66 | /// - Note: AsyncCloudKit executes the delete with a low priority. Use this method when you don’t require the delete 67 | /// to happen immediately. 68 | /// - Returns: The deleted [`CKSubscription.ID`](https://developer.apple.com/documentation/cloudkit/cksubscription/id). 69 | /// - SeeAlso: [`delete`](https://developer.apple.com/documentation/cloudkit/ckdatabase/3003590-delete) 70 | public func deleteAtBackgroundPriority(subscriptionID: CKSubscription.ID) async throws 71 | -> CKSubscription.ID 72 | { 73 | try await asyncAtBackgroundPriorityFrom(delete, with: subscriptionID) 74 | } 75 | 76 | /// Deletes a single subscription. 77 | /// 78 | /// - Parameters: 79 | /// - subscriptionID: The ID of the subscription to delete. 80 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 81 | /// the operation will use a default configuration. 82 | /// - Returns: The deleted [`CKSubscription.ID`](https://developer.apple.com/documentation/cloudkit/cksubscription/id). 83 | /// - SeeAlso: [`CKModifySubscriptionsOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifysubscriptionsoperation) 84 | public func delete( 85 | subscriptionID: CKSubscription.ID, 86 | withConfiguration configuration: CKOperation.Configuration? = nil 87 | ) async throws -> CKSubscription.ID { 88 | try await delete( 89 | subscriptionIDs: [subscriptionID], 90 | withConfiguration: configuration 91 | ).single() 92 | } 93 | 94 | /// Deletes multiple subscriptions. 95 | /// 96 | /// - Parameters: 97 | /// - subscriptionIDs: The IDs of the subscriptions to delete. 98 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 99 | /// the operation will use a default configuration. 100 | /// - Returns: An ``ACKSequence`` that emits the deleted 101 | /// [`CKSubscription.ID`](https://developer.apple.com/documentation/cloudkit/cksubscription/id)s. 102 | /// - SeeAlso: [`CKModifySubscriptionsOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifysubscriptionsoperation) 103 | public func delete( 104 | subscriptionIDs: [CKSubscription.ID], 105 | withConfiguration configuration: CKOperation.Configuration? = nil 106 | ) -> ACKSequence { 107 | modify(subscriptionIDsToDelete: subscriptionIDs, withConfiguration: configuration).compactMap { 108 | _, deleted in 109 | deleted 110 | }.erase() 111 | } 112 | 113 | /// Modifies one or more subscriptions. 114 | /// 115 | /// - Parameters: 116 | /// - subscriptionsToSave: The subscriptions to save. 117 | /// - subscriptionsToDelete: The IDs of the subscriptions to delete. 118 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 119 | /// the operation will use a default configuration. 120 | /// - Returns: An ``ACKSequence`` that emits the saved 121 | /// [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription)s and the deleted 122 | /// [`CKSubscription.ID`](https://developer.apple.com/documentation/cloudkit/cksubscription/id)s. 123 | /// - SeeAlso: [`CKModifySubscriptionsOperation`](https://developer.apple.com/documentation/cloudkit/ckmodifysubscriptionsoperation) 124 | public func modify( 125 | subscriptionsToSave: [CKSubscription]? = nil, 126 | subscriptionIDsToDelete: [CKSubscription.ID]? = nil, 127 | withConfiguration configuration: CKOperation.Configuration? = nil 128 | ) -> ACKSequence<(CKSubscription?, CKSubscription.ID?)> { 129 | let operation = operationFactory.createModifySubscriptionsOperation( 130 | subscriptionsToSave: subscriptionsToSave, 131 | subscriptionIDsToDelete: subscriptionIDsToDelete 132 | ) 133 | return asyncFromModify(operation, configuration) { completion in 134 | operation.modifySubscriptionsCompletionBlock = completion 135 | } 136 | } 137 | 138 | /// Fetches the subscription with the specified ID. 139 | /// 140 | /// - Parameters: 141 | /// - subscriptionID: The ID of the subscription to fetch. 142 | /// - Note: AsyncCloudKit executes the fetch with a low priority. Use this method when you don’t require the 143 | /// subscription immediately. 144 | /// - Returns: The [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription). 145 | /// - SeeAlso: [fetch](https://developer.apple.com/documentation/cloudkit/ckdatabase/3003591-fetch) 146 | public func fetchAtBackgroundPriority(withSubscriptionID subscriptionID: CKSubscription.ID) 147 | async throws -> CKSubscription 148 | { 149 | try await asyncAtBackgroundPriorityFrom(fetch, with: subscriptionID) 150 | } 151 | 152 | /// Fetches the subscription with the specified ID. 153 | /// 154 | /// - Parameters: 155 | /// - subscriptionID: The ID of the subscription to fetch. 156 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 157 | /// the operation will use a default configuration. 158 | /// - Returns: The [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription). 159 | /// - SeeAlso: [CKFetchSubscriptionsOperation](https://developer.apple.com/documentation/cloudkit/ckfetchsubscriptionsoperation) 160 | public func fetch( 161 | subscriptionID: CKSubscription.ID, 162 | withConfiguration configuration: CKOperation.Configuration? = nil 163 | ) async throws -> CKSubscription { 164 | try await fetch( 165 | subscriptionIDs: [subscriptionID], 166 | withConfiguration: configuration 167 | ).single() 168 | } 169 | 170 | /// Fetches multiple subscriptions. 171 | /// 172 | /// - Parameters: 173 | /// - subscriptionIDs: The IDs of the subscriptions to fetch. 174 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 175 | /// the operation will use a default configuration. 176 | /// - Returns: An ``ACKSequence`` that emits the 177 | /// [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription)s. 178 | /// - SeeAlso: [CKFetchSubscriptionsOperation](https://developer.apple.com/documentation/cloudkit/ckfetchsubscriptionsoperation) 179 | public func fetch( 180 | subscriptionIDs: [CKSubscription.ID], 181 | withConfiguration configuration: CKOperation.Configuration? = nil 182 | ) -> ACKSequence { 183 | let operation = operationFactory.createFetchSubscriptionsOperation( 184 | subscriptionIDs: subscriptionIDs) 185 | return asyncFromFetch(operation, configuration) { completion in 186 | operation.fetchSubscriptionCompletionBlock = completion 187 | } 188 | } 189 | 190 | /// Fetches the database's subscriptions. 191 | /// 192 | /// - Note: AsyncCloudKit executes the fetch with a low priority. Use this method when you don’t require the 193 | /// subscriptions immediately. 194 | /// - Returns: An ``ACKSequence`` that emits the 195 | /// [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription)s. 196 | /// - SeeAlso: [fetchAllSubscriptions](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449110-fetchallsubscriptions) 197 | public func fetchAllSubscriptionsAtBackgroundPriority() -> ACKSequence { 198 | asyncFromFetchAll(fetchAllSubscriptions) 199 | } 200 | 201 | /// Fetches the database's subscriptions. 202 | /// 203 | /// - Parameters: 204 | /// - configuration: The configuration to use for the underlying operation. If you don't specify a configuration, 205 | /// the operation will use a default configuration. 206 | /// - Returns: An ``ACKSequence`` that emits the 207 | /// [`CKSubscription`](https://developer.apple.com/documentation/cloudkit/cksubscription)s. 208 | /// - SeeAlso: 209 | /// [fetchAllSubscriptionsOperation] 210 | /// (https://developer.apple.com/documentation/cloudkit/ckfetchsubscriptionsoperation/1515282-fetchallsubscriptionsoperation) 211 | public func fetchAllSubscriptions( 212 | withConfiguration configuration: CKOperation.Configuration? = nil 213 | ) -> ACKSequence { 214 | let operation = operationFactory.createFetchAllSubscriptionsOperation() 215 | return asyncFromFetch(operation, configuration) { completion in 216 | operation.fetchSubscriptionCompletionBlock = completion 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKDatabase.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | /// An extension that declares [`CKDatabase`](https://developer.apple.com/documentation/cloudkit/ckdatabase) 12 | /// conforms to the ``ACKDatabase`` protocol provided by AsyncCloudKit. 13 | /// 14 | /// - SeeAlso: [`CloudKit`](https://developer.apple.com/documentation/cloudkit) 15 | extension CKDatabase: ACKDatabase { 16 | } 17 | 18 | /// A protocol used to abstract a [`CKDatabase`](https://developer.apple.com/documentation/cloudkit/ckdatabase). 19 | /// 20 | /// Invoke the async extension methods on your 21 | /// [`CKDatabase`](https://developer.apple.com/documentation/cloudkit/ckdatabase) 22 | /// instances to process records, record zones, and subscriptions asynchronously. 23 | /// 24 | /// - SeeAlso: [`CloudKit`](https://developer.apple.com/documentation/cloudkit) 25 | public protocol ACKDatabase { 26 | /// Implemented by `CKDatabase`. 27 | /// 28 | /// - SeeAlso: [`delete`](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449122-delete) 29 | func delete( 30 | withRecordID recordID: CKRecord.ID, completionHandler: @escaping (CKRecord.ID?, Error?) -> Void) 31 | 32 | /// Implemented by `CKDatabase`. 33 | /// 34 | /// - SeeAlso: [`delete`](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449118-delete) 35 | func delete( 36 | withRecordZoneID zoneID: CKRecordZone.ID, 37 | completionHandler: @escaping (CKRecordZone.ID?, Error?) -> Void) 38 | 39 | /// Implemented by `CKDatabase`. 40 | /// 41 | /// - SeeAlso: [`delete`](https://developer.apple.com/documentation/cloudkit/ckdatabase/3003590-delete) 42 | func delete( 43 | withSubscriptionID subscriptionID: CKSubscription.ID, 44 | completionHandler: @escaping (String?, Error?) -> Void) 45 | 46 | /// Implemented by `CKDatabase`. 47 | /// 48 | /// - SeeAlso: [fetch](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449126-fetch) 49 | func fetch( 50 | withRecordID recordID: CKRecord.ID, completionHandler: @escaping (CKRecord?, Error?) -> Void) 51 | 52 | /// Implemented by `CKDatabase`. 53 | /// 54 | /// - SeeAlso: [fetch](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449104-fetch) 55 | func fetch( 56 | withRecordZoneID zoneID: CKRecordZone.ID, 57 | completionHandler: @escaping (CKRecordZone?, Error?) -> Void) 58 | 59 | /// Implemented by `CKDatabase`. 60 | /// 61 | /// - SeeAlso: [fetch](https://developer.apple.com/documentation/cloudkit/ckdatabase/3003591-fetch) 62 | func fetch( 63 | withSubscriptionID subscriptionID: CKSubscription.ID, 64 | completionHandler: @escaping (CKSubscription?, Error?) -> Void) 65 | 66 | /// Implemented by `CKDatabase`. 67 | /// 68 | /// - SeeAlso: [fetchAllRecordZones](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449112-fetchallrecordzones) 69 | func fetchAllRecordZones(completionHandler: @escaping ([CKRecordZone]?, Error?) -> Void) 70 | 71 | /// Implemented by `CKDatabase`. 72 | /// 73 | /// - SeeAlso: [fetchAllSubscriptions](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449110-fetchallsubscriptions) 74 | func fetchAllSubscriptions(completionHandler: @escaping ([CKSubscription]?, Error?) -> Void) 75 | 76 | /// Implemented by `CKDatabase`. 77 | /// 78 | /// - SeeAlso: [fetchAllSubscriptions](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449127-perform) 79 | func perform( 80 | _ query: CKQuery, 81 | inZoneWith zoneID: CKRecordZone.ID?, 82 | completionHandler: @escaping ([CKRecord]?, Error?) -> Void) 83 | 84 | /// Implemented by `CKDatabase`. 85 | /// 86 | /// - SeeAlso: [`save`](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449114-save) 87 | func save(_ record: CKRecord, completionHandler: @escaping (CKRecord?, Error?) -> Void) 88 | 89 | /// Implemented by `CKDatabase`. 90 | /// 91 | /// - SeeAlso: [`save`](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449108-save) 92 | func save(_ zone: CKRecordZone, completionHandler: @escaping (CKRecordZone?, Error?) -> Void) 93 | 94 | /// Implemented by `CKDatabase`. 95 | /// 96 | /// - SeeAlso: [`save`](https://developer.apple.com/documentation/cloudkit/ckdatabase/1449102-save) 97 | func save( 98 | _ subscription: CKSubscription, completionHandler: @escaping (CKSubscription?, Error?) -> Void) 99 | } 100 | 101 | extension ACKDatabase { 102 | func add(_ operation: ACKDatabaseOperation) { 103 | guard let database = self as? CKDatabase, let dbOperation = operation as? CKDatabaseOperation 104 | else { 105 | // TODO: Use an OperationQueue. 106 | operation.start() 107 | return 108 | } 109 | 110 | database.add(dbOperation) 111 | } 112 | 113 | func asyncAtBackgroundPriorityFrom( 114 | _ method: @escaping (Input, @escaping (Output?, Error?) -> Void) -> Void, 115 | with input: Input 116 | ) async throws -> Output { 117 | try await withCheckedThrowingContinuation { continuation in 118 | method(input) { output, error in 119 | guard let output = output, error == nil else { 120 | continuation.resume(throwing: error!) 121 | return 122 | } 123 | 124 | continuation.resume(returning: output) 125 | } 126 | } 127 | } 128 | 129 | func asyncFromFetch( 130 | _ operation: ACKDatabaseOperation, 131 | _ configuration: CKOperation.Configuration? = nil, 132 | _ setCompletion: (@escaping ([Ignored: Output]?, Error?) -> Void) -> Void 133 | ) -> ACKSequence { 134 | if configuration != nil { 135 | operation.configuration = configuration 136 | } 137 | return AsyncThrowingStream { continuation in 138 | continuation.onTermination = { @Sendable termination in 139 | operation.cancel() 140 | } 141 | setCompletion { outputs, error in 142 | guard let outputs = outputs, error == nil else { 143 | continuation.finish(throwing: error!) 144 | return 145 | } 146 | 147 | for output in outputs.values { 148 | continuation.yield(output) 149 | } 150 | 151 | continuation.finish() 152 | } 153 | 154 | self.add(operation) 155 | }.erase() 156 | } 157 | 158 | func asyncFromFetchAll( 159 | _ method: @escaping (@escaping ([Output]?, Error?) -> Void) -> Void 160 | ) -> ACKSequence { 161 | AsyncThrowingStream { continuation in 162 | method { outputs, error in 163 | guard let outputs = outputs, error == nil else { 164 | continuation.finish(throwing: error!) 165 | return 166 | } 167 | 168 | for output in outputs { 169 | continuation.yield(output) 170 | } 171 | 172 | continuation.finish() 173 | } 174 | }.erase() 175 | } 176 | 177 | func asyncFromModify( 178 | _ operation: ACKDatabaseOperation, 179 | _ configuration: CKOperation.Configuration? = nil, 180 | _ setCompletion: (@escaping ([Output]?, [OutputID]?, Error?) -> Void) -> Void 181 | ) -> ACKSequence<(Output?, OutputID?)> { 182 | if configuration != nil { 183 | operation.configuration = configuration 184 | } 185 | return AsyncThrowingStream { continuation in 186 | continuation.onTermination = { @Sendable termination in 187 | operation.cancel() 188 | } 189 | setCompletion { saved, deleted, error in 190 | guard error == nil else { 191 | continuation.finish(throwing: error!) 192 | return 193 | } 194 | 195 | if let saved = saved { 196 | for output in saved { 197 | continuation.yield((output, nil)) 198 | } 199 | } 200 | 201 | if let deleted = deleted { 202 | for outputID in deleted { 203 | continuation.yield((nil, outputID)) 204 | } 205 | } 206 | 207 | continuation.finish() 208 | } 209 | 210 | self.add(operation) 211 | }.erase() 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKDatabaseOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKDatabaseOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKDatabaseOperation: ACKDatabaseOperation { 12 | } 13 | 14 | protocol ACKDatabaseOperation: ACKOperation { 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKFetchRecordZonesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKFetchRecordZonesOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKFetchRecordZonesOperation: ACKFetchRecordZonesOperation { 12 | } 13 | 14 | protocol ACKFetchRecordZonesOperation: ACKDatabaseOperation { 15 | var fetchRecordZonesCompletionBlock: (([CKRecordZone.ID: CKRecordZone]?, Error?) -> Void)? { 16 | get set 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKFetchRecordsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKFetchRecordsOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKFetchRecordsOperation: ACKFetchRecordsOperation { 12 | } 13 | 14 | protocol ACKFetchRecordsOperation: ACKDatabaseOperation { 15 | var desiredKeys: [CKRecord.FieldKey]? { get set } 16 | var perRecordProgressBlock: ((CKRecord.ID, Double) -> Void)? { get set } 17 | var perRecordCompletionBlock: ((CKRecord?, CKRecord.ID?, Error?) -> Void)? { get set } 18 | var fetchRecordsCompletionBlock: (([CKRecord.ID: CKRecord]?, Error?) -> Void)? { get set } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKFetchSubscriptionsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKFetchSubscriptionsOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKFetchSubscriptionsOperation: ACKFetchSubscriptionsOperation { 12 | } 13 | 14 | protocol ACKFetchSubscriptionsOperation: ACKDatabaseOperation { 15 | var fetchSubscriptionCompletionBlock: (([CKSubscription.ID: CKSubscription]?, Error?) -> Void)? { 16 | get set 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKModifyRecordZonesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKModifyRecordZonesOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKModifyRecordZonesOperation: ACKModifyRecordZonesOperation { 12 | } 13 | 14 | protocol ACKModifyRecordZonesOperation: ACKDatabaseOperation { 15 | var modifyRecordZonesCompletionBlock: (([CKRecordZone]?, [CKRecordZone.ID]?, Error?) -> Void)? { 16 | get set 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKModifyRecordsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKModifyRecordsOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKModifyRecordsOperation: ACKModifyRecordsOperation { 12 | } 13 | 14 | protocol ACKModifyRecordsOperation: ACKDatabaseOperation { 15 | var isAtomic: Bool { get set } 16 | var savePolicy: CKModifyRecordsOperation.RecordSavePolicy { get set } 17 | var clientChangeTokenData: Data? { get set } 18 | var perRecordProgressBlock: ((CKRecord, Double) -> Void)? { get set } 19 | var perRecordCompletionBlock: ((CKRecord, Error?) -> Void)? { get set } 20 | var modifyRecordsCompletionBlock: (([CKRecord]?, [CKRecord.ID]?, Error?) -> Void)? { get set } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKModifySubscriptionsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKModifySubscriptionsOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKModifySubscriptionsOperation: ACKModifySubscriptionsOperation { 12 | } 13 | 14 | protocol ACKModifySubscriptionsOperation: ACKDatabaseOperation { 15 | var modifySubscriptionsCompletionBlock: 16 | (([CKSubscription]?, [CKSubscription.ID]?, Error?) -> Void)? 17 | { get set } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKOperation: ACKOperation { 12 | } 13 | 14 | protocol ACKOperation: Operation { 15 | var configuration: CKOperation.Configuration! { get set } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/AsyncCloudKit/Protocols/ACKQueryOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ACKQueryOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/17/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | extension CKQueryOperation: ACKQueryOperation { 12 | } 13 | 14 | protocol ACKQueryOperation: ACKDatabaseOperation { 15 | var query: CKQuery? { get set } 16 | var cursor: CKQueryOperation.Cursor? { get set } 17 | var desiredKeys: [CKRecord.FieldKey]? { get set } 18 | var zoneID: CKRecordZone.ID? { get set } 19 | var resultsLimit: Int { get set } 20 | var recordFetchedBlock: ((CKRecord) -> Void)? { get set } 21 | var queryCompletionBlock: ((CKQueryOperation.Cursor?, Error?) -> Void)? { get set } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/AsyncCloudKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncCloudKitTests.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | class AsyncCloudKitTests: XCTestCase { 15 | #if SWIFT_PACKAGE 16 | // Unit tests with mocks 17 | let container: ACKContainer = MockContainer() 18 | let database: ACKDatabase = MockDatabase() 19 | override func setUp() { 20 | AsyncCloudKit.operationFactory = MockOperationFactory(database as! MockDatabase) 21 | } 22 | #else 23 | // Integration tests with CloudKit 24 | let container: ACKContainer 25 | let database: ACKDatabase 26 | override init() { 27 | let container = CKContainer( 28 | identifier: "iCloud.dev.hiddenplace.AsyncCloudKit.Tests") 29 | self.container = container 30 | self.database = container.privateCloudDatabase 31 | super.init() 32 | } 33 | #endif 34 | } 35 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/CKContainerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKContainerTests.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | final class CKContainerTests: AsyncCloudKitTests { 15 | func testAccountStatusIsAvailable() async throws { 16 | let status = try await container.accountStatus() 17 | XCTAssertEqual(status, .available) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/ErrorInjectionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorInjectionTests.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | class ErrorInjectionTests: AsyncCloudKitTests { 15 | func testAccountStatusPropagatesErrors() async throws { 16 | try await verifyErrorPropagation { container, _ in 17 | _ = try await container.accountStatus() 18 | } 19 | } 20 | 21 | func testSaveFetchAndDeleteRecords() async throws { 22 | try await validateSaveFetchAndDelete( 23 | { CKRecord(recordType: "Test") }, 24 | { $0.recordID }, 25 | { database in { record in try await database.save(record: record) } }, 26 | { database in { recordID in try await database.fetch(recordID: recordID) } }, 27 | { database in { recordID in try await database.delete(recordID: recordID) } } 28 | ) 29 | } 30 | 31 | func testSaveFetchAndDeleteRecordZones() async throws { 32 | try await validateSaveFetchAndDelete( 33 | { CKRecordZone(zoneName: "Test") }, 34 | { $0.zoneID }, 35 | { database in { zone in try await database.save(recordZone: zone) } }, 36 | { database in { zoneID in try await database.fetch(recordZoneID: zoneID) } }, 37 | { database in { zoneID in try await database.delete(recordZoneID: zoneID) } } 38 | ) 39 | } 40 | 41 | func testSaveFetchAndDeleteSubscriptions() async throws { 42 | try await validateSaveFetchAndDelete( 43 | { CKDatabaseSubscription(subscriptionID: "Test") }, 44 | { $0.subscriptionID }, 45 | { database in { subscription in try await database.save(subscription: subscription) } }, 46 | { database in { subscriptionID in try await database.fetch(subscriptionID: subscriptionID) } 47 | }, 48 | { database in { subscriptionID in try await database.delete(subscriptionID: subscriptionID) } 49 | } 50 | ) 51 | } 52 | 53 | func testSaveFetchAndDeleteRecordsAtBackgroundPriority() async throws { 54 | try await validateSaveFetchAndDelete( 55 | { CKRecord(recordType: "Test") }, 56 | { $0.recordID }, 57 | { $0.saveAtBackgroundPriority }, 58 | { $0.fetchAtBackgroundPriority }, 59 | { $0.deleteAtBackgroundPriority } 60 | ) 61 | } 62 | 63 | func testSaveFetchAndDeleteRecordZonesAtBackgroundPriority() async throws { 64 | try await validateSaveFetchAndDelete( 65 | { CKRecordZone(zoneName: "Test") }, 66 | { $0.zoneID }, 67 | { $0.saveAtBackgroundPriority }, 68 | { $0.fetchAtBackgroundPriority }, 69 | { $0.deleteAtBackgroundPriority } 70 | ) 71 | } 72 | 73 | func testSaveFetchAndDeleteSubscriptionsAtBackgroundPriority() async throws { 74 | try await validateSaveFetchAndDelete( 75 | { CKDatabaseSubscription(subscriptionID: "Test") }, 76 | { $0.subscriptionID }, 77 | { $0.saveAtBackgroundPriority }, 78 | { $0.fetchAtBackgroundPriority }, 79 | { $0.deleteAtBackgroundPriority } 80 | ) 81 | } 82 | 83 | private func validateSaveFetchAndDelete( 84 | _ create: () -> T, 85 | _ id: (T) -> ID, 86 | _ save: (ACKDatabase) -> ((T) async throws -> T), 87 | _ fetch: (ACKDatabase) -> ((ID) async throws -> T), 88 | _ delete: (ACKDatabase) -> ((ID) async throws -> ID) 89 | ) async throws { 90 | try await verifyErrorPropagation { _, database in 91 | let item = create() 92 | let itemID = id(item) 93 | _ = try await save(database)(item) 94 | _ = try await fetch(database)(itemID) 95 | _ = try await delete(database)(itemID) 96 | } 97 | } 98 | 99 | func testFetchAllRecordZones() async throws { 100 | try await validateFetchAll( 101 | (1...3).map { CKRecordZone(zoneName: "\($0)") }, 102 | { $0.save }, 103 | { database in { database.fetchAllRecordZones() } } 104 | ) 105 | } 106 | 107 | func testFetchAllRecordZonesAtBackgroundPriority() async throws { 108 | try await validateFetchAll( 109 | (1...3).map { CKRecordZone(zoneName: "\($0)") }, 110 | { $0.save }, 111 | { $0.fetchAllRecordZonesAtBackgroundPriority } 112 | ) 113 | } 114 | 115 | func testFetchCurrentUserRecord() async throws { 116 | try await verifyErrorPropagation( 117 | prepare: { _, database in 118 | let userRecord = CKRecord( 119 | recordType: "Test", recordID: MockOperationFactory.currentUserRecordID) 120 | _ = try await database.save(record: userRecord) 121 | }, 122 | simulation: { _, database in 123 | _ = try await database.fetchCurrentUserRecord() 124 | } 125 | ) 126 | } 127 | 128 | func testFetchAllSubscriptions() async throws { 129 | try await validateFetchAll( 130 | (1...3).map { CKDatabaseSubscription(subscriptionID: "\($0)") }, 131 | { $0.save }, 132 | { database in { database.fetchAllSubscriptions() } } 133 | ) 134 | } 135 | 136 | func testFetchAllSubscriptionsAtBackgroundPriority() async throws { 137 | try await validateFetchAll( 138 | (1...3).map { CKDatabaseSubscription(subscriptionID: "\($0)") }, 139 | { $0.save }, 140 | { $0.fetchAllSubscriptionsAtBackgroundPriority } 141 | ) 142 | } 143 | 144 | private func validateFetchAll( 145 | _ items: [T], 146 | _ save: (ACKDatabase) -> (([T], CKOperation.Configuration?) -> ACKSequence), 147 | _ fetch: (ACKDatabase) -> (() -> ACKSequence) 148 | ) async throws where T: Hashable { 149 | try await verifyErrorPropagation( 150 | prepare: { _, database in 151 | _ = try await save(database)(items, nil).collect() 152 | }, 153 | simulation: { _, database in 154 | _ = try await fetch(database)().collect() 155 | } 156 | ) 157 | } 158 | 159 | func testQueryPropagatesErrors() async throws { 160 | try await verifyErrorPropagation( 161 | prepare: { _, database in 162 | let record = CKRecord(recordType: "Test") 163 | _ = try await database.save(record: record) 164 | }, 165 | simulation: { _, database in 166 | _ = try await database.performQuery(ofType: "Test").collect() 167 | } 168 | ) 169 | } 170 | 171 | private func verifyErrorPropagation( 172 | prepare: ((ACKContainer, ACKDatabase) async throws -> Void) = { _, _ in }, 173 | simulation: ((ACKContainer, ACKDatabase) async throws -> Void) 174 | ) async rethrows { 175 | let space = DecisionSpace() 176 | repeat { 177 | let container = MockContainer() 178 | let database = MockDatabase() 179 | let factory = MockOperationFactory(database) 180 | AsyncCloudKit.operationFactory = factory 181 | 182 | // Prepare without error injection. 183 | // An error thrown here indicates a test defect. 184 | try await prepare(container, database) 185 | 186 | container.space = space 187 | database.space = space 188 | factory.space = space 189 | 190 | // Simulate the test scenario with error injection. 191 | // An error thrown here should have been injected or else the test has failed. 192 | do { 193 | try await simulation(container, database) 194 | guard !space.hasDecidedAffirmatively() else { 195 | XCTFail("Simulation was expected to fail with injected error.") 196 | break 197 | } 198 | } catch { 199 | guard let mockError = error as? MockError, 200 | case MockError.simulated = mockError, 201 | space.hasDecidedAffirmatively() 202 | else { 203 | XCTFail("Simulation failed with unexpected error: \(error.localizedDescription)") 204 | break 205 | } 206 | } 207 | 208 | if !space.hasDecided() { 209 | XCTFail("Simulation did not make any decisions.") 210 | break 211 | } 212 | } while space.next() 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockContainer.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | @testable import AsyncCloudKit 12 | 13 | public class MockContainer: ACKContainer { 14 | var space: DecisionSpace? 15 | 16 | init(_ space: DecisionSpace? = nil) { 17 | self.space = space 18 | } 19 | 20 | public func accountStatus(completionHandler: @escaping (CKAccountStatus, Error?) -> Void) { 21 | if let space = space, space.decide() { 22 | completionHandler(.couldNotDetermine, MockError.simulated) 23 | return 24 | } 25 | 26 | completionHandler(.available, nil) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockDatabase.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | @testable import AsyncCloudKit 12 | 13 | public class MockDatabase: ACKDatabase { 14 | var space: DecisionSpace? 15 | let queue: DispatchQueue 16 | 17 | init(_ space: DecisionSpace? = nil) { 18 | self.space = space 19 | self.queue = DispatchQueue(label: String(describing: type(of: self))) 20 | } 21 | 22 | var records = [CKRecord.ID: CKRecord]() 23 | var recordZones = [CKRecordZone.ID: CKRecordZone]() 24 | var subscriptions = [CKSubscription.ID: CKSubscription]() 25 | 26 | public func delete( 27 | withRecordID recordID: CKRecord.ID, completionHandler: @escaping (CKRecord.ID?, Error?) -> Void 28 | ) { 29 | queue.async { 30 | if let space = self.space, space.decide() { 31 | completionHandler(nil, MockError.simulated) 32 | return 33 | } 34 | 35 | let removed = self.records.removeValue(forKey: recordID) 36 | guard let removedID = removed?.recordID else { 37 | completionHandler(nil, MockError.doesNotExist) 38 | return 39 | } 40 | 41 | completionHandler(removedID, nil) 42 | } 43 | } 44 | 45 | public func delete( 46 | withRecordZoneID zoneID: CKRecordZone.ID, 47 | completionHandler: @escaping (CKRecordZone.ID?, Error?) -> Void 48 | ) { 49 | queue.async { 50 | if let space = self.space, space.decide() { 51 | completionHandler(nil, MockError.simulated) 52 | return 53 | } 54 | 55 | let removed = self.recordZones.removeValue(forKey: zoneID) 56 | guard let removedID = removed?.zoneID else { 57 | completionHandler(nil, MockError.doesNotExist) 58 | return 59 | } 60 | 61 | completionHandler(removedID, nil) 62 | } 63 | } 64 | 65 | public func delete( 66 | withSubscriptionID subscriptionID: CKSubscription.ID, 67 | completionHandler: @escaping (String?, Error?) -> Void 68 | ) { 69 | queue.async { 70 | if let space = self.space, space.decide() { 71 | completionHandler(nil, MockError.simulated) 72 | return 73 | } 74 | 75 | let removed = self.subscriptions.removeValue(forKey: subscriptionID) 76 | guard let removedID = removed?.subscriptionID else { 77 | completionHandler(nil, MockError.doesNotExist) 78 | return 79 | } 80 | 81 | completionHandler(removedID, nil) 82 | } 83 | } 84 | 85 | public func fetch( 86 | withRecordID recordID: CKRecord.ID, 87 | completionHandler: @escaping (CKRecord?, Error?) -> Void 88 | ) { 89 | queue.async { 90 | if let space = self.space, space.decide() { 91 | completionHandler(nil, MockError.simulated) 92 | return 93 | } 94 | 95 | guard let record = self.records[recordID] else { 96 | completionHandler(nil, MockError.doesNotExist) 97 | return 98 | } 99 | 100 | completionHandler(record, nil) 101 | } 102 | } 103 | 104 | public func fetch( 105 | withRecordZoneID zoneID: CKRecordZone.ID, 106 | completionHandler: @escaping (CKRecordZone?, Error?) -> Void 107 | ) { 108 | queue.async { 109 | if let space = self.space, space.decide() { 110 | completionHandler(nil, MockError.simulated) 111 | return 112 | } 113 | 114 | guard let recordZone = self.recordZones[zoneID] else { 115 | completionHandler(nil, MockError.doesNotExist) 116 | return 117 | } 118 | 119 | completionHandler(recordZone, nil) 120 | } 121 | } 122 | 123 | public func fetch( 124 | withSubscriptionID subscriptionID: CKSubscription.ID, 125 | completionHandler: @escaping (CKSubscription?, Error?) -> Void 126 | ) { 127 | queue.async { 128 | if let space = self.space, space.decide() { 129 | completionHandler(nil, MockError.simulated) 130 | return 131 | } 132 | 133 | guard let subscription = self.subscriptions[subscriptionID] else { 134 | completionHandler(nil, MockError.doesNotExist) 135 | return 136 | } 137 | 138 | completionHandler(subscription, nil) 139 | } 140 | } 141 | 142 | public func fetchAllRecordZones(completionHandler: @escaping ([CKRecordZone]?, Error?) -> Void) { 143 | queue.async { 144 | if let space = self.space, space.decide() { 145 | completionHandler(nil, MockError.simulated) 146 | return 147 | } 148 | 149 | completionHandler(Array(self.recordZones.values), nil) 150 | } 151 | } 152 | 153 | public func fetchAllSubscriptions( 154 | completionHandler: @escaping ([CKSubscription]?, Error?) -> Void 155 | ) { 156 | queue.async { 157 | if let space = self.space, space.decide() { 158 | completionHandler(nil, MockError.simulated) 159 | return 160 | } 161 | 162 | completionHandler(Array(self.subscriptions.values), nil) 163 | } 164 | } 165 | 166 | public func perform( 167 | _ query: CKQuery, 168 | inZoneWith _: CKRecordZone.ID?, 169 | completionHandler: @escaping ([CKRecord]?, Error?) -> Void 170 | ) { 171 | queue.async { 172 | if let space = self.space, space.decide() { 173 | completionHandler(nil, MockError.simulated) 174 | return 175 | } 176 | 177 | // Our simulated queries will return every second record of matching type, sorted by ID.recordName. 178 | // We will ignore the predicate and other CKQuery fields. 179 | var index = 0 180 | let results = self.records.sorted { 181 | $0.key.recordName.compare($1.key.recordName) == .orderedAscending 182 | }.compactMap { key, value in 183 | value.recordType == query.recordType ? value : nil 184 | }.filter { _ in 185 | index += 1 186 | return index % 2 == 0 187 | } 188 | 189 | completionHandler(results, nil) 190 | } 191 | } 192 | 193 | public func save( 194 | _ record: CKRecord, completionHandler: @escaping (CKRecord?, Error?) -> Void 195 | ) { 196 | queue.async { 197 | if let space = self.space, space.decide() { 198 | completionHandler(nil, MockError.simulated) 199 | return 200 | } 201 | 202 | self.records[record.recordID] = record 203 | completionHandler(record, nil) 204 | } 205 | } 206 | 207 | public func save( 208 | _ zone: CKRecordZone, completionHandler: @escaping (CKRecordZone?, Error?) -> Void 209 | ) { 210 | queue.async { 211 | if let space = self.space, space.decide() { 212 | completionHandler(nil, MockError.simulated) 213 | return 214 | } 215 | 216 | self.recordZones[zone.zoneID] = zone 217 | completionHandler(zone, nil) 218 | } 219 | } 220 | 221 | public func save( 222 | _ subscription: CKSubscription, completionHandler: @escaping (CKSubscription?, Error?) -> Void 223 | ) { 224 | queue.async { 225 | if let space = self.space, space.decide() { 226 | completionHandler(nil, MockError.simulated) 227 | return 228 | } 229 | 230 | self.subscriptions[subscription.subscriptionID] = subscription 231 | completionHandler(subscription, nil) 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockDatabaseOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockDatabaseOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | @testable import AsyncCloudKit 12 | 13 | public class MockDatabaseOperation: MockOperation, ACKDatabaseOperation { 14 | let mockDatabase: MockDatabase 15 | let space: DecisionSpace? 16 | 17 | init(_ database: MockDatabase, _ space: DecisionSpace?) { 18 | self.mockDatabase = database 19 | self.space = space 20 | } 21 | 22 | public var database: CKDatabase? 23 | } 24 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockError.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | enum MockError: Error { 10 | case doesNotExist 11 | case simulated 12 | } 13 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockFetchOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockFetchOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockFetchOperation: MockDatabaseOperation where ID: Hashable { 15 | let databaseItemsSelector: (MockDatabase, (inout [ID: T]) -> Void) -> Void 16 | let itemIDs: [ID]? 17 | 18 | init( 19 | _ database: MockDatabase, 20 | _ space: DecisionSpace?, 21 | _ databaseItemsSelector: @escaping (MockDatabase, (inout [ID: T]) -> Void) -> Void, 22 | _ itemIDs: [ID]? 23 | ) { 24 | self.databaseItemsSelector = databaseItemsSelector 25 | self.itemIDs = itemIDs 26 | super.init(database, space) 27 | } 28 | 29 | public var perItemProgressBlock: ((ID, Double) -> Void)? 30 | 31 | public var perItemCompletionBlock: ((T?, ID?, Error?) -> Void)? 32 | 33 | public var fetchItemsCompletionBlock: (([ID: T]?, Error?) -> Void)? 34 | 35 | public override func start() { 36 | let completion = try! XCTUnwrap(self.fetchItemsCompletionBlock) 37 | mockDatabase.queue.async { 38 | if let space = self.space, space.decide() { 39 | completion(nil, MockError.simulated) 40 | return 41 | } 42 | 43 | self.databaseItemsSelector(self.mockDatabase) { databaseItems in 44 | if let itemIDs = self.itemIDs { 45 | guard itemIDs.allSatisfy(databaseItems.keys.contains) else { 46 | completion(nil, MockError.doesNotExist) 47 | return 48 | } 49 | } 50 | 51 | var perItemError: MockError? 52 | let items = databaseItems.filter { itemID, item in 53 | if let itemIDs = self.itemIDs { 54 | guard itemIDs.contains(itemID) else { 55 | return false 56 | } 57 | } 58 | 59 | if let progress = self.perItemProgressBlock { 60 | progress(itemID, 0.7) 61 | progress(itemID, 1.0) 62 | } 63 | 64 | if let completion = self.perItemCompletionBlock { 65 | if let space = self.space, space.decide() { 66 | perItemError = MockError.simulated 67 | completion(nil, itemID, perItemError) 68 | return false 69 | } else { 70 | completion(item, itemID, nil) 71 | } 72 | } 73 | 74 | return true 75 | } 76 | 77 | guard perItemError == nil else { 78 | completion(nil, perItemError) 79 | return 80 | } 81 | 82 | completion(items, nil) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockFetchRecordZonesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockFetchRecordZonesOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockFetchRecordZonesOperation: MockFetchOperation, 15 | ACKFetchRecordZonesOperation 16 | { 17 | init( 18 | _ database: MockDatabase, 19 | _ space: DecisionSpace?, 20 | _ recordZoneIDs: [CKRecordZone.ID]? = nil 21 | ) { 22 | super.init( 23 | database, 24 | space, 25 | { database, operation in operation(&database.recordZones) }, 26 | recordZoneIDs 27 | ) 28 | super.fetchItemsCompletionBlock = { [unowned self] items, error in 29 | let completion = try! XCTUnwrap(self.fetchRecordZonesCompletionBlock) 30 | completion(items, error) 31 | } 32 | } 33 | 34 | public var fetchRecordZonesCompletionBlock: (([CKRecordZone.ID: CKRecordZone]?, Error?) -> Void)? 35 | } 36 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockFetchRecordsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockFetchRecordsOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockFetchRecordsOperation: MockFetchOperation, 15 | ACKFetchRecordsOperation 16 | { 17 | init( 18 | _ database: MockDatabase, 19 | _ space: DecisionSpace?, 20 | _ recordIDs: [CKRecord.ID] 21 | ) { 22 | super.init( 23 | database, 24 | space, 25 | { database, operation in operation(&database.records) }, 26 | recordIDs 27 | ) 28 | super.perItemProgressBlock = { [unowned self] itemID, progress in 29 | if let update = self.perRecordProgressBlock { 30 | update(itemID, progress) 31 | } 32 | } 33 | super.perItemCompletionBlock = { [unowned self] item, itemID, error in 34 | if let completion = self.perRecordCompletionBlock { 35 | completion(item, itemID, error) 36 | } 37 | } 38 | super.fetchItemsCompletionBlock = { [unowned self] items, error in 39 | let completion = try! XCTUnwrap(self.fetchRecordsCompletionBlock) 40 | completion(items, error) 41 | } 42 | } 43 | 44 | // TODO: Return only desired keys. 45 | public var desiredKeys: [CKRecord.FieldKey]? 46 | 47 | public var perRecordProgressBlock: ((CKRecord.ID, Double) -> Void)? 48 | 49 | public var perRecordCompletionBlock: ((CKRecord?, CKRecord.ID?, Error?) -> Void)? 50 | 51 | public var fetchRecordsCompletionBlock: (([CKRecord.ID: CKRecord]?, Error?) -> Void)? 52 | } 53 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockFetchSubscriptionsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockFetchSubscriptionsOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockFetchSubscriptionsOperation: MockFetchOperation, 15 | ACKFetchSubscriptionsOperation 16 | { 17 | init( 18 | _ database: MockDatabase, 19 | _ space: DecisionSpace?, 20 | _ subscriptionIDs: [CKSubscription.ID]? = nil 21 | ) { 22 | super.init( 23 | database, 24 | space, 25 | { database, operation in operation(&database.subscriptions) }, 26 | subscriptionIDs 27 | ) 28 | super.fetchItemsCompletionBlock = { [unowned self] items, error in 29 | let completion = try! XCTUnwrap(self.fetchSubscriptionCompletionBlock) 30 | completion(items, error) 31 | } 32 | } 33 | 34 | public var fetchSubscriptionCompletionBlock: 35 | (([CKSubscription.ID: CKSubscription]?, Error?) -> Void)? 36 | } 37 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockModifyOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockModifyOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockModifyOperation: MockDatabaseOperation where ID: Hashable { 15 | let databaseItemsSelector: (MockDatabase, (inout [ID: T]) -> Void) -> Void 16 | let id: (T) -> ID 17 | let itemsToSave: [T]? 18 | let itemIDsToDelete: [ID]? 19 | 20 | init( 21 | _ database: MockDatabase, 22 | _ space: DecisionSpace?, 23 | _ databaseItemsSelector: @escaping (MockDatabase, (inout [ID: T]) -> Void) -> Void, 24 | _ id: @escaping (T) -> ID, 25 | _ itemsToSave: [T]? = nil, 26 | _ itemIDsToDelete: [ID]? = nil 27 | ) { 28 | self.databaseItemsSelector = databaseItemsSelector 29 | self.id = id 30 | self.itemsToSave = itemsToSave 31 | self.itemIDsToDelete = itemIDsToDelete 32 | super.init(database, space) 33 | } 34 | 35 | var modifyItemsCompletionBlock: (([T]?, [ID]?, Error?) -> Void)? 36 | var perItemCompletionBlock: ((T, Error?) -> Void)? 37 | 38 | // TODO: Support atomicity. 39 | public override func start() { 40 | let completion = try! XCTUnwrap(self.modifyItemsCompletionBlock) 41 | mockDatabase.queue.async { 42 | if let space = self.space, space.decide() { 43 | completion(nil, nil, MockError.simulated) 44 | return 45 | } 46 | 47 | self.databaseItemsSelector(self.mockDatabase) { databaseItems in 48 | if let itemIDsToDelete = self.itemIDsToDelete { 49 | guard itemIDsToDelete.allSatisfy(databaseItems.keys.contains) else { 50 | completion(nil, nil, MockError.doesNotExist) 51 | return 52 | } 53 | 54 | // TODO: What happens if the caller saves and deletes the same record zone? 55 | for itemID in itemIDsToDelete { 56 | databaseItems.removeValue(forKey: itemID) 57 | } 58 | } 59 | 60 | if let itemsToSave = self.itemsToSave { 61 | for item in itemsToSave { 62 | // TODO: What happens if the caller saves two different items with the same ID? 63 | let id = self.id(item) 64 | databaseItems[id] = item 65 | 66 | if let perItemCompletion = self.perItemCompletionBlock { 67 | if let space = self.space, space.decide() { 68 | perItemCompletion(item, MockError.simulated) 69 | } else { 70 | perItemCompletion(item, nil) 71 | } 72 | } 73 | } 74 | } 75 | 76 | completion(self.itemsToSave, self.itemIDsToDelete, nil) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockModifyRecordZonesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockModifyRecordZonesOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockModifyRecordZonesOperation: 15 | MockModifyOperation, 16 | ACKModifyRecordZonesOperation 17 | { 18 | public var modifyRecordZonesCompletionBlock: 19 | (([CKRecordZone]?, [CKRecordZone.ID]?, Error?) -> Void)? 20 | 21 | init( 22 | _ database: MockDatabase, 23 | _ space: DecisionSpace?, 24 | _ recordZonesToSave: [CKRecordZone]? = nil, 25 | _ recordZoneIDsToDelete: [CKRecordZone.ID]? = nil 26 | ) { 27 | super.init( 28 | database, 29 | space, 30 | { mockDatabase, operation in operation(&mockDatabase.recordZones) }, 31 | { $0.zoneID }, 32 | recordZonesToSave, 33 | recordZoneIDsToDelete 34 | ) 35 | super.modifyItemsCompletionBlock = { [unowned self] itemsToSave, itemIDsToDelete, error in 36 | let completion = try! XCTUnwrap(self.modifyRecordZonesCompletionBlock) 37 | completion(itemsToSave, itemIDsToDelete, error) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockModifyRecordsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockModifyRecordsOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockModifyRecordsOperation: 15 | MockModifyOperation, 16 | ACKModifyRecordsOperation 17 | { 18 | init( 19 | _ database: MockDatabase, 20 | _ space: DecisionSpace?, 21 | _ recordsToSave: [CKRecord]? = nil, 22 | _ recordIDsToDelete: [CKRecord.ID]? = nil 23 | ) { 24 | super.init( 25 | database, 26 | space, 27 | { mockDatabase, operation in operation(&mockDatabase.records) }, 28 | { $0.recordID }, 29 | recordsToSave, 30 | recordIDsToDelete 31 | ) 32 | super.perItemCompletionBlock = { [unowned self] record, error in 33 | if let perRecordProgressBlock = self.perRecordProgressBlock, error == nil { 34 | perRecordProgressBlock(record, 0.7) 35 | perRecordProgressBlock(record, 1.0) 36 | } 37 | 38 | if let perRecordCompletionBlock = self.perRecordCompletionBlock { 39 | perRecordCompletionBlock(record, error) 40 | } 41 | } 42 | super.modifyItemsCompletionBlock = { [unowned self] itemsToSave, itemIDsToDelete, error in 43 | let completion = try! XCTUnwrap(self.modifyRecordsCompletionBlock) 44 | completion(itemsToSave, itemIDsToDelete, error) 45 | } 46 | } 47 | 48 | public var isAtomic = true 49 | 50 | public var savePolicy = CKModifyRecordsOperation.RecordSavePolicy.ifServerRecordUnchanged 51 | 52 | public var clientChangeTokenData: Data? 53 | 54 | public var perRecordProgressBlock: ((CKRecord, Double) -> Void)? 55 | 56 | public var perRecordCompletionBlock: ((CKRecord, Error?) -> Void)? 57 | 58 | public var modifyRecordsCompletionBlock: (([CKRecord]?, [CKRecord.ID]?, Error?) -> Void)? 59 | } 60 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockModifySubscriptionsOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockModifySubscriptionsOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockModifySubscriptionsOperation: 15 | MockModifyOperation, 16 | ACKModifySubscriptionsOperation 17 | { 18 | public var modifySubscriptionsCompletionBlock: 19 | (([CKSubscription]?, [CKSubscription.ID]?, Error?) -> Void)? 20 | 21 | init( 22 | _ database: MockDatabase, 23 | _ space: DecisionSpace?, 24 | _ subscriptionsToSave: [CKSubscription]? = nil, 25 | _ subscriptionIDsToDelete: [CKSubscription.ID]? = nil 26 | ) { 27 | super.init( 28 | database, 29 | space, 30 | { mockDatabase, operation in operation(&mockDatabase.subscriptions) }, 31 | { $0.subscriptionID }, 32 | subscriptionsToSave, 33 | subscriptionIDsToDelete 34 | ) 35 | super.modifyItemsCompletionBlock = { [unowned self] itemsToSave, itemIDsToDelete, error in 36 | let completion = try! XCTUnwrap(self.modifySubscriptionsCompletionBlock) 37 | completion(itemsToSave, itemIDsToDelete, error) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | @testable import AsyncCloudKit 12 | 13 | public class MockOperation: Operation, ACKOperation { 14 | public var configuration: CKOperation.Configuration! 15 | } 16 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockOperationFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockOperationFactory.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | 11 | @testable import AsyncCloudKit 12 | 13 | public class MockOperationFactory: OperationFactory { 14 | let database: MockDatabase 15 | var space: DecisionSpace? 16 | 17 | init(_ database: MockDatabase, _ space: DecisionSpace? = nil) { 18 | self.database = database 19 | self.space = space 20 | } 21 | 22 | public func createFetchAllRecordZonesOperation() -> ACKFetchRecordZonesOperation { 23 | MockFetchRecordZonesOperation(database, space) 24 | } 25 | 26 | public func createFetchAllSubscriptionsOperation() -> ACKFetchSubscriptionsOperation { 27 | MockFetchSubscriptionsOperation(database, space) 28 | } 29 | 30 | public static let currentUserRecordID = CKRecord.ID(recordName: "CurrentUserRecord") 31 | 32 | public func createFetchCurrentUserRecordOperation() -> ACKFetchRecordsOperation { 33 | MockFetchRecordsOperation(database, space, [MockOperationFactory.currentUserRecordID]) 34 | } 35 | 36 | public func createFetchRecordsOperation(recordIDs: [CKRecord.ID]) -> ACKFetchRecordsOperation { 37 | MockFetchRecordsOperation(database, space, recordIDs) 38 | } 39 | 40 | public func createFetchRecordZonesOperation(recordZoneIDs: [CKRecordZone.ID]) 41 | -> ACKFetchRecordZonesOperation 42 | { 43 | MockFetchRecordZonesOperation(database, space, recordZoneIDs) 44 | } 45 | 46 | public func createFetchSubscriptionsOperation( 47 | subscriptionIDs: [CKSubscription.ID] 48 | ) -> ACKFetchSubscriptionsOperation { 49 | MockFetchSubscriptionsOperation(database, space, subscriptionIDs) 50 | } 51 | 52 | public func createModifyRecordsOperation( 53 | recordsToSave: [CKRecord]?, recordIDsToDelete: [CKRecord.ID]? 54 | ) -> ACKModifyRecordsOperation { 55 | MockModifyRecordsOperation(database, space, recordsToSave, recordIDsToDelete) 56 | } 57 | 58 | public func createModifyRecordZonesOperation( 59 | recordZonesToSave: [CKRecordZone]?, recordZoneIDsToDelete: [CKRecordZone.ID]? 60 | ) -> ACKModifyRecordZonesOperation { 61 | MockModifyRecordZonesOperation(database, space, recordZonesToSave, recordZoneIDsToDelete) 62 | } 63 | 64 | public func createModifySubscriptionsOperation( 65 | subscriptionsToSave: [CKSubscription]? = nil, 66 | subscriptionIDsToDelete: [CKSubscription.ID]? = nil 67 | ) -> ACKModifySubscriptionsOperation { 68 | MockModifySubscriptionsOperation(database, space, subscriptionsToSave, subscriptionIDsToDelete) 69 | } 70 | 71 | public func createQueryOperation() -> ACKQueryOperation { 72 | MockQueryOperation(database, space) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Mocks/MockQueryOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockQueryOperation.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import XCTest 11 | 12 | @testable import AsyncCloudKit 13 | 14 | public class MockQueryOperation: MockDatabaseOperation, ACKQueryOperation { 15 | override init(_ database: MockDatabase, _ space: DecisionSpace?) { 16 | super.init(database, space) 17 | } 18 | 19 | public var query: CKQuery? 20 | 21 | public var cursor: CKQueryOperation.Cursor? 22 | 23 | public var desiredKeys: [CKRecord.FieldKey]? 24 | 25 | public var zoneID: CKRecordZone.ID? 26 | 27 | public var resultsLimit: Int = CKQueryOperation.maximumResults 28 | 29 | public var recordFetchedBlock: ((CKRecord) -> Void)? 30 | 31 | public var queryCompletionBlock: ((CKQueryOperation.Cursor?, Error?) -> Void)? 32 | 33 | public override func start() { 34 | let fetched = try! XCTUnwrap(self.recordFetchedBlock) 35 | let completion = try! XCTUnwrap(self.queryCompletionBlock) 36 | mockDatabase.queue.async { 37 | self.mockDatabase.perform(try! XCTUnwrap(self.query), inZoneWith: self.zoneID) { 38 | results, error in 39 | if let space = self.space, space.decide() { 40 | completion(nil, MockError.simulated) 41 | return 42 | } 43 | 44 | guard let results = results else { 45 | completion(nil, error!) 46 | return 47 | } 48 | 49 | var resultsReturned = 0 50 | for record in results { 51 | guard 52 | self.resultsLimit == CKQueryOperation.maximumResults 53 | || resultsReturned < self.resultsLimit 54 | else { 55 | // FIXME: We can't actually create a Cursor. 56 | completion(nil, nil) 57 | return 58 | } 59 | 60 | resultsReturned += 1 61 | fetched(record) 62 | } 63 | 64 | completion(nil, nil) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/ProgressTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressTests.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import AsyncCloudKit 12 | 13 | final class ProgressTests: XCTestCase { 14 | func testCompleteIsNotLessThanItself() { 15 | XCTAssertFalse(Progress.complete < .complete) 16 | } 17 | 18 | func testIncompleteWithSameProgressIsNotLessThanItself() { 19 | XCTAssertFalse(Progress(percent: 50) < Progress(percent: 50)) 20 | } 21 | 22 | func testCompleteIsNotLessThanIncomplete() { 23 | XCTAssertFalse(Progress.complete < .incomplete(percent: 50)) 24 | } 25 | 26 | func testIncompleteWithLessProgressIsLessThanItself() { 27 | XCTAssertLessThan(Progress(percent: 40), Progress(percent: 50)) 28 | } 29 | 30 | func testIncompleteIsLessThanComplete() { 31 | XCTAssertLessThan(Progress(percent: 50), .complete) 32 | } 33 | 34 | func testLessThanZeroPercentEqualsZeroPercent() { 35 | XCTAssertEqual(Progress(rawValue: -1), .incomplete(percent: 0)) 36 | } 37 | 38 | func testMoreThanOneHundredPercentEqualsComplete() { 39 | XCTAssertEqual(Progress(rawValue: 101), .complete) 40 | } 41 | 42 | func testOneHundredPercentEqualsComplete() { 43 | XCTAssertEqual(Progress(percent: 100), .complete) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/AsyncCloudKitTests/Simulation/DecisionSpace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKContainerTests.swift 3 | // AsyncCloudKit 4 | // 5 | // Created by Chris Araman on 12/20/21. 6 | // Copyright © 2021 Chris Araman. All rights reserved. 7 | // 8 | 9 | /// Provides logic to exhaustively search a decision space. 10 | /// - SeeAlso: [rseq](https://qumulo.com/blog/making-100-code-coverage-as-easy-as-flipping-a-coin/) 11 | class DecisionSpace { 12 | var space = [Bool]() 13 | var index = [Bool].Index() 14 | var decided = false 15 | 16 | func decide() -> Bool { 17 | if index == space.endIndex { 18 | space.append(false) 19 | } 20 | 21 | let decision = space[index] 22 | index += 1 23 | decided = true 24 | 25 | return decision 26 | } 27 | 28 | func hasDecided() -> Bool { 29 | decided 30 | } 31 | 32 | func hasDecidedAffirmatively() -> Bool { 33 | space.contains(true) 34 | } 35 | 36 | func next() -> Bool { 37 | index = [Bool].Index() 38 | decided = false 39 | 40 | guard let lastFalse = space.lastIndex(of: false) else { 41 | // We have exhausted the decision space. 42 | space.removeAll() 43 | return false 44 | } 45 | 46 | space.removeLast(space.endIndex - lastFalse - 1) 47 | space[lastFalse] = true 48 | return true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | asynccloudkit.hiddenplace.dev 2 | -------------------------------------------------------------------------------- /docs/Enums.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enumerations Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | AsyncCloudKit 1.0.1 Docs 25 | 26 | (100% documented) 27 |

28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |

36 | 37 | GitHub 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 96 |
97 | 98 |
99 |
100 |

Enumerations

101 |

The following enumerations are available globally.

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

    Represents the completion progress of a CKRecord save or fetch operation.

    124 | 125 | See more 126 |
    127 |
    128 |

    Declaration

    129 |
    130 |

    Swift

    131 |
    public enum Progress
    132 |
    extension Progress: RawRepresentable
    133 |
    extension Progress: Comparable
    134 | 135 |
    136 |
    137 |
    138 | Show on GitHub 139 |
    140 |
    141 |
    142 |
  • 143 |
144 |
145 |
146 |
147 | 148 |
149 |
150 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /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 | 21 |
22 |

23 | 24 | AsyncCloudKit 1.0.1 Docs 25 | 26 | (100% documented) 27 |

28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |

36 | 37 | GitHub 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 96 |
97 | 98 |
99 |
100 |

Extensions

101 |

The following extensions are available globally.

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

    An extension that declares CKContainer 124 | conforms to the ACKContainer protocol provided by AsyncCloudKit.

    125 |
    126 |

    See also

    127 | SeeAlso:CloudKit 128 | 129 |
    130 | 131 |
    132 |
    133 |

    Declaration

    134 |
    135 |

    Swift

    136 |
    extension CKContainer: ACKContainer
    137 | 138 |
    139 |
    140 |
    141 |
    142 |
  • 143 |
  • 144 |
    145 | 146 | 147 | 148 | CKDatabase 149 | 150 |
    151 |
    152 |
    153 |
    154 |
    155 |
    156 |

    An extension that declares CKDatabase 157 | conforms to the ACKDatabase protocol provided by AsyncCloudKit.

    158 |
    159 |

    See also

    160 | CloudKit 161 | 162 |
    163 | 164 |
    165 |
    166 |

    Declaration

    167 |
    168 |

    Swift

    169 |
    extension CKDatabase: ACKDatabase
    170 | 171 |
    172 |
    173 |
    174 |
    175 |
  • 176 |
177 |
178 |
179 |
180 | 181 |
182 |
183 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /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 | 21 |
22 |

23 | 24 | AsyncCloudKit 1.0.1 Docs 25 | 26 | (100% documented) 27 |

28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |

36 | 37 | GitHub 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 96 |
97 | 98 |
99 |
100 |

Protocols

101 |

The following protocols are available globally.

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

    A protocol used to abstract a CKContainer.

    124 | 125 |

    Invoke the async extension method on your CKContainer 126 | instances to fetch the account status asynchronously.

    127 |
    128 |

    See also

    129 | CloudKit 130 | 131 |
    132 | 133 | See more 134 |
    135 |
    136 |

    Declaration

    137 |
    138 |

    Swift

    139 |
    public protocol ACKContainer
    140 | 141 |
    142 |
    143 |
    144 | Show on GitHub 145 |
    146 |
    147 |
    148 |
  • 149 |
  • 150 |
    151 | 152 | 153 | 154 | ACKDatabase 155 | 156 |
    157 |
    158 |
    159 |
    160 |
    161 |
    162 |

    A protocol used to abstract a CKDatabase.

    163 | 164 |

    Invoke the async extension methods on your 165 | CKDatabase 166 | instances to process records, record zones, and subscriptions asynchronously.

    167 |
    168 |

    See also

    169 | CloudKit 170 | 171 |
    172 | 173 | See more 174 |
    175 |
    176 |

    Declaration

    177 |
    178 |

    Swift

    179 |
    public protocol ACKDatabase
    180 | 181 |
    182 |
    183 |
    184 | Show on GitHub 185 |
    186 |
    187 |
    188 |
  • 189 |
190 |
191 |
192 |
193 | 194 |
195 |
196 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /docs/Protocols/ACKContainer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ACKContainer Protocol Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | AsyncCloudKit 1.0.1 Docs 25 | 26 | (100% documented) 27 |

28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |

36 | 37 | GitHub 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 96 |
97 | 98 |
99 |
100 |

ACKContainer

101 |
102 |
103 | 104 |
public protocol ACKContainer
105 | 106 |
107 |
108 |

A protocol used to abstract a CKContainer.

109 | 110 |

Invoke the async extension method on your CKContainer 111 | instances to fetch the account status asynchronously.

112 |
113 |

See also

114 | CloudKit 115 | 116 |
117 | 118 |
119 | Show on GitHub 120 |
121 |
122 |
123 | 124 |
125 |
126 |
127 |
    128 |
  • 129 |
    130 | 131 | 132 | 133 | accountStatus(completionHandler:) 134 | 135 |
    136 |
    137 |
    138 |
    139 |
    140 |
    141 |

    Implemented by CKContainer.

    142 |
    143 |

    See also

    144 | accountStatus 145 | 146 |
    147 | 148 |
    149 |
    150 |

    Declaration

    151 |
    152 |

    Swift

    153 |
    func accountStatus(completionHandler: @escaping (CKAccountStatus, Error?) -> Void)
    154 | 155 |
    156 |
    157 |
    158 | Show on GitHub 159 |
    160 |
    161 |
    162 |
  • 163 |
  • 164 |
    165 | 166 | 167 | 168 | accountStatus() 169 | 170 | 171 | Extension method, asynchronous 172 | 173 |
    174 |
    175 |
    176 |
    177 |
    178 |
    179 |

    Determines whether the system can access the user’s iCloud account.

    180 |
    181 |

    See also

    182 | accountStatus 183 | 184 |
    185 | 186 |
    187 |
    188 |

    Declaration

    189 |
    190 |

    Swift

    191 |
    public func accountStatus() async throws -> CKAccountStatus
    192 | 193 |
    194 |
    195 |
    196 |

    Return Value

    197 |

    The CKAccountStatus.

    198 |
    199 |
    200 | Show on GitHub 201 |
    202 |
    203 |
    204 |
  • 205 |
206 |
207 |
208 |
209 | 210 |
211 |
212 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /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 | 21 |
22 |

23 | 24 | AsyncCloudKit 1.0.1 Docs 25 | 26 | (100% documented) 27 |

28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |

36 | 37 | GitHub 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 96 |
97 | 98 |
99 |
100 |

Structures

101 |

The following structures are available globally.

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

    An AsyncSequence 124 | of results from an AsyncCloudKit operation. If this sequence is iterated twice, 125 | no second operation is queued, and no results are returned.

    126 | 127 | See more 128 |
    129 |
    130 |

    Declaration

    131 |
    132 |

    Swift

    133 |
    public struct ACKSequence<Element> : AsyncSequence
    134 | 135 |
    136 |
    137 |
    138 | Show on GitHub 139 |
    140 |
    141 |
    142 |
  • 143 |
144 |
145 |
146 |
147 | 148 |
149 |
150 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /docs/Structs/ACKSequence.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ACKSequence Structure Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | AsyncCloudKit 1.0.1 Docs 25 | 26 | (100% documented) 27 |

28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |

36 | 37 | GitHub 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 96 |
97 | 98 |
99 |
100 |

ACKSequence

101 |
102 |
103 | 104 |
public struct ACKSequence<Element> : AsyncSequence
105 | 106 |
107 |
108 |

An AsyncSequence 109 | of results from an AsyncCloudKit operation. If this sequence is iterated twice, 110 | no second operation is queued, and no results are returned.

111 | 112 |
113 | Show on GitHub 114 |
115 |
116 |
117 | 118 |
119 |
120 |
121 |
    122 |
  • 123 |
    124 | 125 | 126 | 127 | Iterator 128 | 129 |
    130 |
    131 |
    132 |
    133 |
    134 |
    135 |

    An AsyncIteratorProtocol 136 | of results from an AsyncCloudKit operation.

    137 |
    138 |

    See also

    139 | AsyncSequence 140 | 141 |
    142 | 143 | See more 144 |
    145 |
    146 |

    Declaration

    147 |
    148 |

    Swift

    149 |
    public struct Iterator : AsyncIteratorProtocol
    150 | 151 |
    152 |
    153 |
    154 | Show on GitHub 155 |
    156 |
    157 |
    158 |
  • 159 |
  • 160 |
    161 | 162 | 163 | 164 | makeAsyncIterator() 165 | 166 |
    167 |
    168 |
    169 |
    170 |
    171 |
    172 |

    Creates an Iterator of record zones from an AsyncCloudKit operation.

    173 |
    174 |

    See also

    175 | AsyncSequence 176 | 177 |
    178 | 179 |
    180 |
    181 |

    Declaration

    182 |
    183 |

    Swift

    184 |
    public func makeAsyncIterator() -> Iterator
    185 | 186 |
    187 |
    188 |
    189 |

    Return Value

    190 |

    The Iterator.

    191 |
    192 |
    193 | Show on GitHub 194 |
    195 |
    196 |
    197 |
  • 198 |
199 |
200 |
201 |
202 | 203 |
204 |
205 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /docs/Structs/ACKSequence/Iterator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Iterator Structure Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | AsyncCloudKit 1.0.1 Docs 25 | 26 | (100% documented) 27 |

28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |

36 | 37 | GitHub 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 96 |
97 | 98 |
99 |
100 |

Iterator

101 |
102 |
103 | 104 |
public struct Iterator : AsyncIteratorProtocol
105 | 106 |
107 |
108 |

An AsyncIteratorProtocol 109 | of results from an AsyncCloudKit operation.

110 |
111 |

See also

112 | AsyncSequence 113 | 114 |
115 | 116 |
117 | Show on GitHub 118 |
119 |
120 |
121 | 122 |
123 |
124 |
125 |
    126 |
  • 127 |
    128 | 129 | 130 | 131 | next() 132 | 133 | 134 | Asynchronous 135 | 136 |
    137 |
    138 |
    139 |
    140 |
    141 |
    142 |

    Asynchronously advances to the next result and returns it, or ends the 143 | sequence if there is no next result.

    144 | 145 |
    146 |
    147 |

    Declaration

    148 |
    149 |

    Swift

    150 |
    public mutating func next() async throws -> Element?
    151 | 152 |
    153 |
    154 |
    155 |

    Return Value

    156 |

    The next result, if it exists, or nil to signal the end of 157 | the sequence.

    158 |
    159 |
    160 | Show on GitHub 161 |
    162 |
    163 |
    164 |
  • 165 |
166 |
167 |
168 |
169 | 170 |
171 |
172 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /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 | /*! Jazzy - https://github.com/realm/jazzy 2 | * Copyright Realm Inc. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | /* Credit to https://gist.github.com/wataru420/2048287 */ 6 | .highlight .c { 7 | color: #999988; 8 | font-style: italic; } 9 | 10 | .highlight .err { 11 | color: #a61717; 12 | background-color: #e3d2d2; } 13 | 14 | .highlight .k { 15 | color: #000000; 16 | font-weight: bold; } 17 | 18 | .highlight .o { 19 | color: #000000; 20 | font-weight: bold; } 21 | 22 | .highlight .cm { 23 | color: #999988; 24 | font-style: italic; } 25 | 26 | .highlight .cp { 27 | color: #999999; 28 | font-weight: bold; } 29 | 30 | .highlight .c1 { 31 | color: #999988; 32 | font-style: italic; } 33 | 34 | .highlight .cs { 35 | color: #999999; 36 | font-weight: bold; 37 | font-style: italic; } 38 | 39 | .highlight .gd { 40 | color: #000000; 41 | background-color: #ffdddd; } 42 | 43 | .highlight .gd .x { 44 | color: #000000; 45 | background-color: #ffaaaa; } 46 | 47 | .highlight .ge { 48 | color: #000000; 49 | font-style: italic; } 50 | 51 | .highlight .gr { 52 | color: #aa0000; } 53 | 54 | .highlight .gh { 55 | color: #999999; } 56 | 57 | .highlight .gi { 58 | color: #000000; 59 | background-color: #ddffdd; } 60 | 61 | .highlight .gi .x { 62 | color: #000000; 63 | background-color: #aaffaa; } 64 | 65 | .highlight .go { 66 | color: #888888; } 67 | 68 | .highlight .gp { 69 | color: #555555; } 70 | 71 | .highlight .gs { 72 | font-weight: bold; } 73 | 74 | .highlight .gu { 75 | color: #aaaaaa; } 76 | 77 | .highlight .gt { 78 | color: #aa0000; } 79 | 80 | .highlight .kc { 81 | color: #000000; 82 | font-weight: bold; } 83 | 84 | .highlight .kd { 85 | color: #000000; 86 | font-weight: bold; } 87 | 88 | .highlight .kp { 89 | color: #000000; 90 | font-weight: bold; } 91 | 92 | .highlight .kr { 93 | color: #000000; 94 | font-weight: bold; } 95 | 96 | .highlight .kt { 97 | color: #445588; } 98 | 99 | .highlight .m { 100 | color: #009999; } 101 | 102 | .highlight .s { 103 | color: #d14; } 104 | 105 | .highlight .na { 106 | color: #008080; } 107 | 108 | .highlight .nb { 109 | color: #0086B3; } 110 | 111 | .highlight .nc { 112 | color: #445588; 113 | font-weight: bold; } 114 | 115 | .highlight .no { 116 | color: #008080; } 117 | 118 | .highlight .ni { 119 | color: #800080; } 120 | 121 | .highlight .ne { 122 | color: #990000; 123 | font-weight: bold; } 124 | 125 | .highlight .nf { 126 | color: #990000; } 127 | 128 | .highlight .nn { 129 | color: #555555; } 130 | 131 | .highlight .nt { 132 | color: #000080; } 133 | 134 | .highlight .nv { 135 | color: #008080; } 136 | 137 | .highlight .ow { 138 | color: #000000; 139 | font-weight: bold; } 140 | 141 | .highlight .w { 142 | color: #bbbbbb; } 143 | 144 | .highlight .mf { 145 | color: #009999; } 146 | 147 | .highlight .mh { 148 | color: #009999; } 149 | 150 | .highlight .mi { 151 | color: #009999; } 152 | 153 | .highlight .mo { 154 | color: #009999; } 155 | 156 | .highlight .sb { 157 | color: #d14; } 158 | 159 | .highlight .sc { 160 | color: #d14; } 161 | 162 | .highlight .sd { 163 | color: #d14; } 164 | 165 | .highlight .s2 { 166 | color: #d14; } 167 | 168 | .highlight .se { 169 | color: #d14; } 170 | 171 | .highlight .sh { 172 | color: #d14; } 173 | 174 | .highlight .si { 175 | color: #d14; } 176 | 177 | .highlight .sx { 178 | color: #d14; } 179 | 180 | .highlight .sr { 181 | color: #009926; } 182 | 183 | .highlight .s1 { 184 | color: #d14; } 185 | 186 | .highlight .ss { 187 | color: #990073; } 188 | 189 | .highlight .bp { 190 | color: #999999; } 191 | 192 | .highlight .vc { 193 | color: #008080; } 194 | 195 | .highlight .vg { 196 | color: #008080; } 197 | 198 | .highlight .vi { 199 | color: #008080; } 200 | 201 | .highlight .il { 202 | color: #009999; } 203 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | /*! Jazzy - https://github.com/realm/jazzy 2 | * Copyright Realm Inc. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | *, *:before, *:after { 6 | box-sizing: inherit; } 7 | 8 | body { 9 | margin: 0; 10 | background: #fff; 11 | color: #333; 12 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | letter-spacing: .2px; 14 | -webkit-font-smoothing: antialiased; 15 | box-sizing: border-box; } 16 | 17 | h1 { 18 | font-size: 2rem; 19 | font-weight: 700; 20 | margin: 1.275em 0 0.6em; } 21 | 22 | h2 { 23 | font-size: 1.75rem; 24 | font-weight: 700; 25 | margin: 1.275em 0 0.3em; } 26 | 27 | h3 { 28 | font-size: 1.5rem; 29 | font-weight: 700; 30 | margin: 1em 0 0.3em; } 31 | 32 | h4 { 33 | font-size: 1.25rem; 34 | font-weight: 700; 35 | margin: 1.275em 0 0.85em; } 36 | 37 | h5 { 38 | font-size: 1rem; 39 | font-weight: 700; 40 | margin: 1.275em 0 0.85em; } 41 | 42 | h6 { 43 | font-size: 1rem; 44 | font-weight: 700; 45 | margin: 1.275em 0 0.85em; 46 | color: #777; } 47 | 48 | p { 49 | margin: 0 0 1em; } 50 | 51 | ul, ol { 52 | padding: 0 0 0 2em; 53 | margin: 0 0 0.85em; } 54 | 55 | blockquote { 56 | margin: 0 0 0.85em; 57 | padding: 0 15px; 58 | color: #858585; 59 | border-left: 4px solid #e5e5e5; } 60 | 61 | img { 62 | max-width: 100%; } 63 | 64 | a { 65 | color: #4183c4; 66 | text-decoration: none; } 67 | a:hover, a:focus { 68 | outline: 0; 69 | text-decoration: underline; } 70 | a.discouraged { 71 | text-decoration: line-through; } 72 | a.discouraged:hover, a.discouraged:focus { 73 | text-decoration: underline line-through; } 74 | 75 | table { 76 | background: #fff; 77 | width: 100%; 78 | border-collapse: collapse; 79 | border-spacing: 0; 80 | overflow: auto; 81 | margin: 0 0 0.85em; } 82 | 83 | tr:nth-child(2n) { 84 | background-color: #fbfbfb; } 85 | 86 | th, td { 87 | padding: 6px 13px; 88 | border: 1px solid #ddd; } 89 | 90 | hr { 91 | height: 1px; 92 | border: none; 93 | background-color: #ddd; } 94 | 95 | pre { 96 | margin: 0 0 1.275em; 97 | padding: .85em 1em; 98 | overflow: auto; 99 | background: #f7f7f7; 100 | font-size: .85em; 101 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 102 | 103 | code { 104 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 105 | 106 | .item-container p > code, .item-container li > code, .top-matter p > code, .top-matter li > code { 107 | background: #f7f7f7; 108 | padding: .2em; } 109 | .item-container p > code:before, .item-container p > code:after, .item-container li > code:before, .item-container li > code:after, .top-matter p > code:before, .top-matter p > code:after, .top-matter li > code:before, .top-matter li > code:after { 110 | letter-spacing: -.2em; 111 | content: "\00a0"; } 112 | 113 | pre code { 114 | padding: 0; 115 | white-space: pre; } 116 | 117 | .content-wrapper { 118 | display: flex; 119 | flex-direction: column; } 120 | @media (min-width: 768px) { 121 | .content-wrapper { 122 | flex-direction: row; } } 123 | .header { 124 | display: flex; 125 | padding: 8px; 126 | font-size: 0.875em; 127 | background: #444; 128 | color: #999; } 129 | 130 | .header-col { 131 | margin: 0; 132 | padding: 0 8px; } 133 | 134 | .header-col--primary { 135 | flex: 1; } 136 | 137 | .header-link { 138 | color: #fff; } 139 | 140 | .header-icon { 141 | padding-right: 2px; 142 | vertical-align: -3px; 143 | height: 16px; } 144 | 145 | .breadcrumbs { 146 | font-size: 0.875em; 147 | padding: 8px 16px; 148 | margin: 0; 149 | background: #fbfbfb; 150 | border-bottom: 1px solid #ddd; } 151 | 152 | .carat { 153 | height: 10px; 154 | margin: 0 5px; } 155 | 156 | .navigation { 157 | order: 2; } 158 | @media (min-width: 768px) { 159 | .navigation { 160 | order: 1; 161 | width: 25%; 162 | max-width: 300px; 163 | padding-bottom: 64px; 164 | overflow: hidden; 165 | word-wrap: normal; 166 | background: #fbfbfb; 167 | border-right: 1px solid #ddd; } } 168 | .nav-groups { 169 | list-style-type: none; 170 | padding-left: 0; } 171 | 172 | .nav-group-name { 173 | border-bottom: 1px solid #ddd; 174 | padding: 8px 0 8px 16px; } 175 | 176 | .nav-group-name-link { 177 | color: #333; } 178 | 179 | .nav-group-tasks { 180 | margin: 8px 0; 181 | padding: 0 0 0 8px; } 182 | 183 | .nav-group-task { 184 | font-size: 1em; 185 | list-style-type: none; 186 | white-space: nowrap; } 187 | 188 | .nav-group-task-link { 189 | color: #808080; } 190 | 191 | .main-content { 192 | order: 1; } 193 | @media (min-width: 768px) { 194 | .main-content { 195 | order: 2; 196 | flex: 1; 197 | padding-bottom: 60px; } } 198 | .section { 199 | padding: 0 32px; 200 | border-bottom: 1px solid #ddd; } 201 | 202 | .section-content { 203 | max-width: 834px; 204 | margin: 0 auto; 205 | padding: 16px 0; } 206 | 207 | .section-name { 208 | color: #666; 209 | display: block; } 210 | .section-name p { 211 | margin-bottom: inherit; } 212 | 213 | .declaration .highlight { 214 | overflow-x: initial; 215 | padding: 8px 0; 216 | margin: 0; 217 | background-color: transparent; 218 | border: none; } 219 | 220 | .task-group-section { 221 | border-top: 1px solid #ddd; } 222 | 223 | .task-group { 224 | padding-top: 0px; } 225 | 226 | .task-name-container a[name]:before { 227 | content: ""; 228 | display: block; } 229 | 230 | .section-name-container { 231 | position: relative; } 232 | .section-name-container .section-name-link { 233 | position: absolute; 234 | top: 0; 235 | left: 0; 236 | bottom: 0; 237 | right: 0; 238 | margin-bottom: 0; } 239 | .section-name-container .section-name { 240 | position: relative; 241 | pointer-events: none; 242 | z-index: 1; } 243 | .section-name-container .section-name a { 244 | pointer-events: auto; } 245 | 246 | .item-container { 247 | padding: 0; } 248 | 249 | .item { 250 | padding-top: 8px; 251 | width: 100%; 252 | list-style-type: none; } 253 | .item a[name]:before { 254 | content: ""; 255 | display: block; } 256 | .item .token, .item .direct-link { 257 | display: inline-block; 258 | text-indent: -20px; 259 | padding-left: 3px; 260 | margin-left: 20px; 261 | font-size: 1rem; } 262 | .item .declaration-note { 263 | font-size: .85em; 264 | color: #808080; 265 | font-style: italic; } 266 | 267 | .pointer-container { 268 | border-bottom: 1px solid #ddd; 269 | left: -23px; 270 | padding-bottom: 13px; 271 | position: relative; 272 | width: 110%; } 273 | 274 | .pointer { 275 | left: 21px; 276 | top: 7px; 277 | display: block; 278 | position: absolute; 279 | width: 12px; 280 | height: 12px; 281 | border-left: 1px solid #ddd; 282 | border-top: 1px solid #ddd; 283 | background: #fff; 284 | transform: rotate(45deg); } 285 | 286 | .height-container { 287 | display: none; 288 | position: relative; 289 | width: 100%; 290 | overflow: hidden; } 291 | .height-container .section { 292 | background: #fff; 293 | border: 1px solid #ddd; 294 | border-top-width: 0; 295 | padding-top: 10px; 296 | padding-bottom: 5px; 297 | padding: 8px 16px; } 298 | 299 | .aside, .language { 300 | padding: 6px 12px; 301 | margin: 12px 0; 302 | border-left: 5px solid #dddddd; 303 | overflow-y: hidden; } 304 | .aside .aside-title, .language .aside-title { 305 | font-size: 9px; 306 | letter-spacing: 2px; 307 | text-transform: uppercase; 308 | padding-bottom: 0; 309 | margin: 0; 310 | color: #aaa; 311 | -webkit-user-select: none; } 312 | .aside p:last-child, .language p:last-child { 313 | margin-bottom: 0; } 314 | 315 | .language { 316 | border-left: 5px solid #cde9f4; } 317 | .language .aside-title { 318 | color: #4183c4; } 319 | 320 | .aside-warning, .aside-deprecated, .aside-unavailable { 321 | border-left: 5px solid #ff6666; } 322 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 323 | color: #ff0000; } 324 | 325 | .graybox { 326 | border-collapse: collapse; 327 | width: 100%; } 328 | .graybox p { 329 | margin: 0; 330 | word-break: break-word; 331 | min-width: 50px; } 332 | .graybox td { 333 | border: 1px solid #ddd; 334 | padding: 5px 25px 5px 10px; 335 | vertical-align: middle; } 336 | .graybox tr td:first-of-type { 337 | text-align: right; 338 | padding: 7px; 339 | vertical-align: top; 340 | word-break: normal; 341 | width: 40px; } 342 | 343 | .slightly-smaller { 344 | font-size: 0.9em; } 345 | 346 | .footer { 347 | padding: 8px 16px; 348 | background: #444; 349 | color: #ddd; 350 | font-size: 0.8em; } 351 | .footer p { 352 | margin: 8px 0; } 353 | .footer a { 354 | color: #fff; } 355 | 356 | html.dash .header, html.dash .breadcrumbs, html.dash .navigation { 357 | display: none; } 358 | 359 | html.dash .height-container { 360 | display: block; } 361 | 362 | form[role=search] input { 363 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 364 | font-size: 14px; 365 | line-height: 24px; 366 | padding: 0 10px; 367 | margin: 0; 368 | border: none; 369 | border-radius: 1em; } 370 | .loading form[role=search] input { 371 | background: white url(../img/spinner.gif) center right 4px no-repeat; } 372 | 373 | form[role=search] .tt-menu { 374 | margin: 0; 375 | min-width: 300px; 376 | background: #fbfbfb; 377 | color: #333; 378 | border: 1px solid #ddd; } 379 | 380 | form[role=search] .tt-highlight { 381 | font-weight: bold; } 382 | 383 | form[role=search] .tt-suggestion { 384 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 385 | padding: 0 8px; } 386 | form[role=search] .tt-suggestion span { 387 | display: table-cell; 388 | white-space: nowrap; } 389 | form[role=search] .tt-suggestion .doc-parent-name { 390 | width: 100%; 391 | text-align: right; 392 | font-weight: normal; 393 | font-size: 0.9em; 394 | padding-left: 16px; } 395 | 396 | form[role=search] .tt-suggestion:hover, 397 | form[role=search] .tt-suggestion.tt-cursor { 398 | cursor: pointer; 399 | background-color: #4183c4; 400 | color: #fff; } 401 | 402 | form[role=search] .tt-suggestion:hover .doc-parent-name, 403 | form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { 404 | color: #fff; } 405 | -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-araman/AsyncCloudKit/0f309f3491aa1e88fbc633b80153bf078bc8ced5/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-araman/AsyncCloudKit/0f309f3491aa1e88fbc633b80153bf078bc8ced5/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-araman/AsyncCloudKit/0f309f3491aa1e88fbc633b80153bf078bc8ced5/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-araman/AsyncCloudKit/0f309f3491aa1e88fbc633b80153bf078bc8ced5/docs/img/spinner.gif -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AsyncCloudKit Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

22 | 23 | AsyncCloudKit 1.0.1 Docs 24 | 25 | (100% documented) 26 |

27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 |

35 | 36 | GitHub 37 | View on GitHub 38 | 39 |

40 | 41 |
42 | 43 | 48 | 49 |
50 | 95 |
96 | 97 |
98 |
99 | 100 |

⛅️ AsyncCloudKit

101 | 102 |

Swift extensions for asynchronous CloudKit record processing. Designed for simplicity.

103 | 104 |

Swift 105 | Platforms 106 | License 107 | Release

108 | 109 |

Lint | Build | Test 110 | Coverage

111 | 112 |

AsyncCloudKit exposes CloudKit operations as 113 | async functions and 114 | AsyncSequences.

115 |

📦 Adding AsyncCloudKit to Your Project

116 | 117 |

AsyncCloudKit is a Swift Package. 118 | Add a dependency on AsyncCloudKit to your 119 | Package.swift using 120 | Xcode or the 121 | Swift Package Manager. Optionally, specify a 122 | version requirement.

123 |
dependencies: [
124 |   .package(url: "https://github.com/chris-araman/AsyncCloudKit.git", from: "1.0.0")
125 | ]
126 | 
127 | 128 |

Then resolve the dependency:

129 |
swift package resolve
130 | 
131 | 132 |

To update to the latest AsyncCloudKit version compatible with your version requirement:

133 |
swift package update AsyncCloudKit
134 | 
135 |

🌤 Using AsyncCloudKit in Your Project

136 | 137 |

Swift concurrency allows you to process CloudKit records asynchronously, without writing a lot of boilerplate code involving 138 | CKOperations and completion blocks. 139 | Here, we perform a query on our 140 | CKDatabase, then process the results 141 | asynchronously. As each CKRecord is read from the 142 | database, we print its name field.

143 |
import CloudKit
144 | import AsyncCloudKit
145 | 
146 | func queryDueItems(database: CKDatabase, due: Date) async throws {
147 |   for try await record in database
148 |     .performQuery(ofType: "ToDoItem", where: NSPredicate(format: "due >= %@", due)) { (record: CKRecord) in
149 |     print("\(record.name)")
150 |   })
151 | }
152 | 
153 |

Cancellation

154 | 155 |

AsyncCloudKit functions that return an ACKSequence queue an operation immediately. Iterating 156 | the sequence allows you to inspect the results of the operation. If you stop iterating the sequence early, 157 | the operation may be cancelled.

158 | 159 |

Note that because the atBackgroundPriority functions are built on CKDatabase methods that do not provide means of 160 | cancellation, they cannot be canceled. If you need operations to respond to requests for cooperative cancellation, 161 | please use the publishers that do not have atBackgroundPriority in their names. You can still specify 162 | QualityOfService.background 163 | by passing in a 164 | CKOperation.Configuration.

165 |

📘 Documentation

166 | 167 |

💯% documented using Jazzy. 168 | Hosted by GitHub Pages.

169 |

❤️ Contributing

170 | 171 |

Contributions are welcome!

172 |

📚 Further Reading

173 | 174 |

To learn more about Swift Concurrency and CloudKit, watch these videos from WWDC:

175 | 176 | 181 | 182 |

…or review Apple’s documentation:

183 | 184 | 190 | 191 |

If you’re looking for Swift Combine extensions for CloudKit, take a look at 192 | CombineCloudKit!

193 |

📜 License

194 | 195 |

AsyncCloudKit was created by Chris Araman. It is published under the 196 | MIT license.

197 | 198 |
199 |
200 | 201 | 202 |
203 |
204 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | window.jazzy = {'docset': false} 6 | if (typeof window.dash != 'undefined') { 7 | document.documentElement.className += ' dash' 8 | window.jazzy.docset = true 9 | } 10 | if (navigator.userAgent.match(/xcode/i)) { 11 | document.documentElement.className += ' xcode' 12 | window.jazzy.docset = true 13 | } 14 | 15 | function toggleItem($link, $content) { 16 | var animationDuration = 300; 17 | $link.toggleClass('token-open'); 18 | $content.slideToggle(animationDuration); 19 | } 20 | 21 | function itemLinkToContent($link) { 22 | return $link.parent().parent().next(); 23 | } 24 | 25 | // On doc load + hash-change, open any targetted item 26 | function openCurrentItemIfClosed() { 27 | if (window.jazzy.docset) { 28 | return; 29 | } 30 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 31 | $content = itemLinkToContent($link); 32 | if ($content.is(':hidden')) { 33 | toggleItem($link, $content); 34 | } 35 | } 36 | 37 | $(openCurrentItemIfClosed); 38 | $(window).on('hashchange', openCurrentItemIfClosed); 39 | 40 | // On item link ('token') click, toggle its discussion 41 | $('.token').on('click', function(event) { 42 | if (window.jazzy.docset) { 43 | return; 44 | } 45 | var $link = $(this); 46 | toggleItem($link, itemLinkToContent($link)); 47 | 48 | // Keeps the document from jumping to the hash. 49 | var href = $link.attr('href'); 50 | if (history.pushState) { 51 | history.pushState({}, '', href); 52 | } else { 53 | location.hash = href; 54 | } 55 | event.preventDefault(); 56 | }); 57 | 58 | // Clicks on links to the current, closed, item need to open the item 59 | $("a:not('.token')").on('click', function() { 60 | if (location == this.href) { 61 | openCurrentItemIfClosed(); 62 | } 63 | }); 64 | 65 | // KaTeX rendering 66 | if ("katex" in window) { 67 | $($('.math').each( (_, element) => { 68 | katex.render(element.textContent, element, { 69 | displayMode: $(element).hasClass('m-block'), 70 | throwOnError: false, 71 | trust: true 72 | }); 73 | })) 74 | } 75 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | $(function(){ 6 | var $typeahead = $('[data-typeahead]'); 7 | var $form = $typeahead.parents('form'); 8 | var searchURL = $form.attr('action'); 9 | 10 | function displayTemplate(result) { 11 | return result.name; 12 | } 13 | 14 | function suggestionTemplate(result) { 15 | var t = '
'; 16 | t += '' + result.name + ''; 17 | if (result.parent_name) { 18 | t += '' + result.parent_name + ''; 19 | } 20 | t += '
'; 21 | return t; 22 | } 23 | 24 | $typeahead.one('focus', function() { 25 | $form.addClass('loading'); 26 | 27 | $.getJSON(searchURL).then(function(searchData) { 28 | const searchIndex = lunr(function() { 29 | this.ref('url'); 30 | this.field('name'); 31 | this.field('abstract'); 32 | for (const [url, doc] of Object.entries(searchData)) { 33 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 34 | } 35 | }); 36 | 37 | $typeahead.typeahead( 38 | { 39 | highlight: true, 40 | minLength: 3, 41 | autoselect: true 42 | }, 43 | { 44 | limit: 10, 45 | display: displayTemplate, 46 | templates: { suggestion: suggestionTemplate }, 47 | source: function(query, sync) { 48 | const lcSearch = query.toLowerCase(); 49 | const results = searchIndex.query(function(q) { 50 | q.term(lcSearch, { boost: 100 }); 51 | q.term(lcSearch, { 52 | boost: 10, 53 | wildcard: lunr.Query.wildcard.TRAILING 54 | }); 55 | }).map(function(result) { 56 | var doc = searchData[result.ref]; 57 | doc.url = result.ref; 58 | return doc; 59 | }); 60 | sync(results); 61 | } 62 | } 63 | ); 64 | $form.removeClass('loading'); 65 | $typeahead.trigger('focus'); 66 | }); 67 | }); 68 | 69 | var baseURL = searchURL.slice(0, -"search.json".length); 70 | 71 | $typeahead.on('typeahead:select', function(e, result) { 72 | window.location = baseURL + result.url; 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | 4 | ], 5 | "source_directory": "/Users/caraman/src/AsyncCloudKit" 6 | } -------------------------------------------------------------------------------- /script/build_docs: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | bundle install 4 | 5 | jazzy \ 6 | --author 'Chris Araman' \ 7 | --author_url https://github.com/chris-araman \ 8 | --clean \ 9 | --github_url https://github.com/chris-araman/AsyncCloudKit \ 10 | --github-file-prefix https://github.com/chris-araman/AsyncCloudKit/tree/"$(git describe --abbrev=0 --tags)" \ 11 | --module AsyncCloudKit \ 12 | --module-version "$(git describe --abbrev=0 --tags)" \ 13 | --output docs \ 14 | --theme fullwidth 15 | 16 | # Restore GitHub Pages CNAME file 17 | # https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/troubleshooting-custom-domains-and-github-pages#cname-errors 18 | git checkout -- docs/CNAME 19 | -------------------------------------------------------------------------------- /script/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [[ ! -x "$(command -v brew)" ]]; then 4 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 5 | fi 6 | 7 | brew bundle install --no-lock --quiet >/dev/null 8 | 9 | echo "⚙️ Linting AsyncCloudKit..." 10 | 11 | echo " 🧹 git" 12 | git diff --check 13 | 14 | echo " 🧹 markdownlint" 15 | markdownlint --config .markdownlint.json --fix .github . 16 | 17 | echo " 🧹 periphery" 18 | periphery scan --quiet 19 | 20 | echo " 🧹 prettier" 21 | prettier --loglevel warn --write . 22 | 23 | echo " 🧹 shellcheck" 24 | shellcheck --shell=bash script/* 25 | 26 | echo " 🧹 shfmt" 27 | shfmt -d -i 2 -l -w script/ 28 | 29 | echo " 🧹 swift-format" 30 | SOURCES=( 31 | Package*.swift 32 | Sources 33 | Tests 34 | ) 35 | swift format format --in-place --recursive "${SOURCES[@]}" 36 | swift format lint --recursive "${SOURCES[@]}" 37 | 38 | echo "✅ AsyncCloudKit is free of lint!" 39 | --------------------------------------------------------------------------------