├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── config.yml
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .spi.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Example
├── Compatibility
│ ├── App.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Compatibility.swift
│ └── Preview Content
│ │ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Example.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Example.xcscheme
├── Example
│ ├── Assets.xcassets
│ │ ├── .DS_Store
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ContentView.swift
│ ├── ExampleApp.swift
│ └── Info.plist
└── Package.swift
├── LICENSE
├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── Perception
│ ├── Exports.swift
│ └── Macros.swift
├── PerceptionCore
│ ├── Bindable.swift
│ ├── Environment.swift
│ ├── Internal
│ │ ├── BetaChecking.swift
│ │ ├── Exports.swift
│ │ ├── Locking.swift
│ │ ├── ThreadLocal.swift
│ │ ├── Unchecked.swift
│ │ └── _PerceptionRegistrar.swift
│ ├── Perceptible.swift
│ ├── PerceptionChecking.swift
│ ├── PerceptionRegistrar.swift
│ ├── PerceptionTracking.swift
│ └── WithPerceptionTracking.swift
└── PerceptionMacros
│ ├── Availability.swift
│ ├── Extensions.swift
│ ├── PerceptibleMacro.swift
│ └── Plugins.swift
└── Tests
├── PerceptionMacrosTests
└── PerceptionMacrosTests.swift
└── PerceptionTests
├── ModifyTests.swift
├── PerceptionTrackingTests.swift
├── RuntimeWarningTests.swift
└── WithPerceptionTrackingDSLTests.swift
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 2
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Something isn't working as expected
3 | labels: [bug]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thank you for contributing to Perception!
9 |
10 | Before you submit your issue, please complete each text area below with the relevant details for your bug, and complete the steps in the checklist
11 | - type: textarea
12 | attributes:
13 | label: Description
14 | description: |
15 | A short description of the incorrect behavior.
16 |
17 | If you think this issue has been recently introduced and did not occur in an earlier version, please note that. If possible, include the last version that the behavior was correct in addition to your current version.
18 | validations:
19 | required: true
20 | - type: checkboxes
21 | attributes:
22 | label: Checklist
23 | options:
24 | - label: I have determined that this bug is not reproducible using Swift's observation tools. If the bug is reproducible using the `@Observable` macro or another tool from the `Observation` framework, please [file it directly with Apple](https://github.com/apple/swift/issues/new/choose).
25 | required: false
26 | - label: If possible, I've reproduced the issue using the `main` branch of this package.
27 | required: false
28 | - label: This issue hasn't been addressed in an [existing GitHub issue](https://github.com/pointfreeco/swift-perception/issues) or [discussion](https://github.com/pointfreeco/swift-perception/discussions).
29 | required: true
30 | - type: textarea
31 | attributes:
32 | label: Expected behavior
33 | description: Describe what you expected to happen.
34 | validations:
35 | required: false
36 | - type: textarea
37 | attributes:
38 | label: Actual behavior
39 | description: Describe or copy/paste the behavior you observe.
40 | validations:
41 | required: false
42 | - type: textarea
43 | attributes:
44 | label: Steps to reproduce
45 | description: |
46 | Explanation of how to reproduce the incorrect behavior.
47 |
48 | This could include an attached project or link to code that is exhibiting the issue, and/or a screen recording.
49 | placeholder: |
50 | 1. ...
51 | validations:
52 | required: false
53 | - type: input
54 | attributes:
55 | label: Perception version information
56 | description: The version of Perception used to reproduce this issue.
57 | placeholder: "'1.0.0' for example, or a commit hash"
58 | - type: input
59 | attributes:
60 | label: Destination operating system
61 | description: The OS running your application.
62 | placeholder: "'iOS 13' for example"
63 | - type: input
64 | attributes:
65 | label: Xcode version information
66 | description: The version of Xcode used to reproduce this issue.
67 | placeholder: "The version displayed from 'Xcode 〉About Xcode'"
68 | - type: textarea
69 | attributes:
70 | label: Swift Compiler version information
71 | description: The version of Swift used to reproduce this issue.
72 | placeholder: Output from 'xcrun swiftc --version'
73 | render: shell
74 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
3 | contact_links:
4 | - name: Project Discussion
5 | url: https://github.com/pointfreeco/swift-perception/discussions
6 | about: Perception Q&A, ideas, and more
7 | - name: Documentation
8 | url: https://pointfreeco.github.io/swift-perception/main/documentation/perception/
9 | about: Read Perception's documentation
10 | - name: Videos
11 | url: https://www.pointfree.co/
12 | about: Watch videos to get a behind-the-scenes look at how this library and others were motivated and built
13 | - name: Slack
14 | url: https://www.pointfree.co/slack-invite
15 | about: Community chat
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - '*'
10 | workflow_dispatch:
11 |
12 | concurrency:
13 | group: ci-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | library:
18 | name: macOS
19 | strategy:
20 | matrix:
21 | xcode: ['15.4']
22 | config: ['debug', 'release']
23 | runs-on: macos-14
24 | steps:
25 | - uses: actions/checkout@v4
26 | - name: Select Xcode ${{ matrix.xcode }}
27 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
28 | - name: Run ${{ matrix.config }} tests
29 | run: swift test -c ${{ matrix.config }}
30 | - name: Run compatibility tests
31 | run: make test-compatibility
32 | if: ${{ matrix.config == 'debug' }}
33 |
34 | linux:
35 | name: Linux
36 | strategy:
37 | matrix:
38 | swift:
39 | - '6.0'
40 | runs-on: ubuntu-latest
41 | container: swift:${{ matrix.swift }}
42 | steps:
43 | - uses: actions/checkout@v4
44 | - name: Build
45 | run: swift build
46 |
47 | wasm:
48 | name: Wasm
49 | runs-on: ubuntu-latest
50 | steps:
51 | - uses: actions/checkout@v4
52 | - uses: bytecodealliance/actions/wasmtime/setup@v1
53 | - name: Install Swift and Swift SDK for WebAssembly
54 | run: |
55 | PREFIX=/opt/swift
56 | set -ex
57 | curl -f -o /tmp/swift.tar.gz "https://download.swift.org/swift-6.0.2-release/ubuntu2204/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-ubuntu22.04.tar.gz"
58 | sudo mkdir -p $PREFIX; sudo tar -xzf /tmp/swift.tar.gz -C $PREFIX --strip-component 1
59 | $PREFIX/usr/bin/swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip --checksum 6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4
60 | echo "$PREFIX/usr/bin" >> $GITHUB_PATH
61 |
62 | - name: Build
63 | run: swift build --swift-sdk wasm32-unknown-wasi -Xlinker -z -Xlinker stack-size=$((1024 * 1024))
64 |
65 | check-macro-compatibility:
66 | name: Check Macro Compatibility
67 | runs-on: macos-latest
68 | steps:
69 | - name: Checkout repository
70 | uses: actions/checkout@v4
71 | - name: Run Swift Macro Compatibility Check
72 | uses: Matejkob/swift-macro-compatibility-check@v1
73 | with:
74 | run-tests: false
75 | major-versions-only: true
76 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | release:
4 | types: [published]
5 | workflow_dispatch:
6 | jobs:
7 | project-channel:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Dump Github context
11 | env:
12 | GITHUB_CONTEXT: ${{ toJSON(github) }}
13 | run: echo "$GITHUB_CONTEXT"
14 | - name: Slack Notification on SUCCESS
15 | if: success()
16 | uses: tokorom/action-slack-incoming-webhook@main
17 | env:
18 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_PROJECT_CHANNEL_WEBHOOK_URL }}
19 | with:
20 | text: swift-perception ${{ github.event.release.tag_name }} has been released.
21 | blocks: |
22 | [
23 | {
24 | "type": "header",
25 | "text": {
26 | "type": "plain_text",
27 | "text": "swift-perception ${{ github.event.release.tag_name}}"
28 | }
29 | },
30 | {
31 | "type": "section",
32 | "text": {
33 | "type": "mrkdwn",
34 | "text": ${{ toJSON(github.event.release.body) }}
35 | }
36 | },
37 | {
38 | "type": "section",
39 | "text": {
40 | "type": "mrkdwn",
41 | "text": "${{ github.event.release.html_url }}"
42 | }
43 | }
44 | ]
45 |
46 | releases-channel:
47 | runs-on: ubuntu-latest
48 | steps:
49 | - name: Dump Github context
50 | env:
51 | GITHUB_CONTEXT: ${{ toJSON(github) }}
52 | run: echo "$GITHUB_CONTEXT"
53 | - name: Slack Notification on SUCCESS
54 | if: success()
55 | uses: tokorom/action-slack-incoming-webhook@main
56 | env:
57 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
58 | with:
59 | text: swift-perception ${{ github.event.release.tag_name }} has been released.
60 | blocks: |
61 | [
62 | {
63 | "type": "header",
64 | "text": {
65 | "type": "plain_text",
66 | "text": "swift-perception ${{ github.event.release.tag_name}}"
67 | }
68 | },
69 | {
70 | "type": "section",
71 | "text": {
72 | "type": "mrkdwn",
73 | "text": ${{ toJSON(github.event.release.body) }}
74 | }
75 | },
76 | {
77 | "type": "section",
78 | "text": {
79 | "type": "mrkdwn",
80 | "text": "${{ github.event.release.html_url }}"
81 | }
82 | }
83 | ]
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
92 | .DS_Store
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [PerceptionCore, Perception]
5 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Compatibility/App.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct CompatibilityApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Example/Compatibility/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Compatibility/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Example/Compatibility/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Compatibility/Compatibility.swift:
--------------------------------------------------------------------------------
1 | import Perception
2 | import SwiftUI
3 |
4 | @Observable class Model {}
5 |
6 | func testUnqualifiedBindable() {
7 | @Bindable var model = Model()
8 | }
9 |
--------------------------------------------------------------------------------
/Example/Compatibility/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | CA114E912AE1FC29004844BE /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA114E902AE1FC29004844BE /* ExampleApp.swift */; };
11 | CA114E932AE1FC29004844BE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA114E922AE1FC29004844BE /* ContentView.swift */; };
12 | CA114E952AE1FC2A004844BE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA114E942AE1FC2A004844BE /* Assets.xcassets */; };
13 | CA114EBE2AE1FCD7004844BE /* Perception in Frameworks */ = {isa = PBXBuildFile; productRef = CA114EBD2AE1FCD7004844BE /* Perception */; };
14 | DC40DDAC2C0F98DF00B70F53 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC40DDAB2C0F98DF00B70F53 /* App.swift */; };
15 | DC40DDAE2C0F98DF00B70F53 /* Compatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC40DDAD2C0F98DF00B70F53 /* Compatibility.swift */; };
16 | DC40DDB02C0F98E100B70F53 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC40DDAF2C0F98E100B70F53 /* Assets.xcassets */; };
17 | DC40DDB32C0F98E100B70F53 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC40DDB22C0F98E100B70F53 /* Preview Assets.xcassets */; };
18 | DC40DDD42C0F991D00B70F53 /* Perception in Frameworks */ = {isa = PBXBuildFile; productRef = DC40DDD32C0F991D00B70F53 /* Perception */; };
19 | /* End PBXBuildFile section */
20 |
21 | /* Begin PBXFileReference section */
22 | CA114E8D2AE1FC29004844BE /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
23 | CA114E902AE1FC29004844BE /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
24 | CA114E922AE1FC29004844BE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
25 | CA114E942AE1FC2A004844BE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
26 | CA114EBA2AE1FC71004844BE /* swift-perception */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "swift-perception"; path = ..; sourceTree = ""; };
27 | CA114EBB2AE1FCCE004844BE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
28 | DC40DDA92C0F98DF00B70F53 /* Compatibility.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Compatibility.app; sourceTree = BUILT_PRODUCTS_DIR; };
29 | DC40DDAB2C0F98DF00B70F53 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
30 | DC40DDAD2C0F98DF00B70F53 /* Compatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compatibility.swift; sourceTree = ""; };
31 | DC40DDAF2C0F98E100B70F53 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
32 | DC40DDB22C0F98E100B70F53 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
33 | /* End PBXFileReference section */
34 |
35 | /* Begin PBXFrameworksBuildPhase section */
36 | CA114E8A2AE1FC29004844BE /* Frameworks */ = {
37 | isa = PBXFrameworksBuildPhase;
38 | buildActionMask = 2147483647;
39 | files = (
40 | CA114EBE2AE1FCD7004844BE /* Perception in Frameworks */,
41 | );
42 | runOnlyForDeploymentPostprocessing = 0;
43 | };
44 | DC40DDA62C0F98DF00B70F53 /* Frameworks */ = {
45 | isa = PBXFrameworksBuildPhase;
46 | buildActionMask = 2147483647;
47 | files = (
48 | DC40DDD42C0F991D00B70F53 /* Perception in Frameworks */,
49 | );
50 | runOnlyForDeploymentPostprocessing = 0;
51 | };
52 | /* End PBXFrameworksBuildPhase section */
53 |
54 | /* Begin PBXGroup section */
55 | CA114E842AE1FC28004844BE = {
56 | isa = PBXGroup;
57 | children = (
58 | CA114EBA2AE1FC71004844BE /* swift-perception */,
59 | CA114E8F2AE1FC29004844BE /* Example */,
60 | DC40DDAA2C0F98DF00B70F53 /* Compatibility */,
61 | CA114E8E2AE1FC29004844BE /* Products */,
62 | CA114EBC2AE1FCD7004844BE /* Frameworks */,
63 | );
64 | sourceTree = "";
65 | };
66 | CA114E8E2AE1FC29004844BE /* Products */ = {
67 | isa = PBXGroup;
68 | children = (
69 | CA114E8D2AE1FC29004844BE /* Example.app */,
70 | DC40DDA92C0F98DF00B70F53 /* Compatibility.app */,
71 | );
72 | name = Products;
73 | sourceTree = "";
74 | };
75 | CA114E8F2AE1FC29004844BE /* Example */ = {
76 | isa = PBXGroup;
77 | children = (
78 | CA114EBB2AE1FCCE004844BE /* Info.plist */,
79 | CA114E902AE1FC29004844BE /* ExampleApp.swift */,
80 | CA114E922AE1FC29004844BE /* ContentView.swift */,
81 | CA114E942AE1FC2A004844BE /* Assets.xcassets */,
82 | );
83 | path = Example;
84 | sourceTree = "";
85 | };
86 | CA114EBC2AE1FCD7004844BE /* Frameworks */ = {
87 | isa = PBXGroup;
88 | children = (
89 | );
90 | name = Frameworks;
91 | sourceTree = "";
92 | };
93 | DC40DDAA2C0F98DF00B70F53 /* Compatibility */ = {
94 | isa = PBXGroup;
95 | children = (
96 | DC40DDAB2C0F98DF00B70F53 /* App.swift */,
97 | DC40DDAD2C0F98DF00B70F53 /* Compatibility.swift */,
98 | DC40DDAF2C0F98E100B70F53 /* Assets.xcassets */,
99 | DC40DDB12C0F98E100B70F53 /* Preview Content */,
100 | );
101 | path = Compatibility;
102 | sourceTree = "";
103 | };
104 | DC40DDB12C0F98E100B70F53 /* Preview Content */ = {
105 | isa = PBXGroup;
106 | children = (
107 | DC40DDB22C0F98E100B70F53 /* Preview Assets.xcassets */,
108 | );
109 | path = "Preview Content";
110 | sourceTree = "";
111 | };
112 | /* End PBXGroup section */
113 |
114 | /* Begin PBXNativeTarget section */
115 | CA114E8C2AE1FC29004844BE /* Example */ = {
116 | isa = PBXNativeTarget;
117 | buildConfigurationList = CA114EB12AE1FC2A004844BE /* Build configuration list for PBXNativeTarget "Example" */;
118 | buildPhases = (
119 | CA114E892AE1FC29004844BE /* Sources */,
120 | CA114E8A2AE1FC29004844BE /* Frameworks */,
121 | CA114E8B2AE1FC29004844BE /* Resources */,
122 | );
123 | buildRules = (
124 | );
125 | dependencies = (
126 | );
127 | name = Example;
128 | packageProductDependencies = (
129 | CA114EBD2AE1FCD7004844BE /* Perception */,
130 | );
131 | productName = Example;
132 | productReference = CA114E8D2AE1FC29004844BE /* Example.app */;
133 | productType = "com.apple.product-type.application";
134 | };
135 | DC40DDA82C0F98DF00B70F53 /* Compatibility */ = {
136 | isa = PBXNativeTarget;
137 | buildConfigurationList = DC40DDD02C0F98E100B70F53 /* Build configuration list for PBXNativeTarget "Compatibility" */;
138 | buildPhases = (
139 | DC40DDA52C0F98DF00B70F53 /* Sources */,
140 | DC40DDA62C0F98DF00B70F53 /* Frameworks */,
141 | DC40DDA72C0F98DF00B70F53 /* Resources */,
142 | );
143 | buildRules = (
144 | );
145 | dependencies = (
146 | );
147 | name = Compatibility;
148 | packageProductDependencies = (
149 | DC40DDD32C0F991D00B70F53 /* Perception */,
150 | );
151 | productName = Compatibility;
152 | productReference = DC40DDA92C0F98DF00B70F53 /* Compatibility.app */;
153 | productType = "com.apple.product-type.application";
154 | };
155 | /* End PBXNativeTarget section */
156 |
157 | /* Begin PBXProject section */
158 | CA114E852AE1FC28004844BE /* Project object */ = {
159 | isa = PBXProject;
160 | attributes = {
161 | BuildIndependentTargetsInParallel = 1;
162 | LastSwiftUpdateCheck = 1540;
163 | LastUpgradeCheck = 1500;
164 | TargetAttributes = {
165 | CA114E8C2AE1FC29004844BE = {
166 | CreatedOnToolsVersion = 15.0;
167 | };
168 | DC40DDA82C0F98DF00B70F53 = {
169 | CreatedOnToolsVersion = 15.4;
170 | };
171 | };
172 | };
173 | buildConfigurationList = CA114E882AE1FC29004844BE /* Build configuration list for PBXProject "Example" */;
174 | compatibilityVersion = "Xcode 14.0";
175 | developmentRegion = en;
176 | hasScannedForEncodings = 0;
177 | knownRegions = (
178 | en,
179 | Base,
180 | );
181 | mainGroup = CA114E842AE1FC28004844BE;
182 | productRefGroup = CA114E8E2AE1FC29004844BE /* Products */;
183 | projectDirPath = "";
184 | projectRoot = "";
185 | targets = (
186 | CA114E8C2AE1FC29004844BE /* Example */,
187 | DC40DDA82C0F98DF00B70F53 /* Compatibility */,
188 | );
189 | };
190 | /* End PBXProject section */
191 |
192 | /* Begin PBXResourcesBuildPhase section */
193 | CA114E8B2AE1FC29004844BE /* Resources */ = {
194 | isa = PBXResourcesBuildPhase;
195 | buildActionMask = 2147483647;
196 | files = (
197 | CA114E952AE1FC2A004844BE /* Assets.xcassets in Resources */,
198 | );
199 | runOnlyForDeploymentPostprocessing = 0;
200 | };
201 | DC40DDA72C0F98DF00B70F53 /* Resources */ = {
202 | isa = PBXResourcesBuildPhase;
203 | buildActionMask = 2147483647;
204 | files = (
205 | DC40DDB32C0F98E100B70F53 /* Preview Assets.xcassets in Resources */,
206 | DC40DDB02C0F98E100B70F53 /* Assets.xcassets in Resources */,
207 | );
208 | runOnlyForDeploymentPostprocessing = 0;
209 | };
210 | /* End PBXResourcesBuildPhase section */
211 |
212 | /* Begin PBXSourcesBuildPhase section */
213 | CA114E892AE1FC29004844BE /* Sources */ = {
214 | isa = PBXSourcesBuildPhase;
215 | buildActionMask = 2147483647;
216 | files = (
217 | CA114E932AE1FC29004844BE /* ContentView.swift in Sources */,
218 | CA114E912AE1FC29004844BE /* ExampleApp.swift in Sources */,
219 | );
220 | runOnlyForDeploymentPostprocessing = 0;
221 | };
222 | DC40DDA52C0F98DF00B70F53 /* Sources */ = {
223 | isa = PBXSourcesBuildPhase;
224 | buildActionMask = 2147483647;
225 | files = (
226 | DC40DDAE2C0F98DF00B70F53 /* Compatibility.swift in Sources */,
227 | DC40DDAC2C0F98DF00B70F53 /* App.swift in Sources */,
228 | );
229 | runOnlyForDeploymentPostprocessing = 0;
230 | };
231 | /* End PBXSourcesBuildPhase section */
232 |
233 | /* Begin XCBuildConfiguration section */
234 | CA114EAF2AE1FC2A004844BE /* Debug */ = {
235 | isa = XCBuildConfiguration;
236 | buildSettings = {
237 | ALWAYS_SEARCH_USER_PATHS = NO;
238 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
239 | CLANG_ANALYZER_NONNULL = YES;
240 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
242 | CLANG_ENABLE_MODULES = YES;
243 | CLANG_ENABLE_OBJC_ARC = YES;
244 | CLANG_ENABLE_OBJC_WEAK = YES;
245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
246 | CLANG_WARN_BOOL_CONVERSION = YES;
247 | CLANG_WARN_COMMA = YES;
248 | CLANG_WARN_CONSTANT_CONVERSION = YES;
249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
252 | CLANG_WARN_EMPTY_BODY = YES;
253 | CLANG_WARN_ENUM_CONVERSION = YES;
254 | CLANG_WARN_INFINITE_RECURSION = YES;
255 | CLANG_WARN_INT_CONVERSION = YES;
256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
260 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
262 | CLANG_WARN_STRICT_PROTOTYPES = YES;
263 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
265 | CLANG_WARN_UNREACHABLE_CODE = YES;
266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
267 | COPY_PHASE_STRIP = NO;
268 | DEBUG_INFORMATION_FORMAT = dwarf;
269 | ENABLE_STRICT_OBJC_MSGSEND = YES;
270 | ENABLE_TESTABILITY = YES;
271 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
272 | GCC_C_LANGUAGE_STANDARD = gnu17;
273 | GCC_DYNAMIC_NO_PIC = NO;
274 | GCC_NO_COMMON_BLOCKS = YES;
275 | GCC_OPTIMIZATION_LEVEL = 0;
276 | GCC_PREPROCESSOR_DEFINITIONS = (
277 | "DEBUG=1",
278 | "$(inherited)",
279 | );
280 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
281 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
282 | GCC_WARN_UNDECLARED_SELECTOR = YES;
283 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
284 | GCC_WARN_UNUSED_FUNCTION = YES;
285 | GCC_WARN_UNUSED_VARIABLE = YES;
286 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
287 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
289 | MTL_FAST_MATH = YES;
290 | ONLY_ACTIVE_ARCH = YES;
291 | SDKROOT = iphoneos;
292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
294 | SWIFT_STRICT_CONCURRENCY = complete;
295 | };
296 | name = Debug;
297 | };
298 | CA114EB02AE1FC2A004844BE /* Release */ = {
299 | isa = XCBuildConfiguration;
300 | buildSettings = {
301 | ALWAYS_SEARCH_USER_PATHS = NO;
302 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
303 | CLANG_ANALYZER_NONNULL = YES;
304 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
306 | CLANG_ENABLE_MODULES = YES;
307 | CLANG_ENABLE_OBJC_ARC = YES;
308 | CLANG_ENABLE_OBJC_WEAK = YES;
309 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
310 | CLANG_WARN_BOOL_CONVERSION = YES;
311 | CLANG_WARN_COMMA = YES;
312 | CLANG_WARN_CONSTANT_CONVERSION = YES;
313 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
315 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
316 | CLANG_WARN_EMPTY_BODY = YES;
317 | CLANG_WARN_ENUM_CONVERSION = YES;
318 | CLANG_WARN_INFINITE_RECURSION = YES;
319 | CLANG_WARN_INT_CONVERSION = YES;
320 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
321 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
322 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
323 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
324 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
326 | CLANG_WARN_STRICT_PROTOTYPES = YES;
327 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
328 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
329 | CLANG_WARN_UNREACHABLE_CODE = YES;
330 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
331 | COPY_PHASE_STRIP = NO;
332 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
333 | ENABLE_NS_ASSERTIONS = NO;
334 | ENABLE_STRICT_OBJC_MSGSEND = YES;
335 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
336 | GCC_C_LANGUAGE_STANDARD = gnu17;
337 | GCC_NO_COMMON_BLOCKS = YES;
338 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
339 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
340 | GCC_WARN_UNDECLARED_SELECTOR = YES;
341 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
342 | GCC_WARN_UNUSED_FUNCTION = YES;
343 | GCC_WARN_UNUSED_VARIABLE = YES;
344 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
345 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
346 | MTL_ENABLE_DEBUG_INFO = NO;
347 | MTL_FAST_MATH = YES;
348 | SDKROOT = iphoneos;
349 | SWIFT_COMPILATION_MODE = wholemodule;
350 | SWIFT_STRICT_CONCURRENCY = complete;
351 | VALIDATE_PRODUCT = YES;
352 | };
353 | name = Release;
354 | };
355 | CA114EB22AE1FC2A004844BE /* Debug */ = {
356 | isa = XCBuildConfiguration;
357 | buildSettings = {
358 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
359 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
360 | CODE_SIGN_STYLE = Automatic;
361 | CURRENT_PROJECT_VERSION = 1;
362 | ENABLE_PREVIEWS = YES;
363 | GENERATE_INFOPLIST_FILE = YES;
364 | INFOPLIST_FILE = Example/Info.plist;
365 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
366 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
367 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
368 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
369 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
370 | LD_RUNPATH_SEARCH_PATHS = (
371 | "$(inherited)",
372 | "@executable_path/Frameworks",
373 | );
374 | MARKETING_VERSION = 1.0;
375 | PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Example;
376 | PRODUCT_NAME = "$(TARGET_NAME)";
377 | SWIFT_EMIT_LOC_STRINGS = YES;
378 | SWIFT_VERSION = 5.0;
379 | TARGETED_DEVICE_FAMILY = "1,2";
380 | };
381 | name = Debug;
382 | };
383 | CA114EB32AE1FC2A004844BE /* Release */ = {
384 | isa = XCBuildConfiguration;
385 | buildSettings = {
386 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
387 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
388 | CODE_SIGN_STYLE = Automatic;
389 | CURRENT_PROJECT_VERSION = 1;
390 | ENABLE_PREVIEWS = YES;
391 | GENERATE_INFOPLIST_FILE = YES;
392 | INFOPLIST_FILE = Example/Info.plist;
393 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
394 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
395 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
396 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
397 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
398 | LD_RUNPATH_SEARCH_PATHS = (
399 | "$(inherited)",
400 | "@executable_path/Frameworks",
401 | );
402 | MARKETING_VERSION = 1.0;
403 | PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Example;
404 | PRODUCT_NAME = "$(TARGET_NAME)";
405 | SWIFT_EMIT_LOC_STRINGS = YES;
406 | SWIFT_VERSION = 5.0;
407 | TARGETED_DEVICE_FAMILY = "1,2";
408 | };
409 | name = Release;
410 | };
411 | DC40DDCA2C0F98E100B70F53 /* Debug */ = {
412 | isa = XCBuildConfiguration;
413 | buildSettings = {
414 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
415 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
416 | CODE_SIGN_STYLE = Automatic;
417 | CURRENT_PROJECT_VERSION = 1;
418 | DEVELOPMENT_ASSET_PATHS = "\"Compatibility/Preview Content\"";
419 | ENABLE_PREVIEWS = YES;
420 | GENERATE_INFOPLIST_FILE = YES;
421 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
422 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
423 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
424 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
425 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
426 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
427 | LD_RUNPATH_SEARCH_PATHS = (
428 | "$(inherited)",
429 | "@executable_path/Frameworks",
430 | );
431 | MARKETING_VERSION = 1.0;
432 | PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Compatibility;
433 | PRODUCT_NAME = "$(TARGET_NAME)";
434 | SWIFT_EMIT_LOC_STRINGS = YES;
435 | SWIFT_VERSION = 5.0;
436 | TARGETED_DEVICE_FAMILY = "1,2";
437 | };
438 | name = Debug;
439 | };
440 | DC40DDCB2C0F98E100B70F53 /* Release */ = {
441 | isa = XCBuildConfiguration;
442 | buildSettings = {
443 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
444 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
445 | CODE_SIGN_STYLE = Automatic;
446 | CURRENT_PROJECT_VERSION = 1;
447 | DEVELOPMENT_ASSET_PATHS = "\"Compatibility/Preview Content\"";
448 | ENABLE_PREVIEWS = YES;
449 | GENERATE_INFOPLIST_FILE = YES;
450 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
451 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
452 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
453 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
454 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
455 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
456 | LD_RUNPATH_SEARCH_PATHS = (
457 | "$(inherited)",
458 | "@executable_path/Frameworks",
459 | );
460 | MARKETING_VERSION = 1.0;
461 | PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Compatibility;
462 | PRODUCT_NAME = "$(TARGET_NAME)";
463 | SWIFT_EMIT_LOC_STRINGS = YES;
464 | SWIFT_VERSION = 5.0;
465 | TARGETED_DEVICE_FAMILY = "1,2";
466 | };
467 | name = Release;
468 | };
469 | /* End XCBuildConfiguration section */
470 |
471 | /* Begin XCConfigurationList section */
472 | CA114E882AE1FC29004844BE /* Build configuration list for PBXProject "Example" */ = {
473 | isa = XCConfigurationList;
474 | buildConfigurations = (
475 | CA114EAF2AE1FC2A004844BE /* Debug */,
476 | CA114EB02AE1FC2A004844BE /* Release */,
477 | );
478 | defaultConfigurationIsVisible = 0;
479 | defaultConfigurationName = Release;
480 | };
481 | CA114EB12AE1FC2A004844BE /* Build configuration list for PBXNativeTarget "Example" */ = {
482 | isa = XCConfigurationList;
483 | buildConfigurations = (
484 | CA114EB22AE1FC2A004844BE /* Debug */,
485 | CA114EB32AE1FC2A004844BE /* Release */,
486 | );
487 | defaultConfigurationIsVisible = 0;
488 | defaultConfigurationName = Release;
489 | };
490 | DC40DDD02C0F98E100B70F53 /* Build configuration list for PBXNativeTarget "Compatibility" */ = {
491 | isa = XCConfigurationList;
492 | buildConfigurations = (
493 | DC40DDCA2C0F98E100B70F53 /* Debug */,
494 | DC40DDCB2C0F98E100B70F53 /* Release */,
495 | );
496 | defaultConfigurationIsVisible = 0;
497 | defaultConfigurationName = Release;
498 | };
499 | /* End XCConfigurationList section */
500 |
501 | /* Begin XCSwiftPackageProductDependency section */
502 | CA114EBD2AE1FCD7004844BE /* Perception */ = {
503 | isa = XCSwiftPackageProductDependency;
504 | productName = Perception;
505 | };
506 | DC40DDD32C0F991D00B70F53 /* Perception */ = {
507 | isa = XCSwiftPackageProductDependency;
508 | productName = Perception;
509 | };
510 | /* End XCSwiftPackageProductDependency section */
511 | };
512 | rootObject = CA114E852AE1FC28004844BE /* Project object */;
513 | }
514 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-macro-testing",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/pointfreeco/swift-macro-testing",
7 | "state" : {
8 | "revision" : "a35257b7e9ce44e92636447003a8eeefb77b145c",
9 | "version" : "0.5.1"
10 | }
11 | },
12 | {
13 | "identity" : "swift-snapshot-testing",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
16 | "state" : {
17 | "revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3",
18 | "version" : "1.17.2"
19 | }
20 | },
21 | {
22 | "identity" : "swift-syntax",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/swiftlang/swift-syntax",
25 | "state" : {
26 | "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c",
27 | "version" : "600.0.0-prerelease-2024-06-12"
28 | }
29 | },
30 | {
31 | "identity" : "xctest-dynamic-overlay",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
34 | "state" : {
35 | "revision" : "357ca1e5dd31f613a1d43320870ebc219386a495",
36 | "version" : "1.2.2"
37 | }
38 | }
39 | ],
40 | "version" : 2
41 | }
42 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
35 |
41 |
42 |
43 |
46 |
52 |
53 |
54 |
57 |
63 |
64 |
65 |
68 |
74 |
75 |
76 |
77 |
78 |
88 |
90 |
96 |
97 |
98 |
99 |
105 |
107 |
113 |
114 |
115 |
116 |
118 |
119 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pointfreeco/swift-perception/d924c62a70fca5f43872f286dbd7cef0957f1c01/Example/Example/Assets.xcassets/.DS_Store
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/ContentView.swift:
--------------------------------------------------------------------------------
1 | import Perception
2 | import SwiftUI
3 |
4 | @Perceptible
5 | class CounterModel {
6 | var count = 0
7 | var isDisplayingCount = true
8 | var isPresentingSheet = false
9 | var text = ""
10 | func decrementButtonTapped() {
11 | withAnimation {
12 | count -= 1
13 | }
14 | }
15 | func incrementButtonTapped() {
16 | withAnimation {
17 | count += 1
18 | }
19 | }
20 | func presentSheetButtonTapped() {
21 | isPresentingSheet = true
22 | }
23 | }
24 |
25 | struct ContentView: View {
26 | @Perception.Bindable var model: CounterModel
27 |
28 | var body: some View {
29 | WithPerceptionTracking {
30 | let _ = print("\(Self.self): tracked change.")
31 | Form {
32 | TextField("Text", text: $model.text)
33 | if model.isDisplayingCount {
34 | Text(model.count.description)
35 | .font(.largeTitle)
36 | } else {
37 | Text("Not tracking count")
38 | .font(.largeTitle)
39 | }
40 | Button("Decrement") { model.decrementButtonTapped() }
41 | Button("Increment") { model.incrementButtonTapped() }
42 | Toggle(isOn: $model.isDisplayingCount.animation()) {
43 | Text("Display count?")
44 | }
45 | Button("Present sheet") {
46 | model.presentSheetButtonTapped()
47 | }
48 | }
49 | .sheet(isPresented: $model.isPresentingSheet) {
50 | if #available(iOS 16.0, *) {
51 | WithPerceptionTracking {
52 | Form {
53 | Text(model.count.description)
54 | .font(.largeTitle)
55 | Button("Decrement") { model.decrementButtonTapped() }
56 | Button("Increment") { model.incrementButtonTapped() }
57 | }
58 | .presentationDetents([.medium])
59 | }
60 | } else {
61 | WithPerceptionTracking {
62 | Form {
63 | Text(model.count.description)
64 | Button("Decrement") { model.decrementButtonTapped() }
65 | Button("Increment") { model.incrementButtonTapped() }
66 | }
67 | }
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | #Preview {
75 | ContentView(model: CounterModel())
76 | }
77 |
--------------------------------------------------------------------------------
/Example/Example/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct ExampleApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView(model: CounterModel())
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Example/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Example/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "blank",
7 | products: [],
8 | targets: []
9 | )
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Point-Free
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS 17.2,iPhone \d\+ Pro [^M])
2 |
3 | test-compatibility:
4 | xcodebuild \
5 | -skipMacroValidation \
6 | -project Example/Example.xcodeproj \
7 | -scheme Compatibility \
8 | -destination generic/platform="$(PLATFORM_IOS)"
9 |
10 | format:
11 | find . \
12 | -path '*/Documentation.docc' -prune -o \
13 | -name '*.swift' \
14 | -not -path '*/.*' -print0 \
15 | | xargs -0 swift format --ignore-unparsable-files --in-place
16 |
17 | .PHONY: format test-compatibility
18 |
19 | define udid_for
20 | $(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')
21 | endef
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-macro-testing",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/pointfreeco/swift-macro-testing",
7 | "state" : {
8 | "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4",
9 | "version" : "0.5.2"
10 | }
11 | },
12 | {
13 | "identity" : "swift-snapshot-testing",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
16 | "state" : {
17 | "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7",
18 | "version" : "1.17.6"
19 | }
20 | },
21 | {
22 | "identity" : "swift-syntax",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/swiftlang/swift-syntax",
25 | "state" : {
26 | "revision" : "0687f71944021d616d34d922343dcef086855920",
27 | "version" : "600.0.1"
28 | }
29 | },
30 | {
31 | "identity" : "xctest-dynamic-overlay",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
34 | "state" : {
35 | "revision" : "770f990d3e4eececb57ac04a6076e22f8c97daeb",
36 | "version" : "1.4.2"
37 | }
38 | }
39 | ],
40 | "version" : 2
41 | }
42 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 |
3 | import CompilerPluginSupport
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "swift-perception",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v10_15),
11 | .tvOS(.v13),
12 | .watchOS(.v6),
13 | ],
14 | products: [
15 | .library(name: "Perception", targets: ["Perception"]),
16 | .library(name: "PerceptionCore", targets: ["PerceptionCore"]),
17 | ],
18 | dependencies: [
19 | .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.1.0"),
20 | .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"602.0.0"),
21 | .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
22 | ],
23 | targets: [
24 | .target(
25 | name: "Perception",
26 | dependencies: [
27 | "PerceptionCore",
28 | "PerceptionMacros",
29 | ]
30 | ),
31 | .target(
32 | name: "PerceptionCore",
33 | dependencies: [
34 | .product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
35 | ]
36 | ),
37 | .testTarget(
38 | name: "PerceptionTests",
39 | dependencies: ["Perception"]
40 | ),
41 |
42 | .macro(
43 | name: "PerceptionMacros",
44 | dependencies: [
45 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
46 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
47 | ]
48 | ),
49 | .testTarget(
50 | name: "PerceptionMacrosTests",
51 | dependencies: [
52 | "PerceptionMacros",
53 | .product(name: "MacroTesting", package: "swift-macro-testing"),
54 | ]
55 | ),
56 | ]
57 | )
58 |
59 | for target in package.targets where target.type != .system {
60 | target.swiftSettings = target.swiftSettings ?? []
61 | target.swiftSettings?.append(contentsOf: [
62 | .enableExperimentalFeature("StrictConcurrency"),
63 | ])
64 | }
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Perception
2 |
3 | [](https://github.com/pointfreeco/swift-perception/actions/workflows/ci.yml)
4 | [](https://www.pointfree.co/slack-invite)
5 | [](https://swiftpackageindex.com/pointfreeco/swift-perception)
6 | [](https://swiftpackageindex.com/pointfreeco/swift-perception)
7 |
8 | Observation tools for platforms that do not officially support observation.
9 |
10 | ## Learn More
11 |
12 | This library was created by [Brandon Williams][mbrandonw] and [Stephen Celis][stephencelis], who
13 | host the [Point-Free][pointfreeco] video series which explores advanced Swift language concepts.
14 |
15 |
16 |
17 |
18 |
19 | ## Overview
20 |
21 | The Perception library provides tools that mimic `@Observable` and `withObservationTracking` in
22 | Swift 5.9, but they are backported to work all the way back to iOS 13, macOS 10.15, tvOS 13 and
23 | watchOS 6. This means you can start taking advantage of Swift 5.9's observation tools today,
24 | even if you can't drop support for older Apple platforms. Using this library's tools works almost
25 | exactly as using the official tools, but with one small exception.
26 |
27 | To begin, mark a class as being observable by using the `@Perceptible` macro instead of the
28 | `@Observable` macro:
29 |
30 | ```swift
31 | @Perceptible
32 | class FeatureModel {
33 | var count = 0
34 | }
35 | ```
36 |
37 | Then you can hold onto a perceptible model in your view using a regular `let` property:
38 |
39 | ```swift
40 | struct FeatureView: View {
41 | let model: FeatureModel
42 |
43 | // ...
44 | }
45 | ```
46 |
47 | And in the view's `body` you must wrap your content using the `WithPerceptionTracking` view in
48 | order for observation to be correctly hooked up:
49 |
50 | ```swift
51 | struct FeatureView: View {
52 | let model: FeatureModel
53 |
54 | var body: some View {
55 | WithPerceptionTracking {
56 | Form {
57 | Text(model.count.description)
58 | Button("Increment") { model.count += 1 }
59 | }
60 | }
61 | }
62 | }
63 | ```
64 |
65 | It's unfortunate to have to wrap your view's content in `WithPerceptionTracking`, however if you
66 | forget then you will helpfully get a runtime warning letting you know that observation is not
67 | set up correctly:
68 |
69 | > 🟣 Runtime Warning: Perceptible state was accessed but is not being tracked. Track changes
70 | > to state by wrapping your view in a 'WithPerceptionTracking' view. This must also be done
71 | > for any escaping, trailing closures, such as 'GeometryReader', `LazyVStack` (and all lazy
72 | > views), navigation APIs ('sheet', 'popover', 'fullScreenCover', etc.), and others.
73 |
74 | ### Bindable
75 |
76 | SwiftUI's `@Bindable` property wrapper has also been backported to support perceptible objects. You
77 | can simply qualify the property wrapper with the `Perception` module:
78 |
79 | ```swift
80 | struct FeatureView: View {
81 | @Perception.Bindable var model: FeatureModel
82 |
83 | // ...
84 | }
85 | ```
86 |
87 | ### Environment
88 |
89 | SwiftUI's `@Environment` property wrapper and `environment` view modifier's support for observation
90 | has also been backported to support perceptible objects using the exact same APIs:
91 |
92 | ```swift
93 | struct FeatureView: View {
94 | @Environment(Settings.self) var settings
95 |
96 | // ...
97 | }
98 |
99 | // In some parent view:
100 | .environment(settings)
101 | ```
102 |
103 | ## Community
104 |
105 | If you want to discuss this library or have a question about how to use it to solve
106 | a particular problem, there are a number of places you can discuss with fellow
107 | [Point-Free](https://www.pointfree.co) enthusiasts:
108 |
109 | * For long-form discussions, we recommend the
110 | [discussions](https://github.com/pointfreeco/swift-perception/discussions) tab of this repo.
111 | * For casual chat, we recommend the [Point-Free Community Slack](https://pointfree.co/slack-invite).
112 |
113 | ## Documentation
114 |
115 | The latest documentation for the Perception APIs is available [here][docs].
116 |
117 | ## License
118 |
119 | This library is released under the MIT license. See [LICENSE](LICENSE) for details.
120 |
121 | [pointfreeco]: https://www.pointfree.co
122 | [mbrandonw]: https://twitter.com/mbrandonw
123 | [stephencelis]: https://twitter.com/stephencelis
124 | [docs]: https://swiftpackageindex.com/pointfreeco/swift-perception/main/documentation/perception
125 |
--------------------------------------------------------------------------------
/Sources/Perception/Exports.swift:
--------------------------------------------------------------------------------
1 | @_exported import PerceptionCore
2 |
--------------------------------------------------------------------------------
/Sources/Perception/Macros.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | //
10 | //===----------------------------------------------------------------------===//
11 |
12 | @available(iOS, deprecated: 17, renamed: "Observable")
13 | @available(macOS, deprecated: 14, renamed: "Observable")
14 | @available(watchOS, deprecated: 10, renamed: "Observable")
15 | @available(tvOS, deprecated: 17, renamed: "Observable")
16 | @attached(
17 | member, names: named(_$id), named(_$perceptionRegistrar), named(access), named(withMutation))
18 | @attached(memberAttribute)
19 | @attached(extension, conformances: Perceptible, Observable)
20 | public macro Perceptible() =
21 | #externalMacro(module: "PerceptionMacros", type: "PerceptibleMacro")
22 |
23 | @available(iOS, deprecated: 17, renamed: "ObservationTracked")
24 | @available(macOS, deprecated: 14, renamed: "ObservationTracked")
25 | @available(watchOS, deprecated: 10, renamed: "ObservationTracked")
26 | @available(tvOS, deprecated: 17, renamed: "ObservationTracked")
27 | @attached(accessor, names: named(init), named(get), named(set), named(_modify))
28 | @attached(peer, names: prefixed(_))
29 | public macro PerceptionTracked() =
30 | #externalMacro(module: "PerceptionMacros", type: "PerceptionTrackedMacro")
31 |
32 | @available(iOS, deprecated: 17, renamed: "ObservationIgnored")
33 | @available(macOS, deprecated: 14, renamed: "ObservationIgnored")
34 | @available(watchOS, deprecated: 10, renamed: "ObservationIgnored")
35 | @available(tvOS, deprecated: 17, renamed: "ObservationIgnored")
36 | @attached(accessor, names: named(willSet))
37 | public macro PerceptionIgnored() =
38 | #externalMacro(module: "PerceptionMacros", type: "PerceptionIgnoredMacro")
39 |
--------------------------------------------------------------------------------
/Sources/PerceptionCore/Bindable.swift:
--------------------------------------------------------------------------------
1 | #if canImport(SwiftUI)
2 | import SwiftUI
3 |
4 | /// A property wrapper type that supports creating bindings to the mutable properties of
5 | /// perceptible objects.
6 | ///
7 | /// A backport of SwiftUI's `Bindable` property wrapper.
8 | @available(iOS, introduced: 13, obsoleted: 17, message: "Use @Bindable without the 'Perception.' prefix.")
9 | @available(macOS, introduced: 10.15, obsoleted: 14, message: "Use @Bindable without the 'Perception.' prefix.")
10 | @available(tvOS, introduced: 13, obsoleted: 17, message: "Use @Bindable without the 'Perception.' prefix.")
11 | @available(watchOS, introduced: 6, obsoleted: 10, message: "Use @Bindable without the 'Perception.' prefix.")
12 | @available(visionOS, unavailable)
13 | @dynamicMemberLookup
14 | @propertyWrapper
15 | public struct Bindable {
16 | @ObservedObject fileprivate var observer: Observer
17 |
18 | /// The wrapped object.
19 | public var wrappedValue: Value {
20 | get { self.observer.object }
21 | set { self.observer.object = newValue }
22 | }
23 |
24 | /// The bindable wrapper for the object that creates bindings to its properties using dynamic
25 | /// member lookup.
26 | public var projectedValue: Bindable {
27 | self
28 | }
29 |
30 | /// Returns a binding to the value of a given key path.
31 | public subscript(
32 | dynamicMember keyPath: ReferenceWritableKeyPath
33 | ) -> Binding where Value: AnyObject {
34 | withPerceptionTracking {
35 | self.$observer[dynamicMember: (\Observer.object).appending(path: keyPath)]
36 | } onChange: { [send = UncheckedSendable(self.observer.objectWillChange.send)] in
37 | send.value()
38 | }
39 | }
40 |
41 | /// Creates a bindable object from an observable object.
42 | public init(wrappedValue: Value) where Value: AnyObject & Perceptible {
43 | self.observer = Observer(wrappedValue)
44 | }
45 |
46 | /// Creates a bindable object from an observable object.
47 | public init(_ wrappedValue: Value) where Value: AnyObject & Perceptible {
48 | self.init(wrappedValue: wrappedValue)
49 | }
50 |
51 | /// Creates a bindable from the value of another bindable.
52 | public init(projectedValue: Bindable) where Value: AnyObject & Perceptible {
53 | self = projectedValue
54 | }
55 | }
56 |
57 | @available(visionOS, unavailable)
58 | extension Bindable: Identifiable where Value: Identifiable {
59 | public var id: Value.ID {
60 | wrappedValue.id
61 | }
62 | }
63 |
64 | @available(visionOS, unavailable)
65 | extension Bindable: Sendable where Value: Sendable {}
66 |
67 | private final class Observer